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.FieldSpec;
10  import com.squareup.javapoet.MethodSpec;
11  import com.squareup.javapoet.ParameterSpec;
12  import com.squareup.javapoet.TypeName;
13  import com.squareup.javapoet.TypeSpec;
14  
15  import java.util.LinkedHashMap;
16  import java.util.LinkedHashSet;
17  import java.util.LinkedList;
18  import java.util.Set;
19  
20  import javax.lang.model.element.Modifier;
21  
22  import dev.metaschema.core.model.IAssemblyDefinition;
23  import dev.metaschema.core.model.IFlagInstance;
24  import dev.metaschema.core.model.IGroupable;
25  import dev.metaschema.core.model.IModelDefinition;
26  import dev.metaschema.core.model.INamedModelInstanceAbsolute;
27  import dev.metaschema.core.model.JsonGroupAsBehavior;
28  import dev.metaschema.core.util.CollectionUtil;
29  import dev.metaschema.core.util.ObjectUtils;
30  import dev.metaschema.databind.codegen.ClassUtils;
31  import dev.metaschema.databind.codegen.typeinfo.def.IAssemblyDefinitionTypeInfo;
32  import dev.metaschema.databind.codegen.typeinfo.def.IModelDefinitionTypeInfo;
33  import dev.metaschema.databind.model.annotations.BoundChoice;
34  import edu.umd.cs.findbugs.annotations.NonNull;
35  import edu.umd.cs.findbugs.annotations.Nullable;
36  
37  abstract class AbstractNamedModelInstanceTypeInfo<INSTANCE extends INamedModelInstanceAbsolute>
38      extends AbstractModelInstanceTypeInfo<INSTANCE>
39      implements INamedModelInstanceTypeInfo {
40  
41    @Nullable
42    private String choiceId;
43  
44    public AbstractNamedModelInstanceTypeInfo(
45        @NonNull INSTANCE instance,
46        @NonNull IAssemblyDefinitionTypeInfo parentDefinition) {
47      super(instance, parentDefinition);
48    }
49  
50    /**
51     * Get the choice ID if this instance is part of a choice.
52     *
53     * @return the choice ID, or {@code null} if not part of a choice
54     */
55    @Nullable
56    public String getChoiceId() {
57      return choiceId;
58    }
59  
60    /**
61     * Set the choice ID for this instance.
62     * <p>
63     * This should be called when the instance is part of a Metaschema choice to
64     * associate it with its choice group.
65     *
66     * @param choiceId
67     *          the choice ID to set
68     */
69    @Override
70    public void setChoiceId(@Nullable String choiceId) {
71      this.choiceId = choiceId;
72    }
73  
74    @Override
75    public boolean isRequired() {
76      // Properties inside choice blocks are never required because
77      // the requirement is conditional on the choice branch being taken
78      if (getChoiceId() != null) {
79        return false;
80      }
81  
82      INSTANCE instance = getInstance();
83      // A model instance is required if minOccurs >= 1 AND it's a single item (not a
84      // collection)
85      return instance.getMinOccurs() >= 1 && instance.getMaxOccurs() == 1;
86    }
87  
88    @Override
89    public boolean isCollectionType() {
90      INSTANCE instance = getInstance();
91      // A collection has maxOccurs > 1 or unbounded (-1)
92      return instance.getMaxOccurs() == -1 || instance.getMaxOccurs() > 1;
93    }
94  
95    @NonNull
96    @Override
97    public String getBaseName() {
98      INSTANCE modelInstance = getInstance();
99      String retval;
100     if (modelInstance.getMaxOccurs() == -1 || modelInstance.getMaxOccurs() > 1) {
101       retval = super.getBaseName();
102     } else {
103       retval = modelInstance.getEffectiveName();
104     }
105     return retval;
106   }
107 
108   @Override
109   public String getItemBaseName() {
110     return getInstance().getEffectiveName();
111   }
112 
113   @Override
114   public TypeName getJavaItemType() {
115     return getParentTypeInfo().getTypeResolver().getClassName(this);
116   }
117 
118   @Override
119   public Set<IModelDefinition> buildField(
120       TypeSpec.Builder typeBuilder,
121       FieldSpec.Builder fieldBuilder) {
122     Set<IModelDefinition> retval = super.buildField(typeBuilder, fieldBuilder);
123 
124     // Add @BoundChoice annotation if this instance is part of a choice
125     String choiceIdValue = getChoiceId();
126     if (choiceIdValue != null) {
127       AnnotationSpec.Builder choiceAnnotation = AnnotationSpec.builder(BoundChoice.class);
128       choiceAnnotation.addMember("choiceId", "$S", choiceIdValue);
129       fieldBuilder.addAnnotation(choiceAnnotation.build());
130     }
131 
132     IModelDefinition definition = getInstance().getDefinition();
133     if (definition.isInline() && (definition.hasChildren() || definition instanceof IAssemblyDefinition)) {
134       retval = new LinkedHashSet<>(retval);
135 
136       // this is an inline definition that must be built as a child class
137       retval.add(definition);
138     }
139     return retval.isEmpty() ? CollectionUtil.emptySet() : CollectionUtil.unmodifiableSet(retval);
140   }
141 
142   @Override
143   public Set<IModelDefinition> buildBindingAnnotation(
144       TypeSpec.Builder typeBuilder,
145       FieldSpec.Builder fieldBuilder,
146       AnnotationSpec.Builder annotation) {
147 
148     buildBindingAnnotationCommon(annotation);
149 
150     INamedModelInstanceAbsolute instance = getInstance();
151 
152     int minOccurs = instance.getMinOccurs();
153     if (minOccurs != IGroupable.DEFAULT_GROUP_AS_MIN_OCCURS) {
154       annotation.addMember("minOccurs", "$L", minOccurs);
155     }
156 
157     int maxOccurs = instance.getMaxOccurs();
158     if (maxOccurs != IGroupable.DEFAULT_GROUP_AS_MAX_OCCURS) {
159       annotation.addMember("maxOccurs", "$L", maxOccurs);
160     }
161     if (maxOccurs == -1 || maxOccurs > 1) {
162       // requires a group-as
163       annotation.addMember("groupAs", "$L", generateGroupAsAnnotation().build());
164     }
165 
166     return CollectionUtil.emptySet();
167   }
168 
169   @Override
170   protected void buildExtraMethods(TypeSpec.Builder builder, FieldSpec valueField) {
171     super.buildExtraMethods(builder, valueField);
172 
173     INamedModelInstanceAbsolute instance = getInstance();
174     int maxOccurance = instance.getMaxOccurs();
175     if (maxOccurance == -1 || maxOccurance > 1) {
176       TypeName itemType = getJavaItemType();
177       ParameterSpec valueParam = ParameterSpec.builder(itemType, "item").build();
178 
179       String itemPropertyName = ClassUtils.toPropertyName(getItemBaseName());
180 
181       if (JsonGroupAsBehavior.KEYED.equals(instance.getJsonGroupAsBehavior())) {
182         IFlagInstance jsonKey = instance.getDefinition().getJsonKey();
183         if (jsonKey == null) {
184           throw new IllegalStateException(
185               String.format("JSON key not defined for property: %s", instance.toCoordinates()));
186         }
187 
188         // get the json key property on the instance's definition
189         ITypeResolver typeResolver = getParentTypeInfo().getTypeResolver();
190         IModelDefinitionTypeInfo instanceTypeInfo = typeResolver.getTypeInfo(instance.getDefinition());
191         IFlagInstanceTypeInfo jsonKeyTypeInfo = instanceTypeInfo.getFlagInstanceTypeInfo(jsonKey);
192 
193         if (jsonKeyTypeInfo == null) {
194           throw new IllegalStateException(
195               String.format("Unable to identify JSON key for property: %s", instance.toCoordinates()));
196         }
197 
198         {
199           // create add method
200           MethodSpec.Builder method = MethodSpec.methodBuilder("add" + itemPropertyName)
201               .addParameter(valueParam)
202               .returns(itemType)
203               .addModifiers(Modifier.PUBLIC)
204               .addJavadoc("Add a new {@link $T} item to the underlying collection.\n", itemType)
205               .addJavadoc("@param item the item to add\n")
206               .addJavadoc("@return the existing {@link $T} item in the collection or {@code null} if not item exists\n",
207                   itemType)
208               .addStatement("$1T value = $2T.requireNonNull($3N,\"$3N value cannot be null\")",
209                   itemType, ObjectUtils.class, valueParam)
210               .addStatement("$1T key = $2T.requireNonNull($3N.$4N(),\"$3N key cannot be null\")",
211                   String.class, ObjectUtils.class, valueParam, "get" + jsonKeyTypeInfo.getPropertyName())
212               .beginControlFlow("if ($N == null)", valueField)
213               .addStatement("$N = new $T<>()", valueField, LinkedHashMap.class)
214               .endControlFlow()
215               .addStatement("return $N.put(key, value)", valueField);
216 
217           builder.addMethod(method.build());
218         }
219         {
220           // create remove method
221           MethodSpec.Builder method = MethodSpec.methodBuilder("remove" + itemPropertyName)
222               .addParameter(valueParam)
223               .returns(TypeName.BOOLEAN)
224               .addModifiers(Modifier.PUBLIC)
225               .addJavadoc("Remove the {@link $T} item from the underlying collection.\n", itemType)
226               .addJavadoc("@param item the item to remove\n")
227               .addJavadoc("@return {@code true} if the item was removed or {@code false} otherwise\n")
228               .addStatement("$1T value = $2T.requireNonNull($3N,\"$3N value cannot be null\")",
229                   itemType, ObjectUtils.class, valueParam)
230               .addStatement("$1T key = $2T.requireNonNull($3N.$4N(),\"$3N key cannot be null\")",
231                   String.class, ObjectUtils.class, valueParam, "get" + jsonKeyTypeInfo.getPropertyName())
232               .addStatement("return $1N != null && $1N.remove(key, value)", valueField);
233           builder.addMethod(method.build());
234         }
235       } else {
236         {
237           // create add method
238           MethodSpec.Builder method = MethodSpec.methodBuilder("add" + itemPropertyName)
239               .addParameter(valueParam)
240               .returns(TypeName.BOOLEAN)
241               .addModifiers(Modifier.PUBLIC)
242               .addJavadoc("Add a new {@link $T} item to the underlying collection.\n", itemType)
243               .addJavadoc("@param item the item to add\n")
244               .addJavadoc("@return {@code true}\n")
245               .addStatement("$T value = $T.requireNonNull($N,\"$N cannot be null\")",
246                   itemType, ObjectUtils.class, valueParam, valueParam)
247               .beginControlFlow("if ($N == null)", valueField)
248               .addStatement("$N = new $T<>()", valueField, LinkedList.class)
249               .endControlFlow()
250               .addStatement("return $N.add(value)", valueField);
251 
252           builder.addMethod(method.build());
253         }
254 
255         {
256           // create remove method
257           MethodSpec.Builder method = MethodSpec.methodBuilder("remove" + itemPropertyName)
258               .addParameter(valueParam)
259               .returns(TypeName.BOOLEAN)
260               .addModifiers(Modifier.PUBLIC)
261               .addJavadoc("Remove the first matching {@link $T} item from the underlying collection.\n", itemType)
262               .addJavadoc("@param item the item to remove\n")
263               .addJavadoc("@return {@code true} if the item was removed or {@code false} otherwise\n")
264               .addStatement("$T value = $T.requireNonNull($N,\"$N cannot be null\")",
265                   itemType, ObjectUtils.class, valueParam, valueParam)
266               .addStatement("return $1N != null && $1N.remove(value)", valueField);
267           builder.addMethod(method.build());
268         }
269       }
270     }
271   }
272 
273 }