1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.model.impl;
7   
8   import java.lang.reflect.Field;
9   import java.util.Arrays;
10  import java.util.LinkedHashMap;
11  import java.util.Map;
12  import java.util.Set;
13  import java.util.function.Predicate;
14  import java.util.stream.Collectors;
15  
16  import dev.metaschema.core.datatype.IDataTypeAdapter;
17  import dev.metaschema.core.datatype.markup.MarkupLine;
18  import dev.metaschema.core.datatype.markup.MarkupMultiline;
19  import dev.metaschema.core.model.IAttributable;
20  import dev.metaschema.core.model.IBoundObject;
21  import dev.metaschema.core.model.ISource;
22  import dev.metaschema.core.model.constraint.AssemblyConstraintSet;
23  import dev.metaschema.core.model.constraint.IModelConstrained;
24  import dev.metaschema.core.model.constraint.IValueConstrained;
25  import dev.metaschema.core.util.CollectionUtil;
26  import dev.metaschema.core.util.ObjectUtils;
27  import dev.metaschema.databind.IBindingContext;
28  import dev.metaschema.databind.io.BindingException;
29  import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
30  import dev.metaschema.databind.model.IBoundFieldValue;
31  import dev.metaschema.databind.model.IBoundInstanceFlag;
32  import dev.metaschema.databind.model.IBoundModule;
33  import dev.metaschema.databind.model.IBoundProperty;
34  import dev.metaschema.databind.model.annotations.BoundFieldValue;
35  import dev.metaschema.databind.model.annotations.Ignore;
36  import dev.metaschema.databind.model.annotations.MetaschemaField;
37  import dev.metaschema.databind.model.annotations.ModelUtil;
38  import dev.metaschema.databind.model.annotations.ValueConstraints;
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()));
128     }
129     FieldSupport.bindField(field);
130     this.fieldValue = new FieldValue(field, BoundFieldValue.class, bindingContext);
131     this.flagContainer = ObjectUtils.notNull(Lazy.of(() -> new FlagContainerSupport(this, this::handleFlagInstance)));
132 
133     ISource source = module.getSource();
134 
135     this.constraints = ObjectUtils.notNull(Lazy.of(() -> {
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.of(() -> {
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.of(() -> 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       FieldSupport.bindField(javaField);
273       this.javaField = javaField;
274       this.annotation = ModelUtil.getAnnotation(javaField, annotationClass);
275       this.javaTypeAdapter = ModelUtil.getDataTypeAdapter(
276           this.annotation.typeAdapter(),
277           bindingContext);
278       this.defaultValue = ModelUtil.resolveDefaultValue(this.annotation.defaultValue(), this.javaTypeAdapter);
279     }
280 
281     /**
282      * Get the bound Java field.
283      *
284      * @return the bound Java field
285      */
286     @Override
287     @NonNull
288     public Field getField() {
289       return javaField;
290     }
291 
292     /**
293      * Get the binding Java annotation.
294      *
295      * @return the binding Java annotation
296      */
297     @NonNull
298     public BoundFieldValue getAnnotation() {
299       return annotation;
300     }
301 
302     @Override
303     public IBoundDefinitionModelFieldComplex getParentFieldDefinition() {
304       return DefinitionField.this;
305     }
306 
307     @Override
308     public String getJsonValueKeyName() {
309       String name = ModelUtil.resolveNoneOrValue(getAnnotation().valueKeyName());
310       return name == null ? getJavaTypeAdapter().getDefaultJsonValueKey() : name;
311     }
312 
313     @Override
314     public String getJsonValueKeyFlagName() {
315       return ModelUtil.resolveNoneOrValue(getAnnotation().valueKeyName());
316     }
317 
318     @Override
319     public Object getDefaultValue() {
320       return defaultValue;
321     }
322 
323     @Override
324     public IDataTypeAdapter<?> getJavaTypeAdapter() {
325       return javaTypeAdapter;
326     }
327 
328     @Override
329     public Object getEffectiveDefaultValue() {
330       return getDefaultValue();
331     }
332 
333     @Override
334     public String getJsonName() {
335       return getEffectiveJsonValueKeyName();
336     }
337   }
338   // ----------------------------------------
339   // - End annotation driven code - CPD-OFF -
340   // ----------------------------------------
341 }