diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java index c52c7a56f1c..6fb06591d3c 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Annotation.java @@ -97,7 +97,7 @@ void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrgani if (parameters.size() == 1) { AnnotationParameter parameter = parameters.get(0); if (parameter.name().equals("value")) { - writer.write(parameter.value()); + parameter.writeValue(writer, imports); } else { parameter.writeComponent(writer, declaredTokens, imports, classType); } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java index 23299d70815..cd6bdfb00b5 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/AnnotationParameter.java @@ -16,9 +16,14 @@ package io.helidon.codegen.classmodel; import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; +import io.helidon.common.types.Annotation; import io.helidon.common.types.EnumValue; import io.helidon.common.types.TypeName; @@ -27,13 +32,14 @@ */ public final class AnnotationParameter extends CommonComponent { - private final String value; - private final TypeName importedType; + private final Set importedTypes; + private final Object objectValue; private AnnotationParameter(Builder builder) { super(builder); - this.value = resolveValueToString(builder.type(), builder.value); - this.importedType = resolveImport(builder.value); + + this.objectValue = builder.value; + this.importedTypes = resolveImports(builder.value); } /** @@ -45,51 +51,135 @@ public static Builder builder() { return new Builder(); } + @Override + public String toString() { + return objectValue + " (" + type().simpleTypeName() + ")"; + } + @Override void writeComponent(ModelWriter writer, Set declaredTokens, ImportOrganizer imports, ClassType classType) throws IOException { - writer.write(name() + " = " + value); + writer.write(name() + " = "); + writeValue(writer, imports); } @Override void addImports(ImportOrganizer.Builder imports) { - if (importedType != null) { - imports.addImport(importedType); - } + importedTypes.forEach(imports::addImport); + } + + void writeValue(ModelWriter writer, ImportOrganizer imports) throws IOException { + writer.write(resolveValueToString(imports, type(), objectValue)); } - private static TypeName resolveImport(Object value) { + private static Set resolveImports(Object value) { + Set imports = new HashSet<>(); + + resolveImports(imports, value); + + return imports; + } + + private static void resolveImports(Set imports, Object value) { if (value.getClass().isEnum()) { - return TypeName.create(value.getClass()); + imports.add(TypeName.create(value.getClass())); + return; } - if (value instanceof TypeName tn) { - return tn; + switch (value) { + case TypeName tn -> imports.add(tn); + case EnumValue ev -> imports.add(ev.type()); + case Annotation an -> { + imports.add(an.typeName()); + an.values() + .values() + .forEach(nestedValue -> resolveImports(imports, nestedValue)); + } + default -> { } - if (value instanceof EnumValue ev) { - return ev.type(); } - return null; } - private static String resolveValueToString(Type type, Object value) { + // takes the annotation value objects and converts it to its string representation (as seen in class source) + private static String resolveValueToString(ImportOrganizer imports, Type type, Object value) { Class valueClass = value.getClass(); if (valueClass.isEnum()) { - return valueClass.getSimpleName() + "." + ((Enum) value).name(); - } else if (type.fqTypeName().equals(String.class.getName())) { + return imports.typeName(Type.fromTypeName(TypeName.create(valueClass)), true) + + "." + ((Enum) value).name(); + } + if (type != null && type.fqTypeName().equals(String.class.getName())) { String stringValue = value.toString(); if (!stringValue.startsWith("\"") && !stringValue.endsWith("\"")) { return "\"" + stringValue + "\""; } - } else if (value instanceof TypeName typeName) { - return typeName.classNameWithEnclosingNames() + ".class"; - } else if (value instanceof EnumValue enumValue) { - return enumValue.type().classNameWithEnclosingNames() + "." + enumValue.name(); + return stringValue; + } + + if (type != null && type.fqTypeName().equals(Object.class.getName())) { + // we expect this to be "as is" - such as when parsing annotations + return value.toString(); + } + + return switch (value) { + case TypeName typeName -> imports.typeName(Type.fromTypeName(typeName), true) + ".class"; + case EnumValue enumValue -> imports.typeName(Type.fromTypeName(enumValue.type()), true) + + "." + enumValue.name(); + case Character character -> "'" + character + "'"; + case Long longValue -> longValue + "L"; + case Float floatValue -> floatValue + "F"; + case Double doubleValue -> doubleValue + "D"; + case Byte byteValue -> "(byte) " + byteValue; + case Short shortValue -> "(short) " + shortValue; + case Class clazz -> imports.typeName(Type.fromTypeName(TypeName.create(clazz)), true) + ".class"; + case Annotation annotation -> nestedAnnotationValue(imports, annotation); + case List list -> nestedListValue(imports, list); + case String str -> str.startsWith("\"") && str.endsWith("\"") ? str : "\"" + str + "\""; + default -> value.toString(); + }; + + } + + private static String nestedListValue(ImportOrganizer imports, List list) { + if (list.isEmpty()) { + return "{}"; + } + StringBuilder result = new StringBuilder(); + if (list.size() > 1) { + result.append("{"); } - return value.toString(); + + result.append(list.stream() + .map(it -> resolveValueToString(imports, null, it)) + .collect(Collectors.joining(", "))); + + if (list.size() > 1) { + result.append("}"); + } + return result.toString(); } - String value() { - return value; + private static String nestedAnnotationValue(ImportOrganizer imports, Annotation annotation) { + StringBuilder sb = new StringBuilder("@"); + sb.append(imports.typeName(Type.fromTypeName(annotation.typeName()), true)); + + Map values = annotation.values(); + if (values.isEmpty()) { + return sb.toString(); + } + + sb.append("("); + if (values.size() == 1 && values.containsKey("value")) { + sb.append(resolveValueToString(imports, null, values.get("value"))); + } else { + values.forEach((key, value) -> { + sb.append(key) + .append(" = ") + .append(resolveValueToString(imports, null, value)) + .append(", "); + }); + sb.delete(sb.length() - 2, sb.length()); + } + sb.append(")"); + return sb.toString(); } /** diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java index 6430c3566e9..d5055caae68 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java @@ -23,6 +23,7 @@ import io.helidon.common.types.AccessModifier; import io.helidon.common.types.Annotation; import io.helidon.common.types.ElementKind; +import io.helidon.common.types.EnumValue; import io.helidon.common.types.Modifier; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; @@ -164,7 +165,9 @@ private static void addAnnotationValue(ContentBuilder contentBuilder, Object case Class value -> contentBuilder.addContentCreate(TypeName.create(value)); case TypeName value -> contentBuilder.addContentCreate(value); case Annotation value -> contentBuilder.addContentCreate(value); - case Enum value -> toEnumValue(contentBuilder, value); + case Enum value -> toEnumValue(contentBuilder, + EnumValue.create(TypeName.create(value.getDeclaringClass()), value.name())); + case EnumValue value -> toEnumValue(contentBuilder, value); case List values -> toListValues(contentBuilder, values); default -> throw new IllegalStateException("Unexpected annotation value type " + objectValue.getClass() .getName() + ": " + objectValue); @@ -185,9 +188,17 @@ private static void toListValues(ContentBuilder contentBuilder, List value contentBuilder.addContent(")"); } - private static void toEnumValue(ContentBuilder contentBuilder, Enum enumValue) { - contentBuilder.addContent(enumValue.getDeclaringClass()) - .addContent(".") - .addContent(enumValue.name()); + private static void toEnumValue(ContentBuilder contentBuilder, EnumValue enumValue) { + // it would be easier to just use Enum.VALUE, but annotations and their dependencies + // may not be on runtime classpath, so we have to work around it + + // EnumValue.create(TypeName.create(...), "VALUE") + contentBuilder.addContent(EnumValue.class) + .addContent(".create(") + .addContentCreate(enumValue.type()) + .addContent(",") + .addContent("\"") + .addContent(enumValue.name()) + .addContent("\")"); } } diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportOrganizer.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportOrganizer.java index 812325d396f..9ac5b5c9c5d 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportOrganizer.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ImportOrganizer.java @@ -59,11 +59,12 @@ String typeName(Type type, boolean includedImport) { } Type checkedType = type.declaringClass().orElse(type); String fullTypeName = checkedType.fqTypeName(); - String simpleTypeName = checkedType.simpleTypeName(); if (!includedImport) { return fullTypeName; } + + String simpleTypeName = checkedType.simpleTypeName(); if (forcedFullImports.contains(fullTypeName)) { return type.fqTypeName(); } else if (noImport.contains(fullTypeName) || imports.contains(fullTypeName)) { diff --git a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java new file mode 100644 index 00000000000..85aea878fc1 --- /dev/null +++ b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.classmodel; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Set; + +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.EnumValue; +import io.helidon.common.types.TypeName; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/* +Test that annotations are correctly written. + */ +class AnnotationTest { + private static final TypeName ANNOTATION_TYPE = TypeName.create(Test.class); + + @Test + void testPrintEnumValue() { + TypeName enumType = TypeName.create(TestEnum.class); + + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(String.class) + .name("name") + .addAnnotation(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("enumValue", EnumValue.create(enumType, + "ONE")) + .build()) + .build(); + String text = write(field); + + assertThat(text, is(""" + @Test(enumValue = AnnotationTest.TestEnum.ONE) + private String name;""")); + } + + @Test + void testContentCreateEnumValue() { + TypeName enumType = TypeName.create(TestEnum.class); + + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(Annotation.class) + .name("annotation") + .addContentCreate(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("enumValue", EnumValue.create(enumType, + "ONE")) + .build()) + .build(); + String text = write(field); + + String expected = """ + private Annotation annotation = Annotation.builder() + .typeName(TypeName.create("org.junit.jupiter.api.Test")) + .putValue("enumValue", EnumValue.create(TypeName.create("io.helidon.codegen.classmodel.AnnotationTest.TestEnum"),"ONE")) + .build();"""; + + assertThat(text, is(expected)); + } + + @Test + void testClassValue() { + TypeName enumType = TypeName.create(TestEnum.class); + + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(String.class) + .name("name") + .addAnnotation(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("classValue", enumType) + .build()) + .build(); + String text = write(field); + + assertThat(text, is(""" + @Test(classValue = AnnotationTest.TestEnum.class) + private String name;""")); + } + + @Test + void testContentCreateClassValue() { + TypeName enumType = TypeName.create(TestEnum.class); + + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(Annotation.class) + .name("annotation") + .addContentCreate(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("classValue", enumType) + .build()) + .build(); + String text = write(field); + + String expected = """ + private Annotation annotation = Annotation.builder() + .typeName(TypeName.create("org.junit.jupiter.api.Test")) + .putValue("classValue", TypeName.create("io.helidon.codegen.classmodel.AnnotationTest.TestEnum")) + .build();"""; + + assertThat(text, is(expected)); + } + + String write(ModelComponent component) { + ImportOrganizer io = ImportOrganizer.builder() + .typeName(AnnotationTest.class.getName()) + .addImport(Test.class) + .addImport(String.class) + .addImport(Annotation.class) + .addImport(TypeName.class) + .addImport(EnumValue.class) + .build(); + StringWriter writer = new StringWriter(); + ModelWriter modelWriter = new ModelWriter(writer, ""); + try { + component.writeComponent(modelWriter, + Set.of(), + io, + ClassType.CLASS); + } catch (IOException e) { + throw new RuntimeException(e); + } + return writer.toString(); + } + + private enum TestEnum { + ONE, + TWO + } +} diff --git a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java index e9f763a40a1..fb9ef3c20c2 100644 --- a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java +++ b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/TypesCodegenTest.java @@ -78,7 +78,7 @@ void testIt() { .putValue("float", 49.0F) .putValue("class", @io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest")) .putValue("type", @io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest")) - .putValue("enum", @java.lang.annotation.ElementType@.FIELD) + .putValue("enum", @io.helidon.common.types.EnumValue@.create(@io.helidon.common.types.TypeName@.create("java.lang.annotation.ElementType"),"FIELD")) .putValue("lstring", @java.util.List@.of("value1","value2")) .putValue("lboolean", @java.util.List@.of(true,false)) .putValue("llong", @java.util.List@.of(49L,50L)) @@ -90,7 +90,7 @@ void testIt() { .putValue("lfloat", @java.util.List@.of(49.0F,50.0F)) .putValue("lclass", @java.util.List@.of(@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"),@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"))) .putValue("ltype", @java.util.List@.of(@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"),@io.helidon.common.types.TypeName@.create("io.helidon.codegen.classmodel.TypesCodegenTest"))) - .putValue("lenum", @java.util.List@.of(@java.lang.annotation.ElementType@.FIELD,@java.lang.annotation.ElementType@.MODULE)) + .putValue("lenum", @java.util.List@.of(@io.helidon.common.types.EnumValue@.create(@io.helidon.common.types.TypeName@.create("java.lang.annotation.ElementType"),"FIELD"),@io.helidon.common.types.EnumValue@.create(@io.helidon.common.types.TypeName@.create("java.lang.annotation.ElementType"),"MODULE"))) .build()""")); } } diff --git a/codegen/pom.xml b/codegen/pom.xml index 40dc6a70b9a..4790bf3c7af 100644 --- a/codegen/pom.xml +++ b/codegen/pom.xml @@ -60,4 +60,13 @@ + + + + tests + + tests + + + diff --git a/codegen/tests/pom.xml b/codegen/tests/pom.xml new file mode 100644 index 00000000000..8d6804e59b3 --- /dev/null +++ b/codegen/tests/pom.xml @@ -0,0 +1,50 @@ + + + + + io.helidon.codegen + helidon-codegen-project + 4.1.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + io.helidon.codegen.tests + helidon-codegen-tests-project + + Helidon Codegen Tests Project + pom + + + true + true + true + true + true + true + true + + + + test-codegen + test-codegen-use + + diff --git a/codegen/tests/test-codegen-use/pom.xml b/codegen/tests/test-codegen-use/pom.xml new file mode 100644 index 00000000000..1fafba1fa1f --- /dev/null +++ b/codegen/tests/test-codegen-use/pom.xml @@ -0,0 +1,98 @@ + + + + + io.helidon.codegen.tests + helidon-codegen-tests-project + 4.1.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-codegen-tests-codegen-use + + Helidon Codegen Tests Codegen Use + + Usage of the code generator. + The main reason for this module is to make sure we can compile the generated class. + + + + + io.helidon.codegen + helidon-codegen + + + io.helidon.codegen + helidon-codegen-class-model + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.codegen.tests + helidon-codegen-tests-codegen + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.codegen.tests + helidon-codegen-tests-codegen + ${helidon.version} + + + + + + diff --git a/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/CrazyAnnotation.java b/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/CrazyAnnotation.java new file mode 100644 index 00000000000..5e9dc29b933 --- /dev/null +++ b/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/CrazyAnnotation.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.test.codegen.use; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CrazyAnnotation { + String stringValue(); + boolean booleanValue(); + long longValue(); + double doubleValue(); + int intValue(); + byte byteValue(); + char charValue(); + short shortValue(); + float floatValue(); + Class classValue(); + Class typeValue(); + ElementType enumValue(); + Target annotationValue(); + String[] lstring(); + boolean[] lboolean(); + long[] llong(); + double[] ldouble(); + int[] lint(); + byte[] lbyte(); + char[] lchar(); + short[] lshort(); + float[] lfloat(); + Class[] lclass(); + Class[] ltype(); + ElementType[] lenum(); + Target[] lannotation(); + String[] emptyList(); + String[] singletonList(); +} diff --git a/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/TriggerType.java b/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/TriggerType.java new file mode 100644 index 00000000000..6d9f0ad23ef --- /dev/null +++ b/codegen/tests/test-codegen-use/src/main/java/io/helidon/codegen/test/codegen/use/TriggerType.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.test.codegen.use; + +import io.helidon.common.Weight; + +@Weight(48) +public class TriggerType { +} diff --git a/codegen/tests/test-codegen-use/src/main/java/module-info.java b/codegen/tests/test-codegen-use/src/main/java/module-info.java new file mode 100644 index 00000000000..8dc2fc7f9e9 --- /dev/null +++ b/codegen/tests/test-codegen-use/src/main/java/module-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module io.helidon.codegen.tests.codegen.use { + requires io.helidon.common; + requires io.helidon.common.types; +} \ No newline at end of file diff --git a/codegen/tests/test-codegen-use/src/test/java/io/helidon/codegen/test/codegen/use/CodegenValidationTest.java b/codegen/tests/test-codegen-use/src/test/java/io/helidon/codegen/test/codegen/use/CodegenValidationTest.java new file mode 100644 index 00000000000..d138a8f7ae3 --- /dev/null +++ b/codegen/tests/test-codegen-use/src/test/java/io/helidon/codegen/test/codegen/use/CodegenValidationTest.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.test.codegen.use; + +import java.lang.annotation.ElementType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; + +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsArrayContainingInOrder.arrayContaining; +import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize; +import static org.junit.jupiter.api.Assertions.fail; + +public class CodegenValidationTest { + private static final String CLASS_NAME = "io.helidon.codegen.test.codegen.use.TriggerType__Generated"; + private static Class clazz; + + @BeforeAll + public static void setUpClass() { + try { + clazz = Class.forName(CLASS_NAME); + } catch (ClassNotFoundException e) { + fail("Class " + CLASS_NAME + " should have been code generated by TestCodegenExtension"); + } + } + + @Test + public void testGeneratedClass() { + CrazyAnnotation annotation = clazz.getAnnotation(CrazyAnnotation.class); + assertThat(annotation, notNullValue()); + validateAnnotation(annotation); + } + + @Test + public void testGeneratedConstant() { + Field field; + try { + field = clazz.getDeclaredField("ANNOTATION"); + } catch (NoSuchFieldException e) { + fail("Class " + CLASS_NAME + " should contain a constant field \"ANNOTATION\", generated by TestCodegenExtension"); + return; + } + + Annotation annotation; + try { + annotation = (Annotation) field.get(null); + } catch (IllegalAccessException e) { + fail("Failed to get field ANNOTATION from the generated class " + CLASS_NAME, e); + return; + } + + assertThat(annotation, notNullValue()); + assertThat(annotation.typeName(), is(TypeName.create(CrazyAnnotation.class))); + assertThat(annotation.stringValue("stringValue"), optionalValue(is("value1"))); + assertThat(annotation.booleanValue("booleanValue"), optionalValue(is(true))); + assertThat(annotation.longValue("longValue"), optionalValue(is(49L))); + assertThat(annotation.doubleValue("doubleValue"), optionalValue(is(49D))); + assertThat(annotation.intValue("intValue"), optionalValue(is(49))); + assertThat(annotation.byteValue("byteValue"), optionalValue(is((byte) 49))); + assertThat(annotation.charValue("charValue"), optionalValue(is('x'))); + assertThat(annotation.shortValue("shortValue"), optionalValue(is((short) 49))); + assertThat(annotation.floatValue("floatValue"), optionalValue(is(49F))); + assertThat(annotation.typeValue("classValue"), optionalValue(is(TypeNames.STRING))); + assertThat(annotation.typeValue("typeValue"), optionalValue(is(TypeNames.STRING))); + assertThat(annotation.enumValue("enumValue", ElementType.class), + optionalValue(is(ElementType.FIELD))); + assertThat(annotation.annotationValue("annotationValue"), optionalPresent()); + + // lists + assertThat(annotation.stringValues("lstring"), + optionalValue(is(List.of("value1", "value2")))); + assertThat(annotation.booleanValues("lboolean") + , optionalValue(is(List.of(true, false)))); + assertThat(annotation.longValues("llong"), + optionalValue(is(List.of(49L, 50L)))); + assertThat(annotation.doubleValues("ldouble"), + optionalValue(is(List.of(49D, 50D)))); + assertThat(annotation.intValues("lint"), + optionalValue(is(List.of(49, 50)))); + assertThat(annotation.byteValues("lbyte"), + optionalValue(is(List.of((byte) 49, (byte) 50)))); + assertThat(annotation.charValues("lchar"), + optionalValue(is(List.of('x', 'y')))); + assertThat(annotation.shortValues("lshort"), + optionalValue(is(List.of((short) 49, (short) 50)))); + assertThat(annotation.floatValues("lfloat"), + optionalValue(is(List.of(49F, 50F)))); + assertThat(annotation.typeValues("lclass"), + optionalValue(is(List.of(TypeNames.STRING, TypeNames.BOXED_INT)))); + assertThat(annotation.typeValues("ltype"), + optionalValue(is(List.of(TypeNames.STRING, TypeNames.BOXED_INT)))); + assertThat(annotation.enumValues("lenum", ElementType.class), + optionalValue(is(List.of(ElementType.FIELD, ElementType.MODULE)))); + assertThat(annotation.annotationValues("lannotation"), optionalPresent()); + assertThat(annotation.stringValues("emptyList"), optionalValue(is(List.of()))); + assertThat(annotation.stringValues("singletonList"), optionalValue(is(List.of("value")))); + } + + @Test + public void testGeneratedField() { + Field field; + try { + field = clazz.getDeclaredField("field"); + } catch (NoSuchFieldException e) { + fail("Class " + CLASS_NAME + " should contain a field \"field\", generated by TestCodegenExtension"); + return; + } + + assertThat("Field type should be String", field.getType(), sameInstance(String.class)); + assertThat("Field should be private", Modifier.isPrivate(field.getModifiers())); + assertThat("Field should not be final", !Modifier.isFinal(field.getModifiers())); + assertThat("Field should not be static", !Modifier.isStatic(field.getModifiers())); + + CrazyAnnotation annotation = field.getAnnotation(CrazyAnnotation.class); + assertThat(annotation, notNullValue()); + validateAnnotation(annotation); + } + + @Test + public void testGeneratedConstructor() { + Constructor ctr; + try { + ctr = clazz.getDeclaredConstructor(); + } catch (NoSuchMethodException e) { + fail("Class " + CLASS_NAME + " should contain a no-argument constructor, generated by TestCodegenExtension"); + return; + } + + assertThat("Constructor should be public", Modifier.isPublic(ctr.getModifiers())); + + CrazyAnnotation annotation = ctr.getAnnotation(CrazyAnnotation.class); + assertThat(annotation, notNullValue()); + validateAnnotation(annotation); + } + + @Test + public void testGeneratedMethod() { + Method method; + try { + method = clazz.getDeclaredMethod("method"); + } catch (NoSuchMethodException e) { + fail("Class " + CLASS_NAME + " should contain a method \"method\", generated by TestCodegenExtension"); + return; + } + + assertThat("Method should be public", Modifier.isPublic(method.getModifiers())); + assertThat("Method should not be final", !Modifier.isFinal(method.getModifiers())); + assertThat("Method should not be static", !Modifier.isStatic(method.getModifiers())); + assertThat("Method return type should be void", method.getReturnType(), sameInstance(void.class)); + + CrazyAnnotation annotation = method.getAnnotation(CrazyAnnotation.class); + assertThat(annotation, notNullValue()); + validateAnnotation(annotation); + } + + private void validateAnnotation(CrazyAnnotation annotation) { + // single values + assertThat(annotation.stringValue(), is("value1")); + assertThat(annotation.booleanValue(), is(true)); + assertThat(annotation.longValue(), is(49L)); + assertThat(annotation.doubleValue(), is(49D)); + assertThat(annotation.intValue(), is(49)); + assertThat(annotation.byteValue(), is((byte) 49)); + assertThat(annotation.charValue(), is('x')); + assertThat(annotation.shortValue(), is((short) 49)); + assertThat(annotation.floatValue(), is(49F)); + assertThat(annotation.classValue(), sameInstance(String.class)); + assertThat(annotation.typeValue(), sameInstance(String.class)); + assertThat(annotation.enumValue(), is(ElementType.FIELD)); + assertThat(annotation.annotationValue().value(), arrayContaining(ElementType.CONSTRUCTOR)); + + // arrays + assertThat(annotation.lstring(), arrayContaining("value1", "value2")); + + assertThat("Should be same boolean array, but is: " + Arrays.toString(annotation.lboolean()), + Arrays.equals(annotation.lboolean(), new boolean[] {true, false})); + assertThat("Should be same long array, but is: " + Arrays.toString(annotation.llong()), + Arrays.equals(annotation.llong(), new long[] {49L, 50L})); + assertThat("Should be same double array, but is: " + Arrays.toString(annotation.ldouble()), + Arrays.equals(annotation.ldouble(), new double[] {49D, 50D})); + assertThat("Should be same int array, but is: " + Arrays.toString(annotation.lint()), + Arrays.equals(annotation.lint(), new int[] {49, 50})); + assertThat("Should be same byte array, but is: " + Arrays.toString(annotation.lbyte()), + Arrays.equals(annotation.lbyte(), new byte[] {(byte) 49, (byte) 50})); + assertThat("Should be same char array, but is: " + Arrays.toString(annotation.lchar()), + Arrays.equals(annotation.lchar(), new char[] {'x', 'y'})); + assertThat("Should be same short array, but is: " + Arrays.toString(annotation.lshort()), + Arrays.equals(annotation.lshort(), new short[] {(short) 49, (short) 50})); + assertThat("Should be same float array, but is: " + Arrays.toString(annotation.lfloat()), + Arrays.equals(annotation.lfloat(), new float[] {49F, 50F})); + assertThat(annotation.lclass(), arrayContaining(String.class, Integer.class)); + assertThat(annotation.ltype(), arrayContaining(String.class, Integer.class)); + assertThat(annotation.lenum(), arrayContaining(ElementType.FIELD, ElementType.MODULE)); + assertThat(annotation.lannotation(), arrayWithSize(2)); + assertThat(annotation.emptyList(), arrayWithSize(0)); + assertThat(annotation.singletonList(), arrayContaining("value")); + } +} diff --git a/codegen/tests/test-codegen/pom.xml b/codegen/tests/test-codegen/pom.xml new file mode 100644 index 00000000000..d031df5af68 --- /dev/null +++ b/codegen/tests/test-codegen/pom.xml @@ -0,0 +1,45 @@ + + + + + io.helidon.codegen.tests + helidon-codegen-tests-project + 4.1.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-codegen-tests-codegen + + Helidon Codegen Tests Codegen + Testing code generator + + + + io.helidon.codegen + helidon-codegen + + + io.helidon.codegen + helidon-codegen-class-model + + + diff --git a/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtension.java b/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtension.java new file mode 100644 index 00000000000..8197f3a20b3 --- /dev/null +++ b/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtension.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.test.codegen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.util.Collection; +import java.util.List; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.RoundContext; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.spi.CodegenExtension; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.Annotation; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; + +class TestCodegenExtension implements CodegenExtension { + private static final TypeName CRAZY = TypeName.create("io.helidon.codegen.test.codegen.use.CrazyAnnotation"); + private static final TypeName TARGET = TypeName.create(Target.class); + + private final CodegenContext ctx; + + TestCodegenExtension(CodegenContext ctx) { + this.ctx = ctx; + } + + @Override + public void process(RoundContext roundContext) { + Collection typeInfos = roundContext.annotatedTypes(TestCodegenExtensionProvider.WEIGHT); + for (TypeInfo typeInfo : typeInfos) { + process(typeInfo); + } + } + + private void process(TypeInfo typeInfo) { + TypeName typeName = typeInfo.typeName(); + TypeName generatedType = TypeName.builder() + .packageName(typeName.packageName()) + .className(typeName.className() + "__Generated") + .build(); + + var classModel = ClassModel.builder() + .type(generatedType) + .addAnnotation(annotation()) + .addField(f -> f + .isStatic(true) + .isFinal(true) + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .name("ANNOTATION") + .type(Annotation.class) + .addContentCreate(annotation())) + .addField(f -> f + .name("field") + .type(String.class) + .addAnnotation(annotation())) + .addConstructor(ctr -> ctr.addAnnotation(annotation())) + .addMethod(method -> method + .name("method") + .addAnnotation(annotation())); + + ctx.filer().writeSourceFile(classModel.build()); + } + + private Annotation annotation() { + return Annotation.builder() + .typeName(CRAZY) + .putValue("stringValue", "value1") + .putValue("booleanValue", true) + .putValue("longValue", 49L) + .putValue("doubleValue", 49.0D) + .putValue("intValue", 49) + .putValue("byteValue", (byte) 49) + .putValue("charValue", 'x') + .putValue("shortValue", (short) 49) + .putValue("floatValue", 49.0F) + .putValue("classValue", String.class) + .putValue("typeValue", TypeName.create(String.class)) + .putValue("enumValue", ElementType.FIELD) + .putValue("annotationValue", targetAnnotation(ElementType.CONSTRUCTOR)) + .putValue("lstring", List.of("value1", "value2")) + .putValue("lboolean", List.of(true, false)) + .putValue("llong", List.of(49L, 50L)) + .putValue("ldouble", List.of(49.0, 50.0)) + .putValue("lint", List.of(49, 50)) + .putValue("lbyte", List.of((byte) 49, (byte) 50)) + .putValue("lchar", List.of('x', 'y')) + .putValue("lshort", List.of((short) 49, (short) 50)) + .putValue("lfloat", List.of(49.0F, 50.0F)) + .putValue("lclass", List.of(String.class, Integer.class)) + .putValue("ltype", + List.of(TypeName.create(String.class), TypeName.create(Integer.class))) + .putValue("lenum", List.of(ElementType.FIELD, ElementType.MODULE)) + .putValue("lannotation", List.of(targetAnnotation(ElementType.CONSTRUCTOR), + targetAnnotation(ElementType.FIELD))) + .putValue("emptyList", List.of()) + .putValue("singletonList", List.of("value")) + .build(); + } + + private Annotation targetAnnotation(ElementType elementType) { + return Annotation.builder() + .typeName(TARGET) + .putValue("value", elementType) + .build(); + } +} diff --git a/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtensionProvider.java b/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtensionProvider.java new file mode 100644 index 00000000000..b9966fb9745 --- /dev/null +++ b/codegen/tests/test-codegen/src/main/java/io/helidon/codegen/test/codegen/TestCodegenExtensionProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.codegen.test.codegen; + +import java.util.Set; + +import io.helidon.codegen.CodegenContext; +import io.helidon.codegen.spi.CodegenExtension; +import io.helidon.codegen.spi.CodegenExtensionProvider; +import io.helidon.common.Weight; +import io.helidon.common.types.TypeName; + +public class TestCodegenExtensionProvider implements CodegenExtensionProvider { + static final TypeName WEIGHT = TypeName.create(Weight.class); + + @Override + public CodegenExtension create(CodegenContext ctx, TypeName generatorType) { + return new TestCodegenExtension(ctx); + } + + @Override + public Set supportedAnnotations() { + return Set.of(WEIGHT); + } +} diff --git a/codegen/tests/test-codegen/src/main/java/module-info.java b/codegen/tests/test-codegen/src/main/java/module-info.java new file mode 100644 index 00000000000..092592da857 --- /dev/null +++ b/codegen/tests/test-codegen/src/main/java/module-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.helidon.codegen.test.codegen.TestCodegenExtensionProvider; + +module io.helidon.codegen.tests.codegen { + requires io.helidon.codegen; + requires io.helidon.codegen.classmodel; + + provides io.helidon.codegen.spi.CodegenExtensionProvider + with TestCodegenExtensionProvider; +} \ No newline at end of file diff --git a/common/types/src/main/java/io/helidon/common/types/EnumValue.java b/common/types/src/main/java/io/helidon/common/types/EnumValue.java index 34fa57b6990..3a405d354f2 100644 --- a/common/types/src/main/java/io/helidon/common/types/EnumValue.java +++ b/common/types/src/main/java/io/helidon/common/types/EnumValue.java @@ -16,6 +16,8 @@ package io.helidon.common.types; +import java.util.Objects; + /** * When creating an {@link io.helidon.common.types.Annotation}, we may need to create an enum value * without access to the enumeration. @@ -31,17 +33,24 @@ public interface EnumValue { * @return enum value */ static EnumValue create(TypeName enumType, String enumName) { - return new EnumValue() { - @Override - public TypeName type() { - return enumType; - } + Objects.requireNonNull(enumType); + Objects.requireNonNull(enumName); + return new EnumValueImpl(enumType, enumName); + } + + /** + * Create a new enum value. + * + * @param type enum type + * @param value enum value constant + * @return new enum value + * @param type of the enum + */ + static > EnumValue create(Class type, T value) { + Objects.requireNonNull(type); + Objects.requireNonNull(value); - @Override - public String name() { - return enumName; - } - }; + return new EnumValueImpl(TypeName.create(type), value.name()); } /** diff --git a/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java b/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java new file mode 100644 index 00000000000..76bd62f24dc --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.Objects; + +final class EnumValueImpl implements EnumValue { + private final TypeName type; + private final String name; + + EnumValueImpl(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EnumValue enumValue)) { + return false; + } + return Objects.equals(type, enumValue.type()) && Objects.equals(name, enumValue.name()); + } + + @Override + public int hashCode() { + return Objects.hash(type, name); + } +}