View Javadoc
1   /*
2    *    Copyright 2006-2026 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.mybatis.generator.api.dom.java;
17  
18  import static org.mybatis.generator.internal.util.StringUtility.stringHasValue;
19  import static org.mybatis.generator.internal.util.messages.Messages.getString;
20  
21  import java.util.ArrayList;
22  import java.util.List;
23  import java.util.Objects;
24  import java.util.StringTokenizer;
25  import java.util.stream.Collectors;
26  
27  import org.jspecify.annotations.Nullable;
28  import org.mybatis.generator.exception.TypeParsingException;
29  
30  public class FullyQualifiedJavaType implements Comparable<FullyQualifiedJavaType> {
31  
32      private static final String JAVA_LANG = "java.lang"; //$NON-NLS-1$
33  
34      private static @Nullable FullyQualifiedJavaType intInstance = null;
35  
36      private static @Nullable FullyQualifiedJavaType stringInstance = null;
37  
38      private static @Nullable FullyQualifiedJavaType booleanPrimitiveInstance = null;
39  
40      private static @Nullable FullyQualifiedJavaType objectInstance = null;
41  
42      private static @Nullable FullyQualifiedJavaType dateInstance = null;
43  
44      private static @Nullable FullyQualifiedJavaType criteriaInstance = null;
45  
46      private static @Nullable FullyQualifiedJavaType generatedCriteriaInstance = null;
47  
48      /** The short name without any generic arguments. */
49      private String baseShortName = ""; //$NON-NLS-1$
50  
51      /** The fully qualified name without any generic arguments. */
52      private String baseQualifiedName = ""; //$NON-NLS-1$
53  
54      private boolean explicitlyImported;
55  
56      private String packageName = ""; //$NON-NLS-1$
57  
58      private boolean primitive;
59  
60      private boolean isArray;
61  
62      private @Nullable PrimitiveTypeWrapper primitiveTypeWrapper;
63  
64      private final List<FullyQualifiedJavaType> typeArguments;
65  
66      // the following three values are used for dealing with wildcard types
67      private boolean wildcardType;
68  
69      private boolean boundedWildcard;
70  
71      private boolean extendsBoundedWildcard;
72  
73      /**
74       * Use this constructor to construct a generic type with the specified type parameters.
75       *
76       * @param fullTypeSpecification
77       *            the full type specification
78       */
79      public FullyQualifiedJavaType(String fullTypeSpecification) {
80          super();
81          typeArguments = new ArrayList<>();
82          parse(fullTypeSpecification);
83      }
84  
85      public boolean isExplicitlyImported() {
86          return explicitlyImported;
87      }
88  
89      /**
90       * Returns the fully qualified name - including any generic type parameters.
91       *
92       * @return Returns the fullyQualifiedName.
93       */
94      public String getFullyQualifiedName() {
95          String s = getFullyQualifiedNameWithoutTypeParameters();
96  
97          if (typeArguments.isEmpty()) {
98              return s;
99          } else {
100             return typeArguments.stream()
101                     .map(FullyQualifiedJavaType::getFullyQualifiedName)
102                     .collect(Collectors.joining(", ", s + "<", ">")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
103         }
104     }
105 
106     public String getFullyQualifiedNameWithoutTypeParameters() {
107         return calculateBaseType(baseQualifiedName);
108     }
109 
110     /**
111      * The name (fully qualified) that should be imported.
112      *
113      * @return the fully qualified name that should be imported. Does not include the wildcard bounds.
114      */
115     public String getImportName() {
116         return baseQualifiedName;
117     }
118 
119     /**
120      * Returns a list of Strings that are the fully qualified names of this type, and any generic type argument
121      * associated with this type.
122      *
123      * @return the import list
124      */
125     public List<String> getImportList() {
126         List<String> answer = new ArrayList<>();
127         if (isExplicitlyImported()) {
128             int index = baseShortName.indexOf('.');
129             if (index == -1) {
130                 answer.add(calculateActualImport(baseQualifiedName));
131             } else {
132                 // an inner class is specified, only import the top
133                 // level class
134                 String sb = packageName + '.' + calculateActualImport(baseShortName.substring(0, index));
135                 answer.add(sb);
136             }
137         }
138 
139         typeArguments.forEach(t -> answer.addAll(t.getImportList()));
140 
141         return answer;
142     }
143 
144     private String calculateActualImport(String name) {
145         String answer = name;
146         if (this.isArray()) {
147             int index = name.indexOf('[');
148             if (index != -1) {
149                 answer = name.substring(0, index);
150             }
151         }
152         return answer;
153     }
154 
155     public String getPackageName() {
156         return packageName;
157     }
158 
159     public String getShortName() {
160         String s = getShortNameWithoutTypeArguments();
161 
162         if (typeArguments.isEmpty()) {
163             return s;
164         } else {
165             return typeArguments.stream()
166                     .map(FullyQualifiedJavaType::getShortName)
167                     .collect(Collectors.joining(", ", s + "<", ">")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
168         }
169     }
170 
171     public String getShortNameWithoutTypeArguments() {
172         return calculateBaseType(baseShortName);
173     }
174 
175     private String calculateBaseType(String name) {
176         StringBuilder sb = new StringBuilder();
177         if (wildcardType) {
178             sb.append('?');
179             if (boundedWildcard) {
180                 if (extendsBoundedWildcard) {
181                     sb.append(" extends "); //$NON-NLS-1$
182                 } else {
183                     sb.append(" super "); //$NON-NLS-1$
184                 }
185 
186                 sb.append(name);
187             }
188         } else {
189             sb.append(name);
190         }
191         return sb.toString();
192     }
193 
194     @Override
195     public boolean equals(Object obj) {
196         if (this == obj) {
197             return true;
198         }
199 
200         if (!(obj instanceof FullyQualifiedJavaType other)) {
201             return false;
202         }
203 
204         return getFullyQualifiedName().equals(other.getFullyQualifiedName());
205     }
206 
207     @Override
208     public int hashCode() {
209         return getFullyQualifiedName().hashCode();
210     }
211 
212     @Override
213     public String toString() {
214         return getFullyQualifiedName();
215     }
216 
217     public boolean isPrimitive() {
218         return primitive;
219     }
220 
221     public PrimitiveTypeWrapper getPrimitiveTypeWrapper() {
222         return Objects.requireNonNull(primitiveTypeWrapper);
223     }
224 
225     public static FullyQualifiedJavaType getIntInstance() {
226         if (intInstance == null) {
227             intInstance = new FullyQualifiedJavaType("int"); //$NON-NLS-1$
228         }
229 
230         return intInstance;
231     }
232 
233     public static FullyQualifiedJavaType getNewListInstance() {
234         // always return a new instance because the type may be parameterized
235         return new FullyQualifiedJavaType("java.util.List"); //$NON-NLS-1$
236     }
237 
238     public static FullyQualifiedJavaType getNewHashMapInstance() {
239         // always return a new instance because the type may be parameterized
240         return new FullyQualifiedJavaType("java.util.HashMap"); //$NON-NLS-1$
241     }
242 
243     public static FullyQualifiedJavaType getNewArrayListInstance() {
244         // always return a new instance because the type may be parameterized
245         return new FullyQualifiedJavaType("java.util.ArrayList"); //$NON-NLS-1$
246     }
247 
248     public static FullyQualifiedJavaType getNewIteratorInstance() {
249         // always return a new instance because the type may be parameterized
250         return new FullyQualifiedJavaType("java.util.Iterator"); //$NON-NLS-1$
251     }
252 
253     public static FullyQualifiedJavaType getStringInstance() {
254         stringInstance = Objects.requireNonNullElseGet(stringInstance,
255                 () -> new FullyQualifiedJavaType("java.lang.String")); //$NON-NLS-1$
256         return stringInstance;
257     }
258 
259     public static FullyQualifiedJavaType getBooleanPrimitiveInstance() {
260         booleanPrimitiveInstance = Objects.requireNonNullElseGet(booleanPrimitiveInstance,
261                 () -> new FullyQualifiedJavaType("boolean")); //$NON-NLS-1$
262         return booleanPrimitiveInstance;
263     }
264 
265     public static FullyQualifiedJavaType getObjectInstance() {
266         objectInstance = Objects.requireNonNullElseGet(objectInstance,
267                 () -> new FullyQualifiedJavaType("java.lang.Object")); //$NON-NLS-1$
268         return objectInstance;
269     }
270 
271     public static FullyQualifiedJavaType getDateInstance() {
272         dateInstance = Objects.requireNonNullElseGet(dateInstance,
273                 () -> new FullyQualifiedJavaType("java.util.Date")); //$NON-NLS-1$
274         return dateInstance;
275     }
276 
277     public static FullyQualifiedJavaType getCriteriaInstance() {
278         criteriaInstance = Objects.requireNonNullElseGet(criteriaInstance,
279                 () -> new FullyQualifiedJavaType("Criteria")); //$NON-NLS-1$
280         return criteriaInstance;
281     }
282 
283     public static FullyQualifiedJavaType getGeneratedCriteriaInstance() {
284         generatedCriteriaInstance = Objects.requireNonNullElseGet(generatedCriteriaInstance,
285                 () -> new FullyQualifiedJavaType("GeneratedCriteria")); //$NON-NLS-1$
286         return generatedCriteriaInstance;
287     }
288 
289     @Override
290     public int compareTo(FullyQualifiedJavaType other) {
291         return getFullyQualifiedName().compareTo(other.getFullyQualifiedName());
292     }
293 
294     public void addTypeArgument(FullyQualifiedJavaType type) {
295         typeArguments.add(type);
296     }
297 
298     private void parse(String fullTypeSpecification) {
299         String spec = fullTypeSpecification.trim();
300 
301         if (spec.startsWith("?")) { //$NON-NLS-1$
302             wildcardType = true;
303             spec = spec.substring(1).trim();
304             if (spec.startsWith("extends ")) { //$NON-NLS-1$
305                 boundedWildcard = true;
306                 extendsBoundedWildcard = true;
307                 spec = spec.substring(8); // "extends ".length()
308             } else if (spec.startsWith("super ")) { //$NON-NLS-1$
309                 boundedWildcard = true;
310                 extendsBoundedWildcard = false;
311                 spec = spec.substring(6); // "super ".length()
312             } else {
313                 boundedWildcard = false;
314             }
315             parse(spec);
316         } else {
317             int index = fullTypeSpecification.indexOf('<');
318             if (index == -1) {
319                 simpleParse(fullTypeSpecification);
320             } else {
321                 simpleParse(fullTypeSpecification.substring(0, index));
322                 int endIndex = fullTypeSpecification.lastIndexOf('>');
323                 if (endIndex == -1) {
324                     throw new TypeParsingException(getString("RuntimeError.22", fullTypeSpecification)); //$NON-NLS-1$
325                 }
326                 genericParse(fullTypeSpecification.substring(index, endIndex + 1));
327             }
328 
329             // this is far from a perfect test for detecting arrays, but is close
330             // enough for most cases. It will not detect an improperly specified
331             // array type like byte], but it will detect byte[] and byte[ ]
332             // which are both valid
333             isArray = fullTypeSpecification.endsWith("]"); //$NON-NLS-1$
334         }
335     }
336 
337     private void simpleParse(String typeSpecification) {
338         baseQualifiedName = typeSpecification.trim();
339         if (baseQualifiedName.contains(".")) { //$NON-NLS-1$
340             packageName = getPackage(baseQualifiedName);
341             baseShortName = baseQualifiedName.substring(packageName.length() + 1);
342             int index = baseShortName.lastIndexOf('.');
343             if (index != -1) {
344                 baseShortName = baseShortName.substring(index + 1);
345             }
346 
347             //$NON-NLS-1$
348             explicitlyImported = !JAVA_LANG.equals(packageName);
349         } else {
350             baseShortName = baseQualifiedName;
351             explicitlyImported = false;
352             packageName = ""; //$NON-NLS-1$
353 
354             switch (baseQualifiedName) {
355             case "byte":  //$NON-NLS-1$
356                 primitive = true;
357                 primitiveTypeWrapper = PrimitiveTypeWrapper.getByteInstance();
358                 break;
359             case "short":  //$NON-NLS-1$
360                 primitive = true;
361                 primitiveTypeWrapper = PrimitiveTypeWrapper.getShortInstance();
362                 break;
363             case "int":  //$NON-NLS-1$
364                 primitive = true;
365                 primitiveTypeWrapper = PrimitiveTypeWrapper.getIntegerInstance();
366                 break;
367             case "long":  //$NON-NLS-1$
368                 primitive = true;
369                 primitiveTypeWrapper = PrimitiveTypeWrapper.getLongInstance();
370                 break;
371             case "char":  //$NON-NLS-1$
372                 primitive = true;
373                 primitiveTypeWrapper = PrimitiveTypeWrapper.getCharacterInstance();
374                 break;
375             case "float":  //$NON-NLS-1$
376                 primitive = true;
377                 primitiveTypeWrapper = PrimitiveTypeWrapper.getFloatInstance();
378                 break;
379             case "double":  //$NON-NLS-1$
380                 primitive = true;
381                 primitiveTypeWrapper = PrimitiveTypeWrapper.getDoubleInstance();
382                 break;
383             case "boolean":  //$NON-NLS-1$
384                 primitive = true;
385                 primitiveTypeWrapper = PrimitiveTypeWrapper.getBooleanInstance();
386                 break;
387             default:
388                 primitive = false;
389                 primitiveTypeWrapper = null;
390                 break;
391             }
392         }
393     }
394 
395     private void genericParse(String genericSpecification) {
396         int lastIndex = genericSpecification.lastIndexOf('>');
397         if (lastIndex == -1) {
398             // shouldn't happen - should be caught already, but just in case...
399             throw new TypeParsingException(getString("RuntimeError.22", genericSpecification)); //$NON-NLS-1$
400         }
401         String argumentString = genericSpecification.substring(1, lastIndex);
402         // need to find "," outside a <> bounds
403         StringTokenizer st = new StringTokenizer(argumentString, ",<>", true); //$NON-NLS-1$
404         int openCount = 0;
405         StringBuilder sb = new StringBuilder();
406         while (st.hasMoreTokens()) {
407             String token = st.nextToken();
408             if ("<".equals(token)) { //$NON-NLS-1$
409                 sb.append(token);
410                 openCount++;
411             } else if (">".equals(token)) { //$NON-NLS-1$
412                 sb.append(token);
413                 openCount--;
414             } else if (",".equals(token)) { //$NON-NLS-1$
415                 if (openCount == 0) {
416                     typeArguments
417                             .add(new FullyQualifiedJavaType(sb.toString()));
418                     sb.setLength(0);
419                 } else {
420                     sb.append(token);
421                 }
422             } else {
423                 sb.append(token);
424             }
425         }
426 
427         if (openCount != 0) {
428             throw new TypeParsingException(getString("RuntimeError.22", genericSpecification)); //$NON-NLS-1$
429         }
430 
431         String finalType = sb.toString();
432         if (stringHasValue(finalType)) {
433             typeArguments.add(new FullyQualifiedJavaType(finalType));
434         }
435     }
436 
437     /**
438      * Returns the package name of a fully qualified type.
439      *
440      * <p>This method calculates the package as the part of the fully qualified name up to, but not including, the last
441      * element. Therefore, it does not support fully qualified inner classes. Not totally foolproof, but correct in
442      * most instances.
443      *
444      * @param baseQualifiedName
445      *            the base qualified name
446      *
447      * @return the package
448      */
449     private static String getPackage(String baseQualifiedName) {
450         int index = baseQualifiedName.lastIndexOf('.');
451         return baseQualifiedName.substring(0, index);
452     }
453 
454     public boolean isArray() {
455         return isArray;
456     }
457 
458     public List<FullyQualifiedJavaType> getTypeArguments() {
459         return typeArguments;
460     }
461 }