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.markup.MarkupLine;
17  import dev.metaschema.core.datatype.markup.MarkupMultiline;
18  import dev.metaschema.core.model.AbstractFieldInstance;
19  import dev.metaschema.core.model.IAttributable;
20  import dev.metaschema.core.model.IBoundObject;
21  import dev.metaschema.core.util.CollectionUtil;
22  import dev.metaschema.core.util.ObjectUtils;
23  import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
24  import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
25  import dev.metaschema.databind.model.IBoundFieldValue;
26  import dev.metaschema.databind.model.IBoundInstanceFlag;
27  import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex;
28  import dev.metaschema.databind.model.IBoundModule;
29  import dev.metaschema.databind.model.IBoundProperty;
30  import dev.metaschema.databind.model.IGroupAs;
31  import dev.metaschema.databind.model.annotations.BoundField;
32  import dev.metaschema.databind.model.annotations.GroupAs;
33  import dev.metaschema.databind.model.annotations.ModelUtil;
34  import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
35  import edu.umd.cs.findbugs.annotations.NonNull;
36  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
37  import nl.talsmasoftware.lazy4j.Lazy;
38  
39  /**
40   * Implements a Metaschema module field instance bound to a Java field,
41   * supported by a bound definition class.
42   */
43  /**
44   * Implementation of a complex field instance bound to a Java field.
45   * <p>
46   * This class handles the binding of a field referencing a complex field
47   * definition (one that can contain flags) to the containing assembly's model.
48   */
49  public final class InstanceModelFieldComplex
50      extends AbstractFieldInstance<
51          IBoundDefinitionModelAssembly,
52          IBoundDefinitionModelFieldComplex,
53          IBoundInstanceModelFieldComplex,
54          IBoundDefinitionModelAssembly>
55      implements IBoundInstanceModelFieldComplex, IFeatureInstanceModelGroupAs {
56    @NonNull
57    private final Field javaField;
58    @NonNull
59    private final BoundField annotation;
60    @NonNull
61    private final Lazy<IModelInstanceCollectionInfo<IBoundObject>> collectionInfo;
62    @NonNull
63    private final IGroupAs groupAs;
64    @NonNull
65    private final DefinitionField definition;
66    @NonNull
67    private final Lazy<Object> defaultValue;
68    @NonNull
69    private final Lazy<Map<String, IBoundProperty<?>>> jsonProperties;
70    @NonNull
71    private final Lazy<Map<IAttributable.Key, Set<String>>> properties;
72  
73    /**
74     * Construct a new field instance.
75     *
76     * @param javaField
77     *          the Java field bound to this instance
78     * @param definition
79     *          the associated field definition
80     * @param parent
81     *          the definition containing this instance
82     * @return the field instance
83     */
84    @NonNull
85    public static InstanceModelFieldComplex newInstance(
86        @NonNull Field javaField,
87        @NonNull DefinitionField definition,
88        @NonNull IBoundDefinitionModelAssembly parent) {
89      BoundField annotation = ModelUtil.getAnnotation(javaField, BoundField.class);
90      if (!annotation.inXmlWrapped()) {
91        if (definition.hasChildren()) {
92          throw new IllegalStateException(
93              String.format("Field '%s' on class '%s' is requested to be unwrapped, but it has flags preventing this.",
94                  javaField.getName(),
95                  parent.getBoundClass().getName()));
96        }
97        if (!definition.getJavaTypeAdapter().isUnrappedValueAllowedInXml()) {
98          throw new IllegalStateException(
99              String.format(
100                 "Field '%s' on class '%s' is requested to be unwrapped, but its data type '%s' does not allow this.",
101                 javaField.getName(),
102                 parent.getBoundClass().getName(),
103                 definition.getJavaTypeAdapter().getPreferredName()));
104       }
105     }
106 
107     IGroupAs groupAs = ModelUtil.resolveDefaultGroupAs(
108         annotation.groupAs(),
109         parent.getContainingModule());
110     if (annotation.maxOccurs() == -1 || annotation.maxOccurs() > 1) {
111       if (IGroupAs.SINGLETON_GROUP_AS.equals(groupAs)) {
112         throw new IllegalStateException(String.format("Field '%s' on class '%s' is missing the '%s' annotation.",
113             javaField.getName(),
114             javaField.getDeclaringClass().getName(),
115             GroupAs.class.getName()));
116       }
117     } else if (!IGroupAs.SINGLETON_GROUP_AS.equals(groupAs)) {
118       // max is 1 and a groupAs is set
119       throw new IllegalStateException(
120           String.format(
121               "Field '%s' on class '%s' has the '%s' annotation, but maxOccurs=1. A groupAs must not be specfied.",
122               javaField.getName(),
123               javaField.getDeclaringClass().getName(),
124               GroupAs.class.getName()));
125     }
126     return new InstanceModelFieldComplex(javaField, annotation, groupAs, definition, parent);
127   }
128 
129   /**
130    * Construct a new field instance bound to a Java field, supported by a bound
131    * definition class.
132    *
133    * @param javaField
134    *          the Java field bound to this instance
135    * @param definition
136    *          the assembly definition this instance is bound to
137    * @param parent
138    *          the definition containing this instance
139    */
140   @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields")
141   private InstanceModelFieldComplex(
142       @NonNull Field javaField,
143       @NonNull BoundField annotation,
144       @NonNull IGroupAs groupAs,
145       @NonNull DefinitionField definition,
146       @NonNull IBoundDefinitionModelAssembly parent) {
147     super(parent);
148     FieldSupport.bindField(javaField);
149     this.javaField = javaField;
150     this.annotation = annotation;
151     this.collectionInfo = ObjectUtils.notNull(Lazy.of(() -> IModelInstanceCollectionInfo.of(this)));
152     this.groupAs = groupAs;
153     this.definition = definition;
154     this.defaultValue = ObjectUtils.notNull(Lazy.of(() -> {
155       Object retval = null;
156       if (getMaxOccurs() == 1) {
157         IBoundFieldValue fieldValue = definition.getFieldValue();
158 
159         Object fieldValueDefault = fieldValue.getDefaultValue();
160         if (fieldValueDefault != null) {
161           retval = newInstance(null);
162           fieldValue.setValue(retval, fieldValueDefault);
163 
164           for (IBoundInstanceFlag flag : definition.getFlagInstances()) {
165             assert flag != null;
166 
167             Object flagDefault = flag.getResolvedDefaultValue();
168             if (flagDefault != null) {
169               flag.setValue(retval, flagDefault);
170             }
171           }
172         }
173       }
174       return retval;
175     }));
176     this.jsonProperties = ObjectUtils.notNull(Lazy.of(() -> {
177       Predicate<IBoundInstanceFlag> flagFilter = null;
178       IBoundInstanceFlag jsonKey = getEffectiveJsonKey();
179       if (jsonKey != null) {
180         flagFilter = flag -> !jsonKey.equals(flag);
181       }
182       return definition.getJsonProperties(flagFilter);
183     }));
184     this.properties = ObjectUtils.notNull(
185         Lazy.of(() -> CollectionUtil.unmodifiableMap(ObjectUtils.notNull(
186             Arrays.stream(annotation.properties())
187                 .map(ModelUtil::toPropertyEntry)
188                 .collect(
189                     Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new))))));
190   }
191 
192   // ------------------------------------------
193   // - Start annotation driven code - CPD-OFF -
194   // ------------------------------------------
195 
196   @Override
197   public Field getField() {
198     return javaField;
199   }
200 
201   /**
202    * Get the binding Java annotation.
203    *
204    * @return the binding Java annotation
205    */
206   @NonNull
207   public BoundField getAnnotation() {
208     return annotation;
209   }
210 
211   @SuppressWarnings("null")
212   @Override
213   public IModelInstanceCollectionInfo<IBoundObject> getCollectionInfo() {
214     return collectionInfo.get();
215   }
216 
217   @Override
218   public DefinitionField getDefinition() {
219     return definition;
220   }
221 
222   @Override
223   public IBoundModule getContainingModule() {
224     return getContainingDefinition().getContainingModule();
225   }
226 
227   @Override
228   public Object getDefaultValue() {
229     return defaultValue.get();
230   }
231 
232   @Override
233   public Map<String, IBoundProperty<?>> getJsonProperties() {
234     return ObjectUtils.notNull(jsonProperties.get());
235   }
236 
237   @Override
238   public IGroupAs getGroupAs() {
239     return groupAs;
240   }
241 
242   @Override
243   public String getFormalName() {
244     return ModelUtil.resolveNoneOrValue(getAnnotation().formalName());
245   }
246 
247   @Override
248   public MarkupLine getDescription() {
249     return ModelUtil.resolveToMarkupLine(getAnnotation().description());
250   }
251 
252   @Override
253   public String getUseName() {
254     return ModelUtil.resolveNoneOrValue(getAnnotation().useName());
255   }
256 
257   @Override
258   public Integer getUseIndex() {
259     int value = getAnnotation().useIndex();
260     return value == Integer.MIN_VALUE ? null : value;
261   }
262 
263   @Override
264   public boolean isInXmlWrapped() {
265     return getAnnotation().inXmlWrapped();
266   }
267 
268   @Override
269   public int getMinOccurs() {
270     return getAnnotation().minOccurs();
271   }
272 
273   @Override
274   public int getMaxOccurs() {
275     return getAnnotation().maxOccurs();
276   }
277 
278   @Override
279   public Map<Key, Set<String>> getProperties() {
280     return ObjectUtils.notNull(properties.get());
281   }
282 
283   @Override
284   public MarkupMultiline getRemarks() {
285     return ModelUtil.resolveToMarkupMultiline(getAnnotation().remarks());
286   }
287 }