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