1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.databind.model.impl;
7   
8   import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
9   import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
10  import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline;
11  import gov.nist.secauto.metaschema.core.model.IAttributable;
12  import gov.nist.secauto.metaschema.core.model.IBoundObject;
13  import gov.nist.secauto.metaschema.core.model.constraint.AssemblyConstraintSet;
14  import gov.nist.secauto.metaschema.core.model.constraint.IModelConstrained;
15  import gov.nist.secauto.metaschema.core.model.constraint.IValueConstrained;
16  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
17  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
18  import gov.nist.secauto.metaschema.databind.IBindingContext;
19  import gov.nist.secauto.metaschema.databind.io.BindingException;
20  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
21  import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue;
22  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceFlag;
23  import gov.nist.secauto.metaschema.databind.model.IBoundModule;
24  import gov.nist.secauto.metaschema.databind.model.IBoundProperty;
25  import gov.nist.secauto.metaschema.databind.model.annotations.BoundFieldValue;
26  import gov.nist.secauto.metaschema.databind.model.annotations.Ignore;
27  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
28  import gov.nist.secauto.metaschema.databind.model.annotations.ModelUtil;
29  import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints;
30  
31  import java.lang.reflect.Field;
32  import java.util.Arrays;
33  import java.util.LinkedHashMap;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.function.Predicate;
37  import java.util.stream.Collectors;
38  
39  import edu.umd.cs.findbugs.annotations.NonNull;
40  import edu.umd.cs.findbugs.annotations.Nullable;
41  import nl.talsmasoftware.lazy4j.Lazy;
42  
43  /**
44   * Implements a Metaschema module global field definition bound to a Java class.
45   */
46  @SuppressWarnings("PMD.CouplingBetweenObjects")
47  public final class DefinitionField
48      extends AbstractBoundDefinitionModelComplex<MetaschemaField>
49      implements IBoundDefinitionModelFieldComplex {
50    @NonNull
51    private final FieldValue fieldValue;
52    @Nullable
53    private IBoundInstanceFlag jsonValueKeyFlagInstance;
54    @NonNull
55    private final Lazy<FlagContainerSupport> flagContainer;
56    @NonNull
57    private final Lazy<IValueConstrained> constraints;
58    @NonNull
59    private final Lazy<Map<String, IBoundProperty<?>>> jsonProperties;
60    @NonNull
61    private final Lazy<Map<IAttributable.Key, Set<String>>> properties;
62  
63    /**
64     * Collect all fields that are part of the model for this class.
65     *
66     * @param clazz
67     *          the class
68     * @return the field value instances if found or {@code null} otherwise
69     */
70    @Nullable
71    private static Field getFieldValueField(Class<?> clazz) {
72      Field[] fields = clazz.getDeclaredFields();
73  
74      Field retval = null;
75      for (Field field : fields) {
76        if (!field.isAnnotationPresent(BoundFieldValue.class) || field.isAnnotationPresent(Ignore.class)) {
77          // skip this field, since it is ignored
78          continue;
79        }
80        retval = field;
81      }
82  
83      if (retval == null) {
84        Class<?> superClass = clazz.getSuperclass();
85        if (superClass != null) {
86          // get instances from superclass
87          retval = getFieldValueField(superClass);
88        }
89      }
90      return retval;
91    }
92  
93    /**
94     * Construct a new Metaschema module field definition.
95     *
96     * @param clazz
97     *          the Java class the definition is bound to
98     * @param annotation
99     *          the binding annotation associated with this class
100    * @param module
101    *          the module containing this class
102    * @param bindingContext
103    *          the Metaschema binding context managing this class used to lookup
104    *          binding information
105    * @return the instance
106    */
107   @NonNull
108   public static DefinitionField newInstance(
109       @NonNull Class<? extends IBoundObject> clazz,
110       @NonNull MetaschemaField annotation,
111       @NonNull IBoundModule module,
112       @NonNull IBindingContext bindingContext) {
113     return new DefinitionField(clazz, annotation, module, bindingContext);
114   }
115 
116   private DefinitionField(
117       @NonNull Class<? extends IBoundObject> clazz,
118       @NonNull MetaschemaField annotation,
119       @NonNull IBoundModule module,
120       @NonNull IBindingContext bindingContext) {
121     super(clazz, annotation, module, bindingContext);
122     Field field = getFieldValueField(getBoundClass());
123     if (field == null) {
124       throw new IllegalArgumentException(
125           String.format("Class '%s' is missing the '%s' annotation on one of its fields.",
126               clazz.getName(),
127               BoundFieldValue.class.getName())); // NOPMD false positive
128     }
129     this.fieldValue = new FieldValue(field, BoundFieldValue.class, bindingContext);
130     this.flagContainer = ObjectUtils.notNull(Lazy.lazy(() -> new FlagContainerSupport(this, this::handleFlagInstance)));
131     this.constraints = ObjectUtils.notNull(Lazy.lazy(() -> {
132       IModelConstrained retval = new AssemblyConstraintSet();
133       ValueConstraints valueAnnotation = getAnnotation().valueConstraints();
134       ConstraintSupport.parse(valueAnnotation, module.getSource(), retval);
135       return retval;
136     }));
137     this.jsonProperties = ObjectUtils.notNull(Lazy.lazy(() -> {
138       IBoundInstanceFlag jsonValueKey = getJsonValueKeyFlagInstance();
139       Predicate<IBoundInstanceFlag> flagFilter = jsonValueKey == null ? null : flag -> !flag.equals(jsonValueKey);
140       return getJsonProperties(flagFilter);
141     }));
142     this.properties = ObjectUtils.notNull(
143         Lazy.lazy(() -> CollectionUtil.unmodifiableMap(ObjectUtils.notNull(
144             Arrays.stream(annotation.properties())
145                 .map(ModelUtil::toPropertyEntry)
146                 .collect(
147                     Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new))))));
148   }
149 
150   /**
151    * A callback used to identify the JSON value key flag.
152    *
153    * @param instance
154    *          a flag instance
155    */
156   protected void handleFlagInstance(@NonNull IBoundInstanceFlag instance) {
157     if (instance.isJsonValueKey()) {
158       this.jsonValueKeyFlagInstance = instance;
159     }
160   }
161 
162   @Override
163   @NonNull
164   public FieldValue getFieldValue() {
165     return fieldValue;
166   }
167 
168   @Override
169   public IBoundInstanceFlag getJsonValueKeyFlagInstance() {
170     // lazy load flags
171     getFlagContainer();
172     return jsonValueKeyFlagInstance;
173   }
174 
175   @Override
176   protected void deepCopyItemInternal(IBoundObject fromObject, IBoundObject toObject) throws BindingException {
177     // copy the flags
178     super.deepCopyItemInternal(fromObject, toObject);
179 
180     getFieldValue().deepCopy(fromObject, toObject);
181   }
182 
183   // ------------------------------------------
184   // - Start annotation driven code - CPD-OFF -
185   // ------------------------------------------
186 
187   @Override
188   @SuppressWarnings("null")
189   @NonNull
190   public FlagContainerSupport getFlagContainer() {
191     return flagContainer.get();
192   }
193 
194   @Override
195   @NonNull
196   public IValueConstrained getConstraintSupport() {
197     return ObjectUtils.notNull(constraints.get());
198   }
199 
200   @Override
201   public Map<String, IBoundProperty<?>> getJsonProperties() {
202     return ObjectUtils.notNull(jsonProperties.get());
203   }
204 
205   @Override
206   @Nullable
207   public String getFormalName() {
208     return ModelUtil.resolveNoneOrValue(getAnnotation().formalName());
209   }
210 
211   @Override
212   @Nullable
213   public MarkupLine getDescription() {
214     return ModelUtil.resolveToMarkupLine(getAnnotation().description());
215   }
216 
217   @Override
218   @NonNull
219   public String getName() {
220     return getAnnotation().name();
221   }
222 
223   @Override
224   public Map<Key, Set<String>> getProperties() {
225     return ObjectUtils.notNull(properties.get());
226   }
227 
228   @Override
229   @Nullable
230   public Integer getIndex() {
231     return ModelUtil.resolveDefaultInteger(getAnnotation().index());
232   }
233 
234   @Override
235   @Nullable
236   public MarkupMultiline getRemarks() {
237     return ModelUtil.resolveToMarkupMultiline(getAnnotation().description());
238   }
239 
240   /**
241    * Implements a field definition value bound to a Java field.
242    */
243   protected class FieldValue
244       implements IBoundFieldValue {
245     @NonNull
246     private final Field javaField;
247     @NonNull
248     private final BoundFieldValue annotation;
249     @NonNull
250     private final IDataTypeAdapter<?> javaTypeAdapter;
251     @Nullable
252     private final Object defaultValue;
253 
254     /**
255      * Construct a new field value binding.
256      *
257      * @param javaField
258      *          the Java field the field value is bound to
259      * @param annotationClass
260      *          the field value binding annotation Java class
261      * @param bindingContext
262      *          the Metaschema binding context managing this class
263      */
264     protected FieldValue(
265         @NonNull Field javaField,
266         @NonNull Class<BoundFieldValue> annotationClass,
267         @NonNull IBindingContext bindingContext) {
268       this.javaField = javaField;
269       this.annotation = ModelUtil.getAnnotation(javaField, annotationClass);
270       this.javaTypeAdapter = ModelUtil.getDataTypeAdapter(
271           this.annotation.typeAdapter(),
272           bindingContext);
273       this.defaultValue = ModelUtil.resolveDefaultValue(this.annotation.defaultValue(), this.javaTypeAdapter);
274     }
275 
276     /**
277      * Get the bound Java field.
278      *
279      * @return the bound Java field
280      */
281     @Override
282     @NonNull
283     public Field getField() {
284       return javaField;
285     }
286 
287     /**
288      * Get the binding Java annotation.
289      *
290      * @return the binding Java annotation
291      */
292     @NonNull
293     public BoundFieldValue getAnnotation() {
294       return annotation;
295     }
296 
297     @Override
298     public IBoundDefinitionModelFieldComplex getParentFieldDefinition() {
299       return DefinitionField.this;
300     }
301 
302     @Override
303     public String getJsonValueKeyName() {
304       String name = ModelUtil.resolveNoneOrValue(getAnnotation().valueKeyName());
305       return name == null ? getJavaTypeAdapter().getDefaultJsonValueKey() : name;
306     }
307 
308     @Override
309     public String getJsonValueKeyFlagName() {
310       return ModelUtil.resolveNoneOrValue(getAnnotation().valueKeyName());
311     }
312 
313     @Override
314     public Object getDefaultValue() {
315       return defaultValue;
316     }
317 
318     @Override
319     public IDataTypeAdapter<?> getJavaTypeAdapter() {
320       return javaTypeAdapter;
321     }
322 
323     @Override
324     public Object getEffectiveDefaultValue() {
325       return getDefaultValue();
326     }
327 
328     @Override
329     public String getJsonName() {
330       return getEffectiveJsonValueKeyName();
331     }
332   }
333   // ----------------------------------------
334   // - End annotation driven code - CPD-OFF -
335   // ----------------------------------------
336 }