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