Skip to content

Commit

Permalink
Merge pull request #996 from linwumingshi/fix/see-enum
Browse files Browse the repository at this point in the history
fix: 🐛 Enhance enum dictionary generation and improve enum class resolution
  • Loading branch information
shalousun authored Dec 16, 2024
2 parents 8259cc4 + 7534d6d commit eaec48b
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 28 deletions.
29 changes: 20 additions & 9 deletions src/main/java/com/ly/doc/helper/ParamsBuildHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ private static List<ApiParam> 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);
Expand Down Expand Up @@ -454,7 +454,7 @@ private static List<ApiParam> 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)) {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -793,7 +793,18 @@ public static List<ApiParam> primitiveReturnRespComment(String typeName, AtomicI
return paramList;
}

private static void commonHandleParam(List<ApiParam> 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<ApiParam> paramList, ApiParam param, String isRequired, String comment,
String since, boolean strRequired) {
if (StringUtil.isEmpty(isRequired)) {
param.setDesc(comment).setVersion(since);
Expand Down
175 changes: 160 additions & 15 deletions src/main/java/com/ly/doc/utils/JavaClassUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -556,6 +560,9 @@ public static List<String> 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
Expand All @@ -564,38 +571,176 @@ 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<String> 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;
// 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 (enumClass.isEnum()) {
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;
}
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.<br>
* 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).
* <p>
* 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<String> 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.
* <p>
* 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<String> imports, JavaClass declaringClass) {
Optional<String> 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.
* <p>
* 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<String> 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<String> 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
Expand Down Expand Up @@ -648,11 +793,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("[")) {
Expand Down Expand Up @@ -818,7 +963,7 @@ public static Map<String, String> 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;
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/ly/doc/utils/ParamUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/ly/doc/utils/RequestExampleUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> pathParamsMap, Map<String, String> queryParamsMap) {
Expand Down

0 comments on commit eaec48b

Please sign in to comment.