From c00442e69a5bc0f3717a7fb7484a3923ba662f58 Mon Sep 17 00:00:00 2001 From: linwumingshi Date: Sat, 14 Dec 2024 20:33:46 +0800 Subject: [PATCH 1/3] fix: :bug: improve enum class resolution and refactor related methods - Enhance enum class resolution logic to handle various scenarios - Refactor methods for better code organization and readability - Improve comments and method names for clarity - Update related helper classes to use new methods Closes #994 --- .../java/com/ly/doc/utils/JavaClassUtil.java | 161 ++++++++++++++++-- 1 file changed, 147 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/ly/doc/utils/JavaClassUtil.java b/src/main/java/com/ly/doc/utils/JavaClassUtil.java index 555f852c..e9f45a54 100644 --- a/src/main/java/com/ly/doc/utils/JavaClassUtil.java +++ b/src/main/java/com/ly/doc/utils/JavaClassUtil.java @@ -57,7 +57,6 @@ import com.thoughtworks.qdox.model.expression.TypeRef; import com.thoughtworks.qdox.model.impl.DefaultJavaField; import com.thoughtworks.qdox.model.impl.DefaultJavaParameterizedType; -import net.datafaker.BojackHorseman; import org.apache.commons.lang3.StringUtils; import java.lang.reflect.Field; @@ -91,6 +90,11 @@ public class JavaClassUtil { */ private static final Logger logger = Logger.getLogger(JavaClassUtil.class.getName()); + /** + * dot + */ + private static final String DOT = "."; + /** * private constructor */ @@ -556,6 +560,9 @@ public static List getEnumValues(JavaClass javaClass) { * This method aims to obtain the enum type (JavaClass) associated with the provided * Java field object (JavaField). If the field does not associate with an enum type or * if there is no appropriate @see tag providing enum information, it returns null. + * The splitting logic is implemented to handle the case where the @see tag might + * contain additional description information after the class name (e.g., `@see + * GenderEnum descriptionGender`). * @param javaField The Java field object to inspect * @param builder The builder used to retrieve project documentation configuration * @return The enum class object associated with the field, or null if not found @@ -564,38 +571,164 @@ public static JavaClass getSeeEnum(JavaField javaField, ProjectDocConfigBuilder if (Objects.isNull(javaField)) { return null; } + + // If the field type is already an enum, return it directly JavaClass javaClass = javaField.getType(); if (javaClass.isEnum()) { return javaClass; } + // Process the @see tag if present DocletTag see = javaField.getTagByName(DocTags.SEE); if (Objects.isNull(see)) { return null; } - String value = see.getValue(); - // not FullyQualifiedName - if (!StringUtils.contains(value, ".")) { - List imports = javaField.getDeclaringClass().getSource().getImports(); - String finalValue = value; - value = imports.stream() - .filter(i -> StringUtils.endsWith(i, finalValue)) - .findFirst() - .orElse(StringUtils.EMPTY); + // Extract the enum class name from @see tag + String value = extractEnumClassName(see.getValue()); + if (value == null) { + return null; } + // Resolve the class name + // (handles imports, nested classes, and fully qualified names) + value = resolveClassName(value, javaField.getDeclaringClass()); + + // Check if the value corresponds to a valid class name if (!JavaClassValidateUtil.isClassName(value)) { return null; } + // Retrieve the JavaClass by name and check if it is an enum JavaClass enumClass = builder.getClassByName(value); - if (enumClass.isEnum()) { + if (Objects.nonNull(enumClass) && enumClass.isEnum()) { return enumClass; } return null; } + /** + * Extracts the enum class name from the @see tag value. Handles cases where the @see + * tag contains additional description info after the class name.
+ * e.g. {@code @see TestEnum test} + * @param seeValue The value of the @see tag + * @return The extracted enum class name or null if not found + */ + private static String extractEnumClassName(String seeValue) { + if (seeValue == null || seeValue.trim().isEmpty()) { + return null; + } + // Split the value to extract the class name (first part before any whitespace) + return seeValue.trim().split("\\s+")[0]; + } + + /** + * Resolves the class name by checking imports and nested classes. Handles both fully + * qualified class names and short class names (e.g., class names without package + * information). + *

+ * This method first checks if the given class name is fully qualified (i.e., contains + * a dot). If it's not fully qualified, it will attempt to resolve it using imports + * and nested classes. If the class name is fully qualified, it will check if it can + * be resolved using imports or nested classes. The method handles cases where the + * class name is not directly available or when it's a nested class. + * @param value The class name to resolve. This can either be a fully qualified class + * name (e.g., "com.example.MyClass") or a short class name (e.g., "MyClass"). + * @param declaringClass The declaring class that may contain nested classes or + * imports that could be used to resolve the class name. + * @return The resolved class name, which may be a fully qualified name or the + * original value if it cannot be resolved. If a match is found in imports or nested + * classes, the resolved class name will be returned; otherwise, the original value is + * returned. + */ + private static String resolveClassName(String value, JavaClass declaringClass) { + List imports = declaringClass.getSource().getImports(); + + // If it's not a fully qualified class name + // try resolving from imports or nested classes + if (!StringUtils.contains(value, DOT)) { + value = resolveFromImports(value, imports, declaringClass); + } + // Handle fully qualified names (with a dot) or inner classes + else { + value = resolveFullyQualifiedClass(value, declaringClass, imports); + } + return value; + } + + /** + * Resolves the class name from imports or nested classes for short class names. + *

+ * This method looks for the class name in the list of imports of the declaring class. + * If the class name is not found in the imports, it checks if the class is a nested + * class of the declaring class. If a match is found, the full class name is returned; + * otherwise, the original short class name is returned. + * @param value The short class name (e.g., "MyClass") to resolve. + * @param imports A list of import statements in the declaring class that may contain + * the class name. + * @param declaringClass The declaring class that may contain nested classes and + * imports that could be used to resolve the class name. + * @return The fully qualified class name if found in imports or as a nested class, + * otherwise the original short class name. + */ + private static String resolveFromImports(String value, List imports, JavaClass declaringClass) { + Optional importClass = imports.stream().filter(i -> i.endsWith(value)).findFirst(); + + if (importClass.isPresent()) { + return importClass.get(); + } + + // Check for nested class if not found in imports + for (JavaClass nestedClass : declaringClass.getNestedClasses()) { + if (nestedClass.getFullyQualifiedName().endsWith(DOT + value)) { + return nestedClass.getFullyQualifiedName(); + } + } + + // Return original if no match found + return value; + } + + /** + * Resolves the class name for fully qualified class names or nested classes. + *

+ * This method processes fully qualified class names (i.e., names with package + * information) by checking if the class is present in the imports list of the + * declaring class. If it is not found in the imports, it checks if the class is a + * nested class of the declaring class. The method can also handle cases where the + * class name contains inner class references (e.g., "OuterClass$InnerClass"). + * @param value The fully qualified class name to resolve (e.g., "com.example.MyClass" + * or "OuterClass$InnerClass"). + * @param declaringClass The declaring class that may contain nested classes and + * imports that could be used to resolve the class name. + * @param imports A list of import statements in the declaring class that may contain + * the class name. + * @return The fully qualified class name if found in imports or as a nested class, + * otherwise the original class name is returned. + */ + private static String resolveFullyQualifiedClass(String value, JavaClass declaringClass, List imports) { + String[] parts = value.split("\\.", 2); + String classNamePart = parts[0]; + String restPart = (parts.length > 1) ? parts[1] : ""; + + // Try to resolve the class from imports + Optional importClass = imports.stream().filter(i -> i.endsWith(DOT + classNamePart)).findFirst(); + + if (importClass.isPresent()) { + return importClass.get() + (restPart.isEmpty() ? "" : DOT + restPart); + } + + // If not found in imports, check if it's a nested class + for (JavaClass nestedClass : declaringClass.getNestedClasses()) { + if (nestedClass.getName().equals(classNamePart)) { + return declaringClass.getFullyQualifiedName() + DOT + value; + } + } + + // Return original if no match found + return value; + } + /** * get enum info by java class * @param javaClass the java class info @@ -648,11 +781,11 @@ public static String getAnnotationSimpleName(String annotationName) { * @return String */ public static String getClassSimpleName(String className) { - if (className.contains(".")) { + if (className.contains(DOT)) { if (className.contains("<")) { className = className.substring(0, className.indexOf("<")); } - int index = className.lastIndexOf("."); + int index = className.lastIndexOf(DOT); className = className.substring(index + 1); } if (className.contains("[")) { @@ -818,7 +951,7 @@ public static Map getFinalFieldValue(Class clazz) throws Ille } if (Modifier.isFinal(field.getModifiers()) && Modifier.isStatic(field.getModifiers())) { String name = field.getName(); - constants.put(className + "." + name, String.valueOf(field.get(null))); + constants.put(className + DOT + name, String.valueOf(field.get(null))); } } return constants; From 88a574a13e314f839d318a38c6b4f7ff2de2cad6 Mon Sep 17 00:00:00 2001 From: linwumingshi Date: Sat, 14 Dec 2024 20:39:33 +0800 Subject: [PATCH 2/3] fix: :bug: Fixed issue where enum dictionary could not be generated for enums referenced by `@see` in the same package as the current class. - Add package name prefix to resolve enum classes that are not fully qualified - Attempt to find enum class using the declaring class's package name - Improve error handling and return null for invalid class names Closes #995 --- src/main/java/com/ly/doc/utils/JavaClassUtil.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ly/doc/utils/JavaClassUtil.java b/src/main/java/com/ly/doc/utils/JavaClassUtil.java index e9f45a54..31e3b96f 100644 --- a/src/main/java/com/ly/doc/utils/JavaClassUtil.java +++ b/src/main/java/com/ly/doc/utils/JavaClassUtil.java @@ -596,11 +596,23 @@ public static JavaClass getSeeEnum(JavaField javaField, ProjectDocConfigBuilder // Check if the value corresponds to a valid class name if (!JavaClassValidateUtil.isClassName(value)) { - return null; + // Fixed #995: If the value is not a valid class name, + // attempt to resolve it by adding the current package name prefix. + value = javaField.getDeclaringClass().getPackageName() + DOT + value; + // Check again + if (!JavaClassValidateUtil.isClassName(value)) { + return null; + } } // Retrieve the JavaClass by name and check if it is an enum JavaClass enumClass = builder.getClassByName(value); + if (Objects.isNull(enumClass)) { + // Fixed #995: If the class cannot be found, attempt to resolve the class name + // by adding the package prefix of the declaring class. This approach is used + // when the enum is defined in the same package as the declaring class. + enumClass = builder.getClassByName(javaField.getDeclaringClass().getPackageName() + DOT + value); + } if (Objects.nonNull(enumClass) && enumClass.isEnum()) { return enumClass; } From 7534d6d5b5edaa7e9fd72c9216b9bee5eb9d612d Mon Sep 17 00:00:00 2001 From: linwumingshi Date: Sat, 14 Dec 2024 20:40:32 +0800 Subject: [PATCH 3/3] refactor(helper): rename and restructure parameter processing method - Rename `commonHandleParam` to `processApiParam` for better clarity - Update method comments to accurately reflect functionality - Improve code readability and maintainability --- .../com/ly/doc/helper/ParamsBuildHelper.java | 29 +++++++++++++------ src/main/java/com/ly/doc/utils/ParamUtil.java | 8 ++--- .../com/ly/doc/utils/RequestExampleUtil.java | 1 + 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/ly/doc/helper/ParamsBuildHelper.java b/src/main/java/com/ly/doc/helper/ParamsBuildHelper.java index d0f0fa11..2dbdc181 100644 --- a/src/main/java/com/ly/doc/helper/ParamsBuildHelper.java +++ b/src/main/java/com/ly/doc/helper/ParamsBuildHelper.java @@ -385,7 +385,7 @@ private static List processFields(String className, String pre, int le param.setType(processedType); param.setExtensions(extensionParams); // handle param - commonHandleParam(paramList, param, isRequired, comment.toString(), since, strRequired); + processApiParam(paramList, param, isRequired, comment.toString(), since, strRequired); JavaClass enumClass = ParamUtil.handleSeeEnum(param, field, projectBuilder, isResp || jsonRequest, tagsMap, fieldJsonFormatValue); @@ -454,7 +454,7 @@ private static List processFields(String className, String pre, int le ParamUtil.handleSeeEnum(param, field, projectBuilder, isResp || jsonRequest, tagsMap, fieldJsonFormatValue); // hand Param - commonHandleParam(paramList, param, isRequired, comment.toString(), since, strRequired); + processApiParam(paramList, param, isRequired, comment.toString(), since, strRequired); } else if (JavaClassValidateUtil.isCollection(subTypeName) || JavaClassValidateUtil.isArray(subTypeName)) { @@ -491,10 +491,10 @@ else if (JavaClassValidateUtil.isCollection(subTypeName) else { param.setValue(fieldValue); } - commonHandleParam(paramList, param, isRequired, comment + appendComment, since, strRequired); + processApiParam(paramList, param, isRequired, comment + appendComment, since, strRequired); } else { - commonHandleParam(paramList, param, isRequired, comment + appendComment, since, strRequired); + processApiParam(paramList, param, isRequired, comment + appendComment, since, strRequired); fieldPid = Optional.ofNullable(atomicInteger).isPresent() ? param.getId() : paramList.size() + pid; if (!simpleName.equals(gName)) { @@ -542,7 +542,7 @@ else if (JavaClassValidateUtil.isMap(subTypeName)) { param.setType(ParamTypeConstants.PARAM_TYPE_MAP); param.setValue(fieldValue); } - commonHandleParam(paramList, param, isRequired, comment + appendComment, since, strRequired); + processApiParam(paramList, param, isRequired, comment + appendComment, since, strRequired); fieldPid = Optional.ofNullable(atomicInteger).isPresent() ? param.getId() : paramList.size() + pid; String valType = DocClassUtil.getMapKeyValueType(fieldGicName).length == 0 ? fieldGicName : DocClassUtil.getMapKeyValueType(fieldGicName)[1]; @@ -581,10 +581,10 @@ else if (JavaTypeConstants.JAVA_OBJECT_FULLY.equals(fieldGicName)) { if (StringUtil.isEmpty(param.getDesc())) { param.setDesc(DocGlobalConstants.ANY_OBJECT_MSG); } - commonHandleParam(paramList, param, isRequired, comment + appendComment, since, strRequired); + processApiParam(paramList, param, isRequired, comment + appendComment, since, strRequired); } else if (fieldGicName.length() == 1) { - commonHandleParam(paramList, param, isRequired, comment + appendComment, since, strRequired); + processApiParam(paramList, param, isRequired, comment + appendComment, since, strRequired); fieldPid = Optional.ofNullable(atomicInteger).isPresent() ? param.getId() : paramList.size() + pid; // handle java generic or object if (!simpleName.equals(className)) { @@ -643,7 +643,7 @@ else if (simpleName.equals(subTypeName)) { paramList.add(param1); } else { - commonHandleParam(paramList, param, isRequired, comment + appendComment, since, strRequired); + processApiParam(paramList, param, isRequired, comment + appendComment, since, strRequired); fieldGicName = DocUtil.formatFieldTypeGicName(genericMap, fieldGicName); fieldPid = Optional.ofNullable(atomicInteger).isPresent() ? param.getId() : paramList.size() + pid; paramList.addAll(buildParams(fieldGicName, preBuilder.toString(), nextLevel, isRequired, isResp, @@ -793,7 +793,18 @@ public static List primitiveReturnRespComment(String typeName, AtomicI return paramList; } - private static void commonHandleParam(List paramList, ApiParam param, String isRequired, String comment, + /** + * Processes and sets properties for an API parameter and adds it to the provided + * list. + * @param paramList The list of API parameters to which the processed parameter will + * be added. + * @param param The API parameter to process and set properties for. + * @param isRequired A string indicating if the parameter is required (can be empty). + * @param comment The description or comment for the parameter. + * @param since The version information for the parameter. + * @param strRequired A boolean indicating whether the parameter is required. + */ + private static void processApiParam(List paramList, ApiParam param, String isRequired, String comment, String since, boolean strRequired) { if (StringUtil.isEmpty(isRequired)) { param.setDesc(comment).setVersion(since); diff --git a/src/main/java/com/ly/doc/utils/ParamUtil.java b/src/main/java/com/ly/doc/utils/ParamUtil.java index 7e475e69..92fd9954 100644 --- a/src/main/java/com/ly/doc/utils/ParamUtil.java +++ b/src/main/java/com/ly/doc/utils/ParamUtil.java @@ -97,16 +97,16 @@ public static JavaClass handleSeeEnum(ApiParam param, JavaField javaField, Proje .setEnumInfoAndValues(enumInfoAndValue) .setType(enumInfoAndValue.getType()); } - // If the @JsonFormat annotation's shape attribute value is specified, use it as - // the parameter value + // If the @JsonFormat annotation's shape attribute value is specified, + // use it as the parameter value if (StringUtil.isNotEmpty(jsonFormatValue)) { param.setValue(jsonFormatValue); param.setEnumValues(IntStream.rangeClosed(0, param.getEnumValues().size() - 1) .mapToObj(Integer::toString) .collect(Collectors.toList())); } - // If the tagsMap contains a mock tag and the value is not empty, use the value of - // the mock tag as the parameter value + // If the tagsMap contains a mock tag and the value is not empty + // use the value of the mock tag as the parameter value // Override old value if (tagsMap.containsKey(DocTags.MOCK) && StringUtil.isNotEmpty(tagsMap.get(DocTags.MOCK))) { param.setValue(tagsMap.get(DocTags.MOCK)); diff --git a/src/main/java/com/ly/doc/utils/RequestExampleUtil.java b/src/main/java/com/ly/doc/utils/RequestExampleUtil.java index 01e723ea..716b1683 100644 --- a/src/main/java/com/ly/doc/utils/RequestExampleUtil.java +++ b/src/main/java/com/ly/doc/utils/RequestExampleUtil.java @@ -64,6 +64,7 @@ private RequestExampleUtil() { * variables. * @param queryParamsMap A mapping of query parameters for describing URL query string * parameters. + * @return The updated API request example with the example data set. */ public static ApiRequestExample setExampleBody(ApiMethodDoc apiMethodDoc, ApiRequestExample requestExample, Map pathParamsMap, Map queryParamsMap) {