findAnnotation(
return Optional.empty();
}
+
+ /**
+ * Determines whether a given type element is a functional interface.
+ *
+ * Prefers the explicit @FunctionalInterface annotation. Otherwise, returns true only if the
+ * element is an interface and declares exactly one abstract instance method (ignoring static and
+ * default methods). Inherited abstract methods are ignored for simplicity.
+ */
+ public static boolean isFunctionalInterface(TypeElement typeElement) {
+ if (typeElement == null) {
+ return false;
+ }
+
+ // Prefer explicit annotation
+ if (JavaLangAnalyser.findAnnotation(typeElement, FunctionalInterface.class).isPresent()) {
+ return true;
+ }
+ // Only interfaces can be functional interfaces
+ if (typeElement.getKind() != javax.lang.model.element.ElementKind.INTERFACE) {
+ return false;
+ }
+ // Heuristic: exactly one abstract method declared (ignores inherited ones for simplicity)
+ long abstractDeclared =
+ ElementFilter.methodsIn(typeElement.getEnclosedElements()).stream()
+ .filter(m -> !m.getModifiers().contains(javax.lang.model.element.Modifier.STATIC))
+ .filter(m -> !m.getModifiers().contains(javax.lang.model.element.Modifier.DEFAULT))
+ .count();
+ return abstractDeclared == 1;
+ }
+
+ /**
+ * Extracts the Javadoc text following the @param tag for the given parameter name. Continuation
+ * lines are supported until the next Javadoc tag (starting with '@') or an empty line.
+ *
+ * @param javaDoc the full raw Javadoc as returned by Elements.getDocComment(...)
+ * @param parameter the parameter to look for
+ * @return the extracted text or null if not found or empty
+ */
+ public static String extractParamJavaDoc(String javaDoc, VariableElement parameter) {
+ if (javaDoc == null || parameter == null) {
+ return null;
+ }
+ String parameterName = parameter.getSimpleName().toString();
+ String[] lines = javaDoc.split("\r?\n");
+ boolean capture = false;
+ StringBuilder sb = new StringBuilder();
+ for (String rawLine : lines) {
+ String line = rawLine.trim();
+ if (line.startsWith("*")) {
+ line = line.substring(1).trim();
+ }
+ if (line.startsWith("@")) {
+ // new tag begins; stop capturing unless this is our @param start
+ capture = false;
+ if (line.startsWith("@param ")) {
+ // format: @param text...
+ String rest = line.substring("@param ".length()).trim();
+ int sp = rest.indexOf(' ');
+ String name = sp >= 0 ? rest.substring(0, sp) : rest;
+ String text = sp >= 0 ? rest.substring(sp + 1).trim() : "";
+ if (Strings.CI.equals(parameterName, name)) {
+ if (!text.isEmpty()) {
+ sb.append(text);
+ }
+ capture = true;
+ }
+ }
+ continue;
+ }
+ if (capture) {
+ if (line.isEmpty()) {
+ break;
+ }
+ if (sb.length() > 0) sb.append(' ');
+ sb.append(line);
+ } else {
+ // TODO: add logging for not found javadoc for parameter
+ }
+ }
+ String result = sb.toString().trim();
+ return result.isEmpty() ? null : result;
+ }
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java
index 52e0b8ea..68c50fe8 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java
@@ -33,20 +33,17 @@
import java.util.Set;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.PrimitiveType;
-import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleTypeVisitor14;
import javax.lang.model.util.Types;
-import org.javahelpers.simple.builders.processor.dtos.MethodParameterDto;
-import org.javahelpers.simple.builders.processor.dtos.TypeName;
-import org.javahelpers.simple.builders.processor.dtos.TypeNameArray;
-import org.javahelpers.simple.builders.processor.dtos.TypeNameGeneric;
-import org.javahelpers.simple.builders.processor.dtos.TypeNamePrimitive;
+import org.javahelpers.simple.builders.processor.dtos.*;
/** Helper functions to create simple builder types from java.lang types. */
public final class JavaLangMapper {
@@ -78,6 +75,26 @@ public static TypeName map2TypeName(Class> clazz) {
return new TypeName(clazz.getPackageName(), clazz.getSimpleName());
}
+ /** Maps the declared type parameters of the given type element into GenericParameterDto list. */
+ public static List map2GenericParameterDtos(
+ TypeElement type, Elements elementUtils, Types typeUtils) {
+ return type.getTypeParameters().stream()
+ .map(tp -> map2GenericParameterDto(tp, elementUtils, typeUtils))
+ .toList();
+ }
+
+ /** Maps a single {@code TypeParameterElement} to {@code GenericParameterDto}. */
+ public static GenericParameterDto map2GenericParameterDto(
+ TypeParameterElement tp, Elements elementUtils, Types typeUtils) {
+ GenericParameterDto g = new GenericParameterDto();
+ g.setName(tp.getSimpleName().toString());
+ tp.getBounds().stream()
+ .filter(b -> !"java.lang.Object".equals(b.toString()))
+ .map(b -> extractType(b, elementUtils, typeUtils))
+ .forEach(g::addUpperBound);
+ return g;
+ }
+
/**
* Mapping a method parameter to {@code
* org.javahelpers.simple.builders.processor.dtos.MethodParameterDto}.
@@ -142,7 +159,7 @@ public TypeName visitDeclared(DeclaredType t, Void p) {
TypeName rawType = new TypeName(packageName, simpleClassName);
TypeMirror enclosingType = t.getEnclosingType();
TypeName enclosing =
- (enclosingType.getKind() != TypeKind.NONE)
+ (enclosingType.getKind() != NONE)
&& !t.asElement().getModifiers().contains(Modifier.STATIC)
? enclosingType.accept(this, null)
: null;
@@ -166,6 +183,12 @@ public TypeNameArray visitArray(ArrayType t, Void _p) {
extractType(t.getComponentType(), elementUtils, typeUtils), false);
}
+ @Override
+ public TypeName visitTypeVariable(TypeVariable t, Void _p) {
+ String name = t.asElement().getSimpleName().toString();
+ return new TypeNameVariable(name);
+ }
+
@Override
protected TypeName defaultAction(TypeMirror e, Void _p) {
throw new IllegalArgumentException("Unexpected type mirror: " + e);
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java
index 0164b988..fd271988 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java
@@ -24,13 +24,13 @@
package org.javahelpers.simple.builders.processor.util;
-import static org.javahelpers.simple.builders.processor.dtos.TypeNamePrimitive.PrimitiveTypeEnum.BOOLEAN;
-
import com.palantir.javapoet.ArrayTypeName;
import com.palantir.javapoet.ClassName;
import com.palantir.javapoet.CodeBlock;
import com.palantir.javapoet.ParameterizedTypeName;
import com.palantir.javapoet.TypeName;
+import com.palantir.javapoet.TypeVariableName;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -100,6 +100,31 @@ public static ClassName map2ClassName(
return ClassName.get(typeName.getPackageName(), typeName.getClassName());
}
+ public static ParameterizedTypeName map2ParameterizedTypeName(
+ org.javahelpers.simple.builders.processor.dtos.TypeName baseType,
+ List builderGenerics) {
+ ClassName baseTypeClassName = map2ClassName(baseType);
+ return ParameterizedTypeName.get(
+ baseTypeClassName, map2TypeVariables(builderGenerics).toArray(new TypeVariableName[0]));
+ }
+
+ public static List map2TypeVariables(
+ List builderGenerics) {
+ List javapoetGenerics = new ArrayList<>();
+ for (GenericParameterDto g : builderGenerics) {
+ List bounds = new ArrayList<>();
+ for (org.javahelpers.simple.builders.processor.dtos.TypeName b : g.getUpperBounds()) {
+ bounds.add(map2ParameterType(b));
+ }
+ TypeVariableName tv =
+ bounds.isEmpty()
+ ? TypeVariableName.get(g.getName())
+ : TypeVariableName.get(g.getName(), bounds.toArray(new TypeName[0]));
+ javapoetGenerics.add(tv);
+ }
+ return javapoetGenerics;
+ }
+
/**
* CodeBlock creating by definition in {@code MethodCodeDto}.
*
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
index 6a96fba7..2670f420 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
@@ -3,6 +3,7 @@
import static com.google.testing.compile.CompilationSubject.assertThat;
import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded;
import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.contains;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.notContains;
import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource;
import com.google.testing.compile.Compilation;
@@ -31,12 +32,13 @@ protected void setUp() {
void shouldFailWhenAnnotationPlacedOnInterface() {
// Given: @SimpleBuilder used on an interface instead of a class
JavaFileObject source =
- JavaFileObjects.forSourceString(
- "test.WrongTargetInterface",
- "package test;\n"
- + "import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;\n"
- + "@SimpleBuilder\n"
- + "public interface WrongTargetInterface { void x(); }\n");
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ @SimpleBuilder
+ public interface WrongTargetInterface { void x(); }
+ """);
// When
Compilation compilation =
@@ -54,12 +56,13 @@ void shouldFailWhenAnnotationPlacedOnInterface() {
void shouldFailWhenAnnotationPlacedOnAbstractClass() {
// Given: @SimpleBuilder used on an abstract class
JavaFileObject source =
- JavaFileObjects.forSourceString(
- "test.AbstractAnnotated",
- "package test;\n"
- + "import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;\n"
- + "@SimpleBuilder\n"
- + "public abstract class AbstractAnnotated { }\n");
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ @SimpleBuilder
+ public abstract class AbstractAnnotated { }
+ """);
// When
Compilation compilation =
@@ -74,7 +77,7 @@ void shouldFailWhenAnnotationPlacedOnAbstractClass() {
}
@Test
- @Disabled("Constructor-Parameters are not supported yet")
+ @Disabled("TODO: Constructor-Parameters are not supported yet")
void shouldIgnoreConstructorParametersAndOnlyUseSetters() {
// Given
String packageName = "test";
@@ -95,7 +98,7 @@ public CtorAndSetter() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
// a has no setter intentionally
- """);
+ """);
// When
Compilation compilation = compile(sourceFile);
@@ -104,36 +107,33 @@ public CtorAndSetter() {}
String generatedCode = loadGeneratedSource(compilation, builderClassName);
assertGenerationSucceeded(compilation, builderClassName, generatedCode);
// Setter-based API should appear (name) and Constructor params (a)
- ProcessorAsserts.assertingResult(
+ ProcessorAsserts.assertContaining(
generatedCode,
- contains("public CtorAndSetterBuilder a(int a)"),
- contains("instance.setA(a);"),
- contains("public CtorAndSetterBuilder name(String name)"),
- contains("instance.setName(name);"));
+ "public CtorAndSetterBuilder a(int a)",
+ "instance.setA(a);",
+ "public CtorAndSetterBuilder name(String name)",
+ "instance.setName(name);");
}
@Test
- @Disabled("Records are not supported for builder generation yet")
+ @Disabled("TODO: Records are not supported for builder generation yet")
void shouldHandleRecordWithoutGeneratingBuilder() {
// Given
- String packageName = "test";
String recordName = "PersonRecord";
String builderClassName = recordName + "Builder";
- String source =
- """
- package test;
-
- import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ JavaFileObject recordFile =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
- @SimpleBuilder
- public record PersonRecord(String name, int age) {
- public String upperName() { return name.toUpperCase(); }
- }
- """;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
- JavaFileObject recordFile =
- JavaFileObjects.forSourceString(packageName + "." + recordName, source);
+ @SimpleBuilder
+ public record PersonRecord(String name, int age) {
+ public String upperName() { return name.toUpperCase(); }
+ }
+ """);
// When
Compilation compilation = compile(recordFile);
@@ -142,12 +142,12 @@ public record PersonRecord(String name, int age) {
String generatedCode = loadGeneratedSource(compilation, builderClassName);
assertGenerationSucceeded(compilation, builderClassName, generatedCode);
// Setter-based API should support record Constructor params (name, age)
- ProcessorAsserts.assertingResult(
+ ProcessorAsserts.assertContaining(
generatedCode,
- contains("public PersonRecordBuilder name(String name)"),
- contains("instance.setName(name);"),
- contains("public PersonRecordBuilder age(int age)"),
- contains("instance.setAge(age);"));
+ "public PersonRecordBuilder name(String name)",
+ "instance.setName(name);",
+ "public PersonRecordBuilder age(int age)",
+ "instance.setAge(age);");
}
@Test
@@ -174,7 +174,7 @@ void shouldGenerateCollectionSettersAndProviders() {
public java.util.Map getMap() { return map; }
public void setMap(java.util.Map map) { this.map = map; }
- """);
+ """);
// When
Compilation compilation = compile(sourceFile);
@@ -184,19 +184,19 @@ void shouldGenerateCollectionSettersAndProviders() {
assertGenerationSucceeded(compilation, builderClassName, generatedCode);
// Direct setters and varargs convenience for List and Set; direct setter for Map
- ProcessorAsserts.assertingResult(
+ ProcessorAsserts.assertContaining(
generatedCode,
- contains("public WithCollectionsBuilder names(List names)"),
- contains("public WithCollectionsBuilder names(Supplier> namesSupplier)"),
- contains("public WithCollectionsBuilder names(String... names)"),
- contains("instance.setNames(List.of(names));"),
- contains("public WithCollectionsBuilder tags(Set tags)"),
- contains("public WithCollectionsBuilder tags(Supplier> tagsSupplier)"),
- contains("public WithCollectionsBuilder tags(String... tags)"),
- contains("instance.setTags(Set.of(tags));"),
- contains("public WithCollectionsBuilder map(Map map)"),
- contains("public WithCollectionsBuilder map(Supplier