Skip to content

Commit 40bac37

Browse files
authored
Fix generation of annotations, including lists, nested annotations etc. (helidon-io#9182)
EnumValue now has correct equals and hashcode methods.
1 parent 0873e92 commit 40bac37

19 files changed

Lines changed: 1079 additions & 44 deletions

File tree

codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ void writeComponent(ModelWriter writer, Set<String> declaredTokens, ImportOrgani
9797
if (parameters.size() == 1) {
9898
AnnotationParameter parameter = parameters.get(0);
9999
if (parameter.name().equals("value")) {
100-
writer.write(parameter.value());
100+
parameter.writeValue(writer, imports);
101101
} else {
102102
parameter.writeComponent(writer, declaredTokens, imports, classType);
103103
}

codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java

Lines changed: 115 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@
1616
package io.helidon.codegen.classmodel;
1717

1818
import java.io.IOException;
19+
import java.util.HashSet;
20+
import java.util.List;
21+
import java.util.Map;
1922
import java.util.Objects;
2023
import java.util.Set;
24+
import java.util.stream.Collectors;
2125

26+
import io.helidon.common.types.Annotation;
2227
import io.helidon.common.types.EnumValue;
2328
import io.helidon.common.types.TypeName;
2429

@@ -27,13 +32,14 @@
2732
*/
2833
public final class AnnotationParameter extends CommonComponent {
2934

30-
private final String value;
31-
private final TypeName importedType;
35+
private final Set<TypeName> importedTypes;
36+
private final Object objectValue;
3237

3338
private AnnotationParameter(Builder builder) {
3439
super(builder);
35-
this.value = resolveValueToString(builder.type(), builder.value);
36-
this.importedType = resolveImport(builder.value);
40+
41+
this.objectValue = builder.value;
42+
this.importedTypes = resolveImports(builder.value);
3743
}
3844

3945
/**
@@ -45,51 +51,135 @@ public static Builder builder() {
4551
return new Builder();
4652
}
4753

54+
@Override
55+
public String toString() {
56+
return objectValue + " (" + type().simpleTypeName() + ")";
57+
}
58+
4859
@Override
4960
void writeComponent(ModelWriter writer, Set<String> declaredTokens, ImportOrganizer imports, ClassType classType)
5061
throws IOException {
51-
writer.write(name() + " = " + value);
62+
writer.write(name() + " = ");
63+
writeValue(writer, imports);
5264
}
5365

5466
@Override
5567
void addImports(ImportOrganizer.Builder imports) {
56-
if (importedType != null) {
57-
imports.addImport(importedType);
58-
}
68+
importedTypes.forEach(imports::addImport);
69+
}
70+
71+
void writeValue(ModelWriter writer, ImportOrganizer imports) throws IOException {
72+
writer.write(resolveValueToString(imports, type(), objectValue));
5973
}
6074

61-
private static TypeName resolveImport(Object value) {
75+
private static Set<TypeName> resolveImports(Object value) {
76+
Set<TypeName> imports = new HashSet<>();
77+
78+
resolveImports(imports, value);
79+
80+
return imports;
81+
}
82+
83+
private static void resolveImports(Set<TypeName> imports, Object value) {
6284
if (value.getClass().isEnum()) {
63-
return TypeName.create(value.getClass());
85+
imports.add(TypeName.create(value.getClass()));
86+
return;
6487
}
65-
if (value instanceof TypeName tn) {
66-
return tn;
88+
switch (value) {
89+
case TypeName tn -> imports.add(tn);
90+
case EnumValue ev -> imports.add(ev.type());
91+
case Annotation an -> {
92+
imports.add(an.typeName());
93+
an.values()
94+
.values()
95+
.forEach(nestedValue -> resolveImports(imports, nestedValue));
96+
}
97+
default -> {
6798
}
68-
if (value instanceof EnumValue ev) {
69-
return ev.type();
7099
}
71-
return null;
72100
}
73101

74-
private static String resolveValueToString(Type type, Object value) {
102+
// takes the annotation value objects and converts it to its string representation (as seen in class source)
103+
private static String resolveValueToString(ImportOrganizer imports, Type type, Object value) {
75104
Class<?> valueClass = value.getClass();
76105
if (valueClass.isEnum()) {
77-
return valueClass.getSimpleName() + "." + ((Enum<?>) value).name();
78-
} else if (type.fqTypeName().equals(String.class.getName())) {
106+
return imports.typeName(Type.fromTypeName(TypeName.create(valueClass)), true)
107+
+ "." + ((Enum<?>) value).name();
108+
}
109+
if (type != null && type.fqTypeName().equals(String.class.getName())) {
79110
String stringValue = value.toString();
80111
if (!stringValue.startsWith("\"") && !stringValue.endsWith("\"")) {
81112
return "\"" + stringValue + "\"";
82113
}
83-
} else if (value instanceof TypeName typeName) {
84-
return typeName.classNameWithEnclosingNames() + ".class";
85-
} else if (value instanceof EnumValue enumValue) {
86-
return enumValue.type().classNameWithEnclosingNames() + "." + enumValue.name();
114+
return stringValue;
115+
}
116+
117+
if (type != null && type.fqTypeName().equals(Object.class.getName())) {
118+
// we expect this to be "as is" - such as when parsing annotations
119+
return value.toString();
120+
}
121+
122+
return switch (value) {
123+
case TypeName typeName -> imports.typeName(Type.fromTypeName(typeName), true) + ".class";
124+
case EnumValue enumValue -> imports.typeName(Type.fromTypeName(enumValue.type()), true)
125+
+ "." + enumValue.name();
126+
case Character character -> "'" + character + "'";
127+
case Long longValue -> longValue + "L";
128+
case Float floatValue -> floatValue + "F";
129+
case Double doubleValue -> doubleValue + "D";
130+
case Byte byteValue -> "(byte) " + byteValue;
131+
case Short shortValue -> "(short) " + shortValue;
132+
case Class<?> clazz -> imports.typeName(Type.fromTypeName(TypeName.create(clazz)), true) + ".class";
133+
case Annotation annotation -> nestedAnnotationValue(imports, annotation);
134+
case List<?> list -> nestedListValue(imports, list);
135+
case String str -> str.startsWith("\"") && str.endsWith("\"") ? str : "\"" + str + "\"";
136+
default -> value.toString();
137+
};
138+
139+
}
140+
141+
private static String nestedListValue(ImportOrganizer imports, List<?> list) {
142+
if (list.isEmpty()) {
143+
return "{}";
144+
}
145+
StringBuilder result = new StringBuilder();
146+
if (list.size() > 1) {
147+
result.append("{");
87148
}
88-
return value.toString();
149+
150+
result.append(list.stream()
151+
.map(it -> resolveValueToString(imports, null, it))
152+
.collect(Collectors.joining(", ")));
153+
154+
if (list.size() > 1) {
155+
result.append("}");
156+
}
157+
return result.toString();
89158
}
90159

91-
String value() {
92-
return value;
160+
private static String nestedAnnotationValue(ImportOrganizer imports, Annotation annotation) {
161+
StringBuilder sb = new StringBuilder("@");
162+
sb.append(imports.typeName(Type.fromTypeName(annotation.typeName()), true));
163+
164+
Map<String, Object> values = annotation.values();
165+
if (values.isEmpty()) {
166+
return sb.toString();
167+
}
168+
169+
sb.append("(");
170+
if (values.size() == 1 && values.containsKey("value")) {
171+
sb.append(resolveValueToString(imports, null, values.get("value")));
172+
} else {
173+
values.forEach((key, value) -> {
174+
sb.append(key)
175+
.append(" = ")
176+
.append(resolveValueToString(imports, null, value))
177+
.append(", ");
178+
});
179+
sb.delete(sb.length() - 2, sb.length());
180+
}
181+
sb.append(")");
182+
return sb.toString();
93183
}
94184

95185
/**

codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.helidon.common.types.AccessModifier;
2424
import io.helidon.common.types.Annotation;
2525
import io.helidon.common.types.ElementKind;
26+
import io.helidon.common.types.EnumValue;
2627
import io.helidon.common.types.Modifier;
2728
import io.helidon.common.types.TypeName;
2829
import io.helidon.common.types.TypeNames;
@@ -164,7 +165,9 @@ private static void addAnnotationValue(ContentBuilder<?> contentBuilder, Object
164165
case Class<?> value -> contentBuilder.addContentCreate(TypeName.create(value));
165166
case TypeName value -> contentBuilder.addContentCreate(value);
166167
case Annotation value -> contentBuilder.addContentCreate(value);
167-
case Enum<?> value -> toEnumValue(contentBuilder, value);
168+
case Enum<?> value -> toEnumValue(contentBuilder,
169+
EnumValue.create(TypeName.create(value.getDeclaringClass()), value.name()));
170+
case EnumValue value -> toEnumValue(contentBuilder, value);
168171
case List<?> values -> toListValues(contentBuilder, values);
169172
default -> throw new IllegalStateException("Unexpected annotation value type " + objectValue.getClass()
170173
.getName() + ": " + objectValue);
@@ -185,9 +188,17 @@ private static void toListValues(ContentBuilder<?> contentBuilder, List<?> value
185188
contentBuilder.addContent(")");
186189
}
187190

188-
private static void toEnumValue(ContentBuilder<?> contentBuilder, Enum<?> enumValue) {
189-
contentBuilder.addContent(enumValue.getDeclaringClass())
190-
.addContent(".")
191-
.addContent(enumValue.name());
191+
private static void toEnumValue(ContentBuilder<?> contentBuilder, EnumValue enumValue) {
192+
// it would be easier to just use Enum.VALUE, but annotations and their dependencies
193+
// may not be on runtime classpath, so we have to work around it
194+
195+
// EnumValue.create(TypeName.create(...), "VALUE")
196+
contentBuilder.addContent(EnumValue.class)
197+
.addContent(".create(")
198+
.addContentCreate(enumValue.type())
199+
.addContent(",")
200+
.addContent("\"")
201+
.addContent(enumValue.name())
202+
.addContent("\")");
192203
}
193204
}

codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportOrganizer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@ String typeName(Type type, boolean includedImport) {
5959
}
6060
Type checkedType = type.declaringClass().orElse(type);
6161
String fullTypeName = checkedType.fqTypeName();
62-
String simpleTypeName = checkedType.simpleTypeName();
6362

6463
if (!includedImport) {
6564
return fullTypeName;
6665
}
66+
67+
String simpleTypeName = checkedType.simpleTypeName();
6768
if (forcedFullImports.contains(fullTypeName)) {
6869
return type.fqTypeName();
6970
} else if (noImport.contains(fullTypeName) || imports.contains(fullTypeName)) {

0 commit comments

Comments
 (0)