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