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