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.model.AbstractChoiceGroupInstance;
9   import gov.nist.secauto.metaschema.core.model.DefaultChoiceGroupModelBuilder;
10  import gov.nist.secauto.metaschema.core.model.IBoundObject;
11  import gov.nist.secauto.metaschema.core.model.IContainerModelSupport;
12  import gov.nist.secauto.metaschema.core.qname.IEnhancedQName;
13  import gov.nist.secauto.metaschema.core.util.CustomCollectors;
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.IBoundInstanceFlag;
17  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
18  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
19  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedField;
20  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
21  import gov.nist.secauto.metaschema.databind.model.IBoundModule;
22  import gov.nist.secauto.metaschema.databind.model.IGroupAs;
23  import gov.nist.secauto.metaschema.databind.model.annotations.BoundChoiceGroup;
24  import gov.nist.secauto.metaschema.databind.model.annotations.BoundGroupedAssembly;
25  import gov.nist.secauto.metaschema.databind.model.annotations.BoundGroupedField;
26  import gov.nist.secauto.metaschema.databind.model.annotations.GroupAs;
27  import gov.nist.secauto.metaschema.databind.model.annotations.ModelUtil;
28  import gov.nist.secauto.metaschema.databind.model.info.IModelInstanceCollectionInfo;
29  
30  import java.lang.reflect.Field;
31  import java.util.Arrays;
32  import java.util.Collections;
33  import java.util.Map;
34  import java.util.stream.Collectors;
35  
36  import edu.umd.cs.findbugs.annotations.NonNull;
37  import edu.umd.cs.findbugs.annotations.Nullable;
38  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
39  import nl.talsmasoftware.lazy4j.Lazy;
40  
41  /**
42   * Implements a Metaschema module choice group instance bound to a Java field.
43   */
44  @SuppressWarnings("PMD.CouplingBetweenObjects")
45  public final class InstanceModelChoiceGroup
46      extends AbstractChoiceGroupInstance<
47          IBoundDefinitionModelAssembly,
48          IBoundInstanceModelGroupedNamed,
49          IBoundInstanceModelGroupedField,
50          IBoundInstanceModelGroupedAssembly>
51      implements IBoundInstanceModelChoiceGroup, IFeatureInstanceModelGroupAs {
52    @NonNull
53    private final Field javaField;
54    @NonNull
55    private final BoundChoiceGroup annotation;
56    @NonNull
57    private final Lazy<IModelInstanceCollectionInfo<IBoundObject>> collectionInfo;
58    @NonNull
59    private final IGroupAs groupAs;
60    @NonNull
61    private final Lazy<IContainerModelSupport<
62        IBoundInstanceModelGroupedNamed,
63        IBoundInstanceModelGroupedNamed,
64        IBoundInstanceModelGroupedField,
65        IBoundInstanceModelGroupedAssembly>> modelContainer;
66    @NonNull
67    private final Lazy<Map<Class<?>, IBoundInstanceModelGroupedNamed>> classToInstanceMap;
68    @NonNull
69    private final Lazy<Map<IEnhancedQName, IBoundInstanceModelGroupedNamed>> qnameToInstanceMap;
70    @NonNull
71    private final Lazy<Map<String, IBoundInstanceModelGroupedNamed>> discriminatorToInstanceMap;
72  
73    /**
74     * Construct a new Metaschema module choice group instance.
75     *
76     * @param javaField
77     *          the Java field bound to this instance
78     * @param parent
79     *          the definition containing this instance
80     * @return the instance
81     */
82    @NonNull
83    public static InstanceModelChoiceGroup newInstance(
84        @NonNull Field javaField,
85        @NonNull IBoundDefinitionModelAssembly parent) {
86      BoundChoiceGroup annotation = ModelUtil.getAnnotation(javaField, BoundChoiceGroup.class);
87      IGroupAs groupAs = ModelUtil.resolveDefaultGroupAs(annotation.groupAs(), parent.getContainingModule());
88      if (annotation.maxOccurs() == -1 || annotation.maxOccurs() > 1) {
89        if (IGroupAs.SINGLETON_GROUP_AS.equals(groupAs)) {
90          throw new IllegalStateException(String.format("Field '%s' on class '%s' is missing the '%s' annotation.",
91              javaField.getName(),
92              parent.getBoundClass().getName(),
93              GroupAs.class.getName()));
94        }
95      } else if (!IGroupAs.SINGLETON_GROUP_AS.equals(groupAs)) {
96        // max is 1 and a groupAs is set
97        throw new IllegalStateException(
98            String.format(
99                "Field '%s' on class '%s' has the '%s' annotation, but maxOccurs=1. A groupAs must not be specfied.",
100               javaField.getName(),
101               parent.getBoundClass().getName(),
102               GroupAs.class.getName()));
103     }
104     return new InstanceModelChoiceGroup(javaField, annotation, groupAs, parent);
105   }
106 
107   @NonNull
108   private static IContainerModelSupport<
109       IBoundInstanceModelGroupedNamed,
110       IBoundInstanceModelGroupedNamed,
111       IBoundInstanceModelGroupedField,
112       IBoundInstanceModelGroupedAssembly> newContainerModel(
113           @NonNull BoundGroupedAssembly[] assemblies,
114           @NonNull BoundGroupedField[] fields,
115           @NonNull IBoundInstanceModelChoiceGroup container) {
116     DefaultChoiceGroupModelBuilder<
117         IBoundInstanceModelGroupedNamed,
118         IBoundInstanceModelGroupedField,
119         IBoundInstanceModelGroupedAssembly> builder = new DefaultChoiceGroupModelBuilder<>();
120 
121     Arrays.stream(assemblies)
122         .map(instance -> {
123           assert instance != null;
124           return IBoundInstanceModelGroupedAssembly.newInstance(instance, container);
125         }).forEachOrdered(builder::append);
126     Arrays.stream(fields)
127         .map(instance -> {
128           assert instance != null;
129           return IBoundInstanceModelGroupedField.newInstance(instance, container);
130         }).forEachOrdered(builder::append);
131 
132     return builder.buildChoiceGroup();
133   }
134 
135   @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields")
136   private InstanceModelChoiceGroup(
137       @NonNull Field javaField,
138       @NonNull BoundChoiceGroup annotation,
139       @NonNull IGroupAs groupAs,
140       @NonNull IBoundDefinitionModelAssembly parent) {
141     super(parent);
142     FieldSupport.bindField(javaField);
143     this.javaField = javaField;
144     this.annotation = annotation;
145     this.groupAs = groupAs;
146     this.collectionInfo = ObjectUtils.notNull(Lazy.lazy(() -> IModelInstanceCollectionInfo.of(this)));
147     this.modelContainer = ObjectUtils.notNull(Lazy.lazy(() -> newContainerModel(
148         this.annotation.assemblies(),
149         this.annotation.fields(),
150         this)));
151     this.classToInstanceMap = ObjectUtils.notNull(Lazy.lazy(() -> Collections.unmodifiableMap(
152         getNamedModelInstances().stream()
153             .map(instance -> instance)
154             .collect(Collectors.toMap(
155                 item -> item.getDefinition().getBoundClass(),
156                 CustomCollectors.identity())))));
157     this.qnameToInstanceMap = ObjectUtils.notNull(Lazy.lazy(() -> Collections.unmodifiableMap(
158         getNamedModelInstances().stream()
159             .collect(Collectors.toMap(
160                 IBoundInstanceModelGroupedNamed::getQName,
161                 CustomCollectors.identity())))));
162     this.discriminatorToInstanceMap = ObjectUtils.notNull(Lazy.lazy(() -> Collections.unmodifiableMap(
163         getNamedModelInstances().stream()
164             .collect(Collectors.toMap(
165                 IBoundInstanceModelGroupedNamed::getEffectiveDisciminatorValue,
166                 CustomCollectors.identity())))));
167   }
168 
169   // ------------------------------------------
170   // - Start annotation driven code - CPD-OFF -
171   // ------------------------------------------
172 
173   @Override
174   public Field getField() {
175     return javaField;
176   }
177 
178   /**
179    * Get the binding Java annotation.
180    *
181    * @return the binding Java annotation
182    */
183   @NonNull
184   public BoundChoiceGroup getAnnotation() {
185     return annotation;
186   }
187 
188   @SuppressWarnings("null")
189   @Override
190   public IModelInstanceCollectionInfo<IBoundObject> getCollectionInfo() {
191     return collectionInfo.get();
192   }
193 
194   /**
195    * Get the mapping of XML qualified names bound to a distinct grouped model
196    * instance.
197    *
198    * @return the mapping
199    */
200   @SuppressWarnings("null")
201   @NonNull
202   protected Map<IEnhancedQName, IBoundInstanceModelGroupedNamed> getQNameToInstanceMap() {
203     return qnameToInstanceMap.get();
204   }
205 
206   /**
207    * Get the mapping of Java classes bound to a distinct grouped model instance.
208    *
209    * @return the mapping
210    */
211   @SuppressWarnings("null")
212   @NonNull
213   protected Map<Class<?>, IBoundInstanceModelGroupedNamed> getClassToInstanceMap() {
214     return classToInstanceMap.get();
215   }
216 
217   /**
218    * Get the mapping of JSON discriminator values bound to a distinct grouped
219    * model instance.
220    *
221    * @return the mapping
222    */
223   @SuppressWarnings("null")
224   @NonNull
225   protected Map<String, IBoundInstanceModelGroupedNamed> getDiscriminatorToInstanceMap() {
226     return discriminatorToInstanceMap.get();
227   }
228 
229   @Override
230   @Nullable
231   public IBoundInstanceModelGroupedNamed getGroupedModelInstance(@NonNull Class<?> clazz) {
232     return getClassToInstanceMap().get(clazz);
233   }
234 
235   @Override
236   @Nullable
237   public IBoundInstanceModelGroupedNamed getGroupedModelInstance(@NonNull IEnhancedQName name) {
238     return getQNameToInstanceMap().get(name);
239   }
240 
241   @Override
242   public IBoundInstanceModelGroupedNamed getGroupedModelInstance(String discriminator) {
243     return getDiscriminatorToInstanceMap().get(discriminator);
244   }
245 
246   @Override
247   public IGroupAs getGroupAs() {
248     return groupAs;
249   }
250 
251   @SuppressWarnings("null")
252   @Override
253   public IContainerModelSupport<
254       IBoundInstanceModelGroupedNamed,
255       IBoundInstanceModelGroupedNamed,
256       IBoundInstanceModelGroupedField,
257       IBoundInstanceModelGroupedAssembly> getModelContainer() {
258     return modelContainer.get();
259   }
260 
261   @Override
262   public IBoundDefinitionModelAssembly getOwningDefinition() {
263     return getParentContainer();
264   }
265 
266   @Override
267   public IBoundModule getContainingModule() {
268     return getOwningDefinition().getContainingModule();
269   }
270 
271   @Override
272   public int getMinOccurs() {
273     return getAnnotation().minOccurs();
274   }
275 
276   @Override
277   public int getMaxOccurs() {
278     return getAnnotation().maxOccurs();
279   }
280 
281   @Override
282   public String getJsonDiscriminatorProperty() {
283     return getAnnotation().discriminator();
284   }
285 
286   @Override
287   public String getJsonKeyFlagInstanceName() {
288     return getAnnotation().jsonKey();
289   }
290 
291   @Override
292   public IBoundInstanceFlag getItemJsonKey(Object item) {
293     String jsonKeyFlagName = getJsonKeyFlagInstanceName();
294     IBoundInstanceFlag retval = null;
295 
296     if (jsonKeyFlagName != null) {
297       Class<?> clazz = item.getClass();
298 
299       IBoundInstanceModelGroupedNamed itemInstance = getClassToInstanceMap().get(clazz);
300       String namespace = itemInstance.getQName().getNamespace();
301       retval = itemInstance.getDefinition().getFlagInstanceByName(IEnhancedQName.of(namespace, jsonKeyFlagName)
302           .getIndexPosition());
303     }
304     return retval;
305   }
306 }