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.Optional;
13  import java.util.Set;
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.AbstractInlineFieldDefinition;
20  import dev.metaschema.core.model.IAttributable;
21  import dev.metaschema.core.model.IModule;
22  import dev.metaschema.core.model.ISource;
23  import dev.metaschema.core.model.constraint.IValueConstrained;
24  import dev.metaschema.core.model.constraint.ValueConstraintSet;
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.model.IBoundDefinitionModelAssembly;
29  import dev.metaschema.databind.model.IBoundDefinitionModelField;
30  import dev.metaschema.databind.model.IBoundInstanceFlag;
31  import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar;
32  import dev.metaschema.databind.model.IBoundModule;
33  import dev.metaschema.databind.model.IGroupAs;
34  import dev.metaschema.databind.model.annotations.BoundField;
35  import dev.metaschema.databind.model.annotations.GroupAs;
36  import dev.metaschema.databind.model.annotations.ModelUtil;
37  import dev.metaschema.databind.model.annotations.ValueConstraints;
38  import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
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 field instance bound to a scalar valued Java
45   * field.
46   */
47  /**
48   * Implementation of a scalar field instance bound to a Java field.
49   * <p>
50   * This class handles the binding of a field with only a scalar value (no flags)
51   * to the containing assembly's model.
52   */
53  public final class InstanceModelFieldScalar
54      extends AbstractInlineFieldDefinition<
55          IBoundDefinitionModelAssembly,
56          IBoundDefinitionModelField<Object>,
57          IBoundInstanceModelFieldScalar,
58          IBoundDefinitionModelAssembly,
59          IBoundInstanceFlag>
60      implements IBoundInstanceModelFieldScalar, IFeatureInstanceModelGroupAs {
61    @NonNull
62    private final Field javaField;
63    @NonNull
64    private final BoundField annotation;
65    @NonNull
66    private final Lazy<IModelInstanceCollectionInfo<Object>> collectionInfo;
67    @NonNull
68    private final IGroupAs groupAs;
69    @NonNull
70    private final IDataTypeAdapter<?> javaTypeAdapter;
71    @Nullable
72    private final Object defaultValue;
73    @NonNull
74    private final Lazy<IValueConstrained> constraints;
75    @NonNull
76    private final Lazy<Map<IAttributable.Key, Set<String>>> properties;
77  
78    /**
79     * Construct a new field instance bound to a Java field.
80     *
81     * @param javaField
82     *          the Java field bound to this instance
83     * @param parent
84     *          the definition containing this instance
85     * @return the instance
86     */
87    @NonNull
88    public static InstanceModelFieldScalar newInstance(
89        @NonNull Field javaField,
90        @NonNull IBoundDefinitionModelAssembly parent) {
91      BoundField annotation = ModelUtil.getAnnotation(javaField, BoundField.class);
92      IGroupAs groupAs = ModelUtil.resolveDefaultGroupAs(
93          annotation.groupAs(),
94          parent.getContainingModule());
95  
96      if (annotation.maxOccurs() == -1 || annotation.maxOccurs() > 1) {
97        if (IGroupAs.SINGLETON_GROUP_AS.equals(groupAs)) {
98          throw new IllegalStateException(String.format("Field '%s' on class '%s' is missing the '%s' annotation.",
99              javaField.getName(),
100             javaField.getDeclaringClass().getName(),
101             GroupAs.class.getName()));
102       }
103     } else if (!IGroupAs.SINGLETON_GROUP_AS.equals(groupAs)) {
104       // max is 1 and a groupAs is set
105       throw new IllegalStateException(
106           String.format(
107               "Field '%s' on class '%s' has the '%s' annotation, but maxOccurs=1. A groupAs must not be specfied.",
108               javaField.getName(),
109               javaField.getDeclaringClass().getName(),
110               GroupAs.class.getName()));
111     }
112 
113     return new InstanceModelFieldScalar(
114         javaField,
115         annotation,
116         groupAs,
117         parent);
118   }
119 
120   private InstanceModelFieldScalar(
121       @NonNull Field javaField,
122       @NonNull BoundField annotation,
123       @NonNull IGroupAs groupAs,
124       @NonNull IBoundDefinitionModelAssembly parent) {
125     super(parent);
126     FieldSupport.bindField(javaField);
127     this.javaField = javaField;
128     this.annotation = annotation;
129     this.collectionInfo = ObjectUtils.notNull(Lazy.of(() -> IModelInstanceCollectionInfo.of(this)));
130     this.groupAs = groupAs;
131     this.javaTypeAdapter = ModelUtil.getDataTypeAdapter(
132         annotation.typeAdapter(),
133         parent.getBindingContext());
134     this.defaultValue = ModelUtil.resolveDefaultValue(annotation.defaultValue(), this.javaTypeAdapter);
135 
136     IModule module = getContainingModule();
137     ISource source = module.getSource();
138 
139     this.constraints = ObjectUtils.notNull(Lazy.of(() -> {
140       IValueConstrained retval = new ValueConstraintSet(source);
141       ValueConstraints valueAnnotation = annotation.valueConstraints();
142       ConstraintSupport.parse(valueAnnotation, module.getSource(), retval);
143       return retval;
144     }));
145     this.properties = ObjectUtils.notNull(
146         Lazy.of(() -> CollectionUtil.unmodifiableMap(ObjectUtils.notNull(
147             Arrays.stream(annotation.properties())
148                 .map(ModelUtil::toPropertyEntry)
149                 .collect(
150                     Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new))))));
151   }
152 
153   // ------------------------------------------
154   // - Start annotation driven code - CPD-OFF -
155   // ------------------------------------------
156 
157   @Override
158   public IBindingContext getBindingContext() {
159     return getContainingDefinition().getBindingContext();
160   }
161 
162   @Override
163   public IBoundModule getContainingModule() {
164     return getContainingDefinition().getContainingModule();
165   }
166 
167   @Override
168   public Field getField() {
169     return javaField;
170   }
171 
172   /**
173    * Get the binding Java annotation.
174    *
175    * @return the binding Java annotation
176    */
177   @NonNull
178   public BoundField getAnnotation() {
179     return annotation;
180   }
181 
182   @SuppressWarnings("null")
183   @Override
184   public IModelInstanceCollectionInfo<Object> getCollectionInfo() {
185     return collectionInfo.get();
186   }
187 
188   @SuppressWarnings("null")
189   @Override
190   @NonNull
191   public IValueConstrained getConstraintSupport() {
192     return constraints.get();
193   }
194 
195   @Override
196   public IDataTypeAdapter<?> getJavaTypeAdapter() {
197     return javaTypeAdapter;
198   }
199 
200   @Override
201   public Object getDefaultValue() {
202     return defaultValue;
203   }
204 
205   @Override
206   public IGroupAs getGroupAs() {
207     return groupAs;
208   }
209 
210   @Override
211   public String getFormalName() {
212     return ModelUtil.resolveNoneOrValue(getAnnotation().formalName());
213   }
214 
215   @Override
216   public MarkupLine getDescription() {
217     return ModelUtil.resolveToMarkupLine(getAnnotation().description());
218   }
219 
220   @Override
221   public Integer getUseIndex() {
222     int value = getAnnotation().useIndex();
223     return value == Integer.MIN_VALUE ? null : value;
224   }
225 
226   @Override
227   public boolean isInXmlWrapped() {
228     return getAnnotation().inXmlWrapped();
229   }
230 
231   @Override
232   public int getMinOccurs() {
233     return getAnnotation().minOccurs();
234   }
235 
236   @Override
237   public int getMaxOccurs() {
238     return getAnnotation().maxOccurs();
239   }
240 
241   @Override
242   public Map<Key, Set<String>> getProperties() {
243     return ObjectUtils.notNull(properties.get());
244   }
245 
246   @Override
247   public MarkupMultiline getRemarks() {
248     return ModelUtil.resolveToMarkupMultiline(getAnnotation().remarks());
249   }
250 
251   @Override
252   public String getName() {
253     // the name is stored as a usename to remain consistent with non-scalar valued
254     // fields
255     return ObjectUtils.notNull(
256         Optional.ofNullable(ModelUtil.resolveNoneOrValue(getAnnotation().useName())).orElse(getField().getName()));
257   }
258 
259   // ----------------------------------------
260   // - End annotation driven code - CPD-OFF -
261   // ----------------------------------------
262 }