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     this.fieldValue = new FieldValue(field, BoundFieldValue.class, bindingContext);
131     this.flagContainer = ObjectUtils.notNull(Lazy.lazy(() -> new FlagContainerSupport(this, this::handleFlagInstance)));
132 
133     ISource source = module.getSource();
134 
135     this.constraints = ObjectUtils.notNull(Lazy.lazy(() -> {
136       IModelConstrained retval = new AssemblyConstraintSet(source);
137       ValueConstraints valueAnnotation = getAnnotation().valueConstraints();
138       ConstraintSupport.parse(valueAnnotation, source, retval);
139       return retval;
140     }));
141     this.jsonProperties = ObjectUtils.notNull(Lazy.lazy(() -> {
142       IBoundInstanceFlag jsonValueKey = getJsonValueKeyFlagInstance();
143       Predicate<IBoundInstanceFlag> flagFilter = jsonValueKey == null ? null : flag -> !flag.equals(jsonValueKey);
144       return getJsonProperties(flagFilter);
145     }));
146     this.properties = ObjectUtils.notNull(
147         Lazy.lazy(() -> CollectionUtil.unmodifiableMap(ObjectUtils.notNull(
148             Arrays.stream(annotation.properties())
149                 .map(ModelUtil::toPropertyEntry)
150                 .collect(
151                     Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new))))));
152   }
153 
154   /**
155    * A callback used to identify the JSON value key flag.
156    *
157    * @param instance
158    *          a flag instance
159    */
160   protected void handleFlagInstance(@NonNull IBoundInstanceFlag instance) {
161     if (instance.isJsonValueKey()) {
162       this.jsonValueKeyFlagInstance = instance;
163     }
164   }
165 
166   @Override
167   @NonNull
168   public FieldValue getFieldValue() {
169     return fieldValue;
170   }
171 
172   @Override
173   public IBoundInstanceFlag getJsonValueKeyFlagInstance() {
174     // lazy load flags
175     getFlagContainer();
176     return jsonValueKeyFlagInstance;
177   }
178 
179   @Override
180   protected void deepCopyItemInternal(IBoundObject fromObject, IBoundObject toObject) throws BindingException {
181     // copy the flags
182     super.deepCopyItemInternal(fromObject, toObject);
183 
184     getFieldValue().deepCopy(fromObject, toObject);
185   }
186 
187   // ------------------------------------------
188   // - Start annotation driven code - CPD-OFF -
189   // ------------------------------------------
190 
191   @Override
192   @SuppressWarnings("null")
193   @NonNull
194   public FlagContainerSupport getFlagContainer() {
195     return flagContainer.get();
196   }
197 
198   @Override
199   @NonNull
200   public IValueConstrained getConstraintSupport() {
201     return ObjectUtils.notNull(constraints.get());
202   }
203 
204   @Override
205   public Map<String, IBoundProperty<?>> getJsonProperties() {
206     return ObjectUtils.notNull(jsonProperties.get());
207   }
208 
209   @Override
210   @Nullable
211   public String getFormalName() {
212     return ModelUtil.resolveNoneOrValue(getAnnotation().formalName());
213   }
214 
215   @Override
216   @Nullable
217   public MarkupLine getDescription() {
218     return ModelUtil.resolveToMarkupLine(getAnnotation().description());
219   }
220 
221   @Override
222   @NonNull
223   public String getName() {
224     return getAnnotation().name();
225   }
226 
227   @Override
228   public Map<Key, Set<String>> getProperties() {
229     return ObjectUtils.notNull(properties.get());
230   }
231 
232   @Override
233   @Nullable
234   public Integer getIndex() {
235     return ModelUtil.resolveDefaultInteger(getAnnotation().index());
236   }
237 
238   @Override
239   @Nullable
240   public MarkupMultiline getRemarks() {
241     return ModelUtil.resolveToMarkupMultiline(getAnnotation().description());
242   }
243 
244   /**
245    * Implements a field definition value bound to a Java field.
246    */
247   protected class FieldValue
248       implements IBoundFieldValue {
249     @NonNull
250     private final Field javaField;
251     @NonNull
252     private final BoundFieldValue annotation;
253     @NonNull
254     private final IDataTypeAdapter<?> javaTypeAdapter;
255     @Nullable
256     private final Object defaultValue;
257 
258     /**
259      * Construct a new field value binding.
260      *
261      * @param javaField
262      *          the Java field the field value is bound to
263      * @param annotationClass
264      *          the field value binding annotation Java class
265      * @param bindingContext
266      *          the Metaschema binding context managing this class
267      */
268     protected FieldValue(
269         @NonNull Field javaField,
270         @NonNull Class<BoundFieldValue> annotationClass,
271         @NonNull IBindingContext bindingContext) {
272       this.javaField = javaField;
273       this.annotation = ModelUtil.getAnnotation(javaField, annotationClass);
274       this.javaTypeAdapter = ModelUtil.getDataTypeAdapter(
275           this.annotation.typeAdapter(),
276           bindingContext);
277       this.defaultValue = ModelUtil.resolveDefaultValue(this.annotation.defaultValue(), this.javaTypeAdapter);
278     }
279 
280     /**
281      * Get the bound Java field.
282      *
283      * @return the bound Java field
284      */
285     @Override
286     @NonNull
287     public Field getField() {
288       return javaField;
289     }
290 
291     /**
292      * Get the binding Java annotation.
293      *
294      * @return the binding Java annotation
295      */
296     @NonNull
297     public BoundFieldValue getAnnotation() {
298       return annotation;
299     }
300 
301     @Override
302     public IBoundDefinitionModelFieldComplex getParentFieldDefinition() {
303       return DefinitionField.this;
304     }
305 
306     @Override
307     public String getJsonValueKeyName() {
308       String name = ModelUtil.resolveNoneOrValue(getAnnotation().valueKeyName());
309       return name == null ? getJavaTypeAdapter().getDefaultJsonValueKey() : name;
310     }
311 
312     @Override
313     public String getJsonValueKeyFlagName() {
314       return ModelUtil.resolveNoneOrValue(getAnnotation().valueKeyName());
315     }
316 
317     @Override
318     public Object getDefaultValue() {
319       return defaultValue;
320     }
321 
322     @Override
323     public IDataTypeAdapter<?> getJavaTypeAdapter() {
324       return javaTypeAdapter;
325     }
326 
327     @Override
328     public Object getEffectiveDefaultValue() {
329       return getDefaultValue();
330     }
331 
332     @Override
333     public String getJsonName() {
334       return getEffectiveJsonValueKeyName();
335     }
336   }
337   // ----------------------------------------
338   // - End annotation driven code - CPD-OFF -
339   // ----------------------------------------
340 }