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