1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.codegen.typeinfo;
7   
8   import com.squareup.javapoet.AnnotationSpec;
9   import com.squareup.javapoet.ClassName;
10  import com.squareup.javapoet.FieldSpec;
11  import com.squareup.javapoet.MethodSpec;
12  import com.squareup.javapoet.ParameterizedTypeName;
13  import com.squareup.javapoet.TypeName;
14  import com.squareup.javapoet.TypeSpec;
15  import com.squareup.javapoet.WildcardTypeName;
16  
17  import java.util.Collection;
18  import java.util.LinkedHashMap;
19  import java.util.LinkedHashSet;
20  import java.util.LinkedList;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.Set;
24  
25  import dev.metaschema.core.model.IAssemblyDefinition;
26  import dev.metaschema.core.model.IChoiceGroupInstance;
27  import dev.metaschema.core.model.IGroupable;
28  import dev.metaschema.core.model.IModelDefinition;
29  import dev.metaschema.core.model.INamedModelInstanceGrouped;
30  import dev.metaschema.core.model.JsonGroupAsBehavior;
31  import dev.metaschema.core.util.ObjectUtils;
32  import dev.metaschema.databind.codegen.config.IBindingConfiguration;
33  import dev.metaschema.databind.codegen.config.IChoiceGroupBindingConfiguration;
34  import dev.metaschema.databind.codegen.config.IDefinitionBindingConfiguration;
35  import dev.metaschema.databind.codegen.typeinfo.def.IAssemblyDefinitionTypeInfo;
36  import dev.metaschema.databind.model.annotations.BoundChoiceGroup;
37  import edu.umd.cs.findbugs.annotations.NonNull;
38  import edu.umd.cs.findbugs.annotations.Nullable;
39  
40  public class ChoiceGroupTypeInfoImpl
41      extends AbstractModelInstanceTypeInfo<IChoiceGroupInstance>
42      implements IChoiceGroupTypeInfo {
43  
44    /**
45     * Create a type information object describing a choice group instance.
46     *
47     * @param instance
48     *          the choice group instance to generate type information for
49     * @param parent
50     *          the type information for the parent assembly definition containing
51     *          the choice group
52     */
53    public ChoiceGroupTypeInfoImpl(
54        @NonNull IChoiceGroupInstance instance,
55        @NonNull IAssemblyDefinitionTypeInfo parent) {
56      super(instance, parent);
57    }
58  
59    @Override
60    public TypeName getJavaItemType() {
61      return getParentTypeInfo().getTypeResolver().getClassName(getInstance());
62    }
63  
64    /**
65     * Get the Java field type for this choice group instance.
66     *
67     * <p>
68     * Returns a collection type ({@link List} or {@link Map}) when maxOccurs allows
69     * multiple items, or the item type directly for single-valued instances. When
70     * binding configuration specifies a custom item type with wildcard usage
71     * enabled, generates bounded wildcard types (e.g.,
72     * {@code List<? extends Type>}).
73     *
74     * @return the Java field type for code generation
75     */
76    @NonNull
77    @Override
78    public TypeName getJavaFieldType() {
79      TypeName item = getJavaItemType();
80  
81      @NonNull
82      TypeName retval;
83      IChoiceGroupInstance instance = getInstance();
84      int maxOccurrence = instance.getMaxOccurs();
85      if (maxOccurrence == -1 || maxOccurrence > 1) {
86        // Check if we should use wildcard types
87        TypeName collectionItemType = item;
88        IAssemblyDefinition parent = instance.getContainingDefinition();
89        ITypeResolver resolver = getParentTypeInfo().getTypeResolver();
90        IBindingConfiguration bindingConfig = resolver.getBindingConfiguration();
91        IDefinitionBindingConfiguration defConfig = bindingConfig.getBindingConfigurationForDefinition(parent);
92        if (defConfig != null) {
93          IChoiceGroupBindingConfiguration choiceConfig = defConfig.getChoiceGroupBindings()
94              .get(instance.getGroupAsName());
95          if (choiceConfig != null && choiceConfig.getItemTypeName() != null && choiceConfig.isUseWildcard()) {
96            // Use wildcard type for flexibility
97            collectionItemType = WildcardTypeName.subtypeOf(item);
98          }
99        }
100 
101       if (JsonGroupAsBehavior.KEYED.equals(instance.getJsonGroupAsBehavior())) {
102         retval = ObjectUtils.notNull(
103             ParameterizedTypeName.get(ClassName.get(Map.class), ClassName.get(String.class), collectionItemType));
104       } else {
105         retval = ObjectUtils.notNull(ParameterizedTypeName.get(ClassName.get(List.class), collectionItemType));
106       }
107     } else {
108       retval = item;
109     }
110     return retval;
111   }
112 
113   @Override
114   protected AnnotationSpec.Builder newBindingAnnotation() {
115     return ObjectUtils.notNull(AnnotationSpec.builder(BoundChoiceGroup.class));
116   }
117 
118   @SuppressWarnings({ "PMD.UseConcurrentHashMap", "PMD.NPathComplexity", "PMD.CyclomaticComplexity" })
119   @Override
120   public Set<IModelDefinition> buildBindingAnnotation(
121       TypeSpec.Builder typeBuilder,
122       FieldSpec.Builder fieldBuilder,
123       AnnotationSpec.Builder annotation) {
124     IChoiceGroupInstance choiceGroup = getInstance();
125 
126     String discriminator = choiceGroup.getJsonDiscriminatorProperty();
127     if (!IChoiceGroupInstance.DEFAULT_JSON_DISCRIMINATOR_PROPERTY_NAME.equals(discriminator)) {
128       annotation.addMember("discriminator", "$S", discriminator);
129     }
130 
131     int minOccurs = choiceGroup.getMinOccurs();
132     if (minOccurs != IGroupable.DEFAULT_GROUP_AS_MIN_OCCURS) {
133       annotation.addMember("minOccurs", "$L", minOccurs);
134     }
135 
136     int maxOccurs = choiceGroup.getMaxOccurs();
137     if (maxOccurs != IGroupable.DEFAULT_GROUP_AS_MAX_OCCURS) {
138       annotation.addMember("maxOccurs", "$L", maxOccurs);
139     }
140 
141     if (maxOccurs == -1 || maxOccurs > 1) {
142       // requires a group-as
143       annotation.addMember("groupAs", "$L", generateGroupAsAnnotation().build());
144     }
145 
146     String jsonKeyName = choiceGroup.getJsonKeyFlagInstanceName();
147     if (jsonKeyName != null) {
148       annotation.addMember("jsonKey", "$S", jsonKeyName);
149     }
150 
151     Set<IModelDefinition> retval = new LinkedHashSet<>();
152 
153     IAssemblyDefinitionTypeInfo parentTypeInfo = getParentTypeInfo();
154     ITypeResolver typeResolver = parentTypeInfo.getTypeResolver();
155 
156     Map<ClassName, List<INamedModelInstanceGrouped>> referencedDefinitions = new LinkedHashMap<>();
157     Collection<? extends INamedModelInstanceGrouped> modelInstances = getInstance().getNamedModelInstances();
158     for (INamedModelInstanceGrouped modelInstance : modelInstances) {
159       ClassName className = typeResolver.getClassName(modelInstance.getDefinition());
160       List<INamedModelInstanceGrouped> instances = referencedDefinitions.get(className);
161       if (instances == null) {
162         instances = new LinkedList<>(); // NOPMD needed
163         referencedDefinitions.put(className, instances);
164       }
165       instances.add(modelInstance);
166     }
167 
168     for (INamedModelInstanceGrouped modelInstance : modelInstances) {
169       assert modelInstance != null;
170       IGroupedNamedModelInstanceTypeInfo instanceTypeInfo = typeResolver.getTypeInfo(modelInstance, this);
171 
172       ClassName className = typeResolver.getClassName(modelInstance.getDefinition());
173       retval.addAll(instanceTypeInfo.generateMemberAnnotation(
174           annotation,
175           typeBuilder,
176           referencedDefinitions.get(className).size() > 1));
177     }
178     return retval;
179   }
180 
181   /**
182    * Get the binding configuration for this choice group, if one exists.
183    *
184    * @return the choice group binding configuration, or {@code null} if not
185    *         configured
186    */
187   @Nullable
188   private IChoiceGroupBindingConfiguration getChoiceGroupBindingConfiguration() {
189     IChoiceGroupInstance instance = getInstance();
190     IAssemblyDefinition parent = instance.getContainingDefinition();
191     ITypeResolver resolver = getParentTypeInfo().getTypeResolver();
192     IBindingConfiguration bindingConfig = resolver.getBindingConfiguration();
193     IDefinitionBindingConfiguration defConfig = bindingConfig.getBindingConfigurationForDefinition(parent);
194     if (defConfig != null) {
195       return defConfig.getChoiceGroupBindings().get(instance.getGroupAsName());
196     }
197     return null;
198   }
199 
200   @Override
201   public void buildGetterJavadoc(@NonNull MethodSpec.Builder builder) {
202     IChoiceGroupInstance instance = getInstance();
203     String groupAsName = instance.getGroupAsName();
204 
205     builder.addJavadoc("Get the {@code $L} choice group items.\n", groupAsName);
206 
207     // Add item type information if configured
208     IChoiceGroupBindingConfiguration choiceConfig = getChoiceGroupBindingConfiguration();
209     if (choiceConfig != null) {
210       String itemTypeName = choiceConfig.getItemTypeName();
211       if (itemTypeName != null) {
212         builder.addJavadoc("\n");
213         builder.addJavadoc("<p>\n");
214         String simpleTypeName = itemTypeName.substring(itemTypeName.lastIndexOf('.') + 1);
215         if (choiceConfig.isUseWildcard()) {
216           builder.addJavadoc("Items in this collection implement {@link $L}.\n", simpleTypeName);
217         } else {
218           builder.addJavadoc("Items in this collection are of type {@link $L}.\n", simpleTypeName);
219         }
220       }
221     }
222 
223     builder.addJavadoc("\n");
224     builder.addJavadoc("@return the $L items\n", groupAsName);
225   }
226 
227   /**
228    * {@inheritDoc}
229    * <p>
230    * Generates Javadoc for choice group setter methods, including documentation of
231    * the configured item type (if binding configuration specifies one) and whether
232    * wildcard types are required.
233    */
234   @Override
235   public void buildSetterJavadoc(@NonNull MethodSpec.Builder builder, @NonNull String paramName) {
236     IChoiceGroupInstance instance = getInstance();
237     String groupAsName = instance.getGroupAsName();
238 
239     builder.addJavadoc("Set the {@code $L} choice group items.\n", groupAsName);
240 
241     // Add item type information if configured
242     IChoiceGroupBindingConfiguration choiceConfig = getChoiceGroupBindingConfiguration();
243     if (choiceConfig != null) {
244       String itemTypeName = choiceConfig.getItemTypeName();
245       if (itemTypeName != null) {
246         builder.addJavadoc("\n");
247         builder.addJavadoc("<p>\n");
248         String simpleTypeName = itemTypeName.substring(itemTypeName.lastIndexOf('.') + 1);
249         if (choiceConfig.isUseWildcard()) {
250           builder.addJavadoc("Items in this collection must implement {@link $L}.\n", simpleTypeName);
251         } else {
252           builder.addJavadoc("Items in this collection must be of type {@link $L}.\n", simpleTypeName);
253         }
254       }
255     }
256 
257     builder.addJavadoc("\n");
258     builder.addJavadoc("@param $L\n", paramName);
259     builder.addJavadoc("          the $L items to set\n", groupAsName);
260   }
261 
262 }