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<IBoundObject> {
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     this.javaField = javaField;
144     this.annotation = annotation;
145     this.collectionInfo = ObjectUtils.notNull(Lazy.lazy(() -> IModelInstanceCollectionInfo.of(this)));
146     this.groupAs = groupAs;
147     this.definition = definition;
148     this.defaultValue = ObjectUtils.notNull(Lazy.lazy(() -> {
149       Object retval = null;
150       if (getMaxOccurs() == 1) {
151         IBoundFieldValue fieldValue = definition.getFieldValue();
152 
153         Object fieldValueDefault = fieldValue.getDefaultValue();
154         if (fieldValueDefault != null) {
155           retval = newInstance(null);
156           fieldValue.setValue(retval, fieldValueDefault);
157 
158           for (IBoundInstanceFlag flag : definition.getFlagInstances()) {
159             Object flagDefault = flag.getResolvedDefaultValue();
160             if (flagDefault != null) {
161               flag.setValue(retval, flagDefault);
162             }
163           }
164         }
165       }
166       return retval;
167     }));
168     this.jsonProperties = ObjectUtils.notNull(Lazy.lazy(() -> {
169       Predicate<IBoundInstanceFlag> flagFilter = null;
170       IBoundInstanceFlag jsonKey = getEffectiveJsonKey();
171       if (jsonKey != null) {
172         flagFilter = flag -> !jsonKey.equals(flag);
173       }
174       return getDefinition().getJsonProperties(flagFilter);
175     }));
176     this.properties = ObjectUtils.notNull(
177         Lazy.lazy(() -> CollectionUtil.unmodifiableMap(ObjectUtils.notNull(
178             Arrays.stream(annotation.properties())
179                 .map(ModelUtil::toPropertyEntry)
180                 .collect(
181                     Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new))))));
182   }
183 
184   // ------------------------------------------
185   // - Start annotation driven code - CPD-OFF -
186   // ------------------------------------------
187 
188   @Override
189   public Field getField() {
190     return javaField;
191   }
192 
193   /**
194    * Get the binding Java annotation.
195    *
196    * @return the binding Java annotation
197    */
198   @NonNull
199   public BoundField getAnnotation() {
200     return annotation;
201   }
202 
203   @SuppressWarnings("null")
204   @Override
205   public IModelInstanceCollectionInfo<IBoundObject> getCollectionInfo() {
206     return collectionInfo.get();
207   }
208 
209   @Override
210   public DefinitionField getDefinition() {
211     return definition;
212   }
213 
214   @Override
215   public IBoundModule getContainingModule() {
216     return getContainingDefinition().getContainingModule();
217   }
218 
219   @Override
220   public Object getDefaultValue() {
221     return defaultValue.get();
222   }
223 
224   @Override
225   public Map<String, IBoundProperty<?>> getJsonProperties() {
226     return ObjectUtils.notNull(jsonProperties.get());
227   }
228 
229   @Override
230   public IGroupAs getGroupAs() {
231     return groupAs;
232   }
233 
234   @Override
235   public String getFormalName() {
236     return ModelUtil.resolveNoneOrValue(getAnnotation().formalName());
237   }
238 
239   @Override
240   public MarkupLine getDescription() {
241     return ModelUtil.resolveToMarkupLine(getAnnotation().description());
242   }
243 
244   @Override
245   public String getUseName() {
246     return ModelUtil.resolveNoneOrValue(getAnnotation().useName());
247   }
248 
249   @Override
250   public Integer getUseIndex() {
251     int value = getAnnotation().useIndex();
252     return value == Integer.MIN_VALUE ? null : value;
253   }
254 
255   @Override
256   public boolean isInXmlWrapped() {
257     return getAnnotation().inXmlWrapped();
258   }
259 
260   @Override
261   public int getMinOccurs() {
262     return getAnnotation().minOccurs();
263   }
264 
265   @Override
266   public int getMaxOccurs() {
267     return getAnnotation().maxOccurs();
268   }
269 
270   @Override
271   public Map<Key, Set<String>> getProperties() {
272     return ObjectUtils.notNull(properties.get());
273   }
274 
275   @Override
276   public MarkupMultiline getRemarks() {
277     return ModelUtil.resolveToMarkupMultiline(getAnnotation().remarks());
278   }
279 }