Support Include versions of the annotation

This commit is contained in:
Jordan Zimmerman
2020-10-05 14:42:02 -05:00
parent 7811ff8823
commit 410cae4d32
13 changed files with 360 additions and 44 deletions

View File

@@ -231,6 +231,31 @@ Notes:
- Methods with default implementations are used in the generation unless they are annotated with `@IgnoreDefaultMethod`
- If you do not want a record builder generated, annotate your interface as `@RecordInterface(addRecordBuilder = false)`
## Generation Via Includes
An alternate method of generation is to use the Include variants of the annotations. These variants
act on lists of specified classes. This allows the source classes to be pristine or even come from
libraries where you are not able to annotate the source.
E.g.
```
import some.library.code.ImportedRecord
import some.library.code.ImportedInterface
@RecordBuilder.Include({
ImportedRecord.class // generates a record builder for ImportedRecord
})
@RecordInterface.Include({
ImportedInterface.class // generates a record interface for ImportedInterface
})
public void Placeholder {
}
```
The target package for generation is the same as the package that contains the "Include"
annotation. Use `packagePattern` to change this (see Javadoc for details).
## Usage
### Maven

View File

@@ -23,4 +23,19 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface RecordBuilder {
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@interface Include {
Class<?>[] value();
/**
* Pattern used to generate the package for the generated class. The value
* is the literal package name however two replacement values can be used. '@'
* is replaced with the package of the Include annotation. '*' is replaced with
* the package of the included class.
*
* @return package pattern
*/
String packagePattern() default "@";
}
}

View File

@@ -24,4 +24,22 @@ import java.lang.annotation.Target;
@Target(ElementType.TYPE)
public @interface RecordInterface {
boolean addRecordBuilder() default true;
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@interface Include {
Class<?>[] value();
boolean addRecordBuilder() default true;
/**
* Pattern used to generate the package for the generated class. The value
* is the literal package name however two replacement values can be used. '@'
* is replaced with the package of the Include annotation. '*' is replaced with
* the package of the included class.
*
* @return package pattern
*/
String packagePattern() default "@";
}
}

View File

@@ -20,14 +20,63 @@ import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.TypeMirror;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
public class ElementUtils {
public static Optional<? extends AnnotationMirror> findAnnotationMirror(ProcessingEnvironment processingEnv, Element element, String annotationClass) {
return processingEnv.getElementUtils().getAllAnnotationMirrors(element).stream()
.filter(e -> e.getAnnotationType().toString().equals(annotationClass))
.findFirst();
}
public static Optional<? extends AnnotationValue> getAnnotationValue(Map<? extends ExecutableElement, ? extends AnnotationValue> values, String name) {
return values.entrySet()
.stream()
.filter(e -> e.getKey().getSimpleName().toString().equals(name))
.map(Map.Entry::getValue)
.findFirst();
}
@SuppressWarnings("unchecked")
public static List<TypeMirror> getClassesAttribute(AnnotationValue attribute)
{
List<? extends AnnotationValue> values = (attribute != null) ? (List<? extends AnnotationValue>)attribute.getValue() : Collections.emptyList();
return values.stream().map(v -> (TypeMirror)v.getValue()).collect(Collectors.toList());
}
public static boolean getBooleanAttribute(AnnotationValue attribute)
{
Object value = (attribute != null) ? attribute.getValue() : null;
if ( value != null )
{
return Boolean.parseBoolean(String.valueOf(value));
}
return false;
}
public static String getStringAttribute(AnnotationValue attribute, String defaultValue)
{
Object value = (attribute != null) ? attribute.getValue() : null;
if ( value != null )
{
return String.valueOf(value);
}
return defaultValue;
}
public static String getPackageName(TypeElement typeElement) {
while (typeElement.getNestingKind().isNested()) {
Element enclosingElement = typeElement.getEnclosingElement();

View File

@@ -33,6 +33,7 @@ import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -53,11 +54,11 @@ class InternalRecordBuilderProcessor
private final TypeSpec builderType;
private final TypeSpec.Builder builder;
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData)
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageNameOpt)
{
this.metaData = metaData;
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
packageName = ElementUtils.getPackageName(record);
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(record));
builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters());
typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
recordComponents = record.getRecordComponents().stream().map(ElementUtils::getClassType).collect(Collectors.toList());

View File

@@ -15,12 +15,14 @@
*/
package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.*;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import io.soabase.recordbuilder.core.RecordInterface;
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
@@ -28,7 +30,13 @@ import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.tools.Diagnostic;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -46,9 +54,9 @@ class InternalRecordInterfaceProcessor {
private static final String FAKE_METHOD_NAME = "__FAKE__";
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, RecordInterface recordInterface, RecordBuilderMetaData metaData) {
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv;
packageName = ElementUtils.getPackageName(iface);
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
recordComponents = getRecordComponents(iface);
this.iface = iface;
@@ -65,7 +73,7 @@ class InternalRecordInterfaceProcessor {
.addAnnotation(generatedRecordInterfaceAnnotation)
.addTypeVariables(typeVariables);
if (recordInterface.addRecordBuilder()) {
if (addRecordBuilder) {
ClassType builderClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, recordClassType, metaData.suffix()) + "." + metaData.withClassName(), iface.getTypeParameters());
builder.addAnnotation(RecordBuilder.class);
builder.addSuperinterface(builderClassType.typeName());

View File

@@ -18,39 +18,46 @@ package io.soabase.recordbuilder.processor;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import io.soabase.recordbuilder.core.RecordInterface;
import javax.annotation.processing.*;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Generated;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.RECORD_BUILDER;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.RECORD_INTERFACE;
@SupportedAnnotationTypes({RECORD_BUILDER, RECORD_INTERFACE})
public class RecordBuilderProcessor extends AbstractProcessor {
public static final String RECORD_BUILDER = "io.soabase.recordbuilder.core.RecordBuilder";
public static final String RECORD_INTERFACE = "io.soabase.recordbuilder.core.RecordInterface";
private static final String RECORD_BUILDER = RecordBuilder.class.getName();
private static final String RECORD_BUILDER_INCLUDE = RecordBuilder.Include.class.getName().replace('$', '.');
private static final String RECORD_INTERFACE = RecordInterface.class.getName();
private static final String RECORD_INTERFACE_INCLUDE = RecordInterface.Include.class.getName().replace('$', '.');
static final AnnotationSpec generatedRecordBuilderAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RECORD_BUILDER).build();
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RECORD_INTERFACE).build();
static final AnnotationSpec generatedRecordBuilderAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordBuilder.class.getName()).build();
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordInterface.class.getName()).build();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation)
.forEach(element -> process(annotation, element))
);
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> process(annotation, element)));
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(RECORD_BUILDER, RECORD_BUILDER_INCLUDE, RECORD_INTERFACE, RECORD_INTERFACE_INCLUDE);
}
@Override
public SourceVersion getSupportedSourceVersion() {
// we don't directly return RELEASE_14 as that may
@@ -63,37 +70,102 @@ public class RecordBuilderProcessor extends AbstractProcessor {
private void process(TypeElement annotation, Element element) {
var metaData = new RecordBuilderMetaDataLoader(processingEnv, s -> processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
if (annotation.getQualifiedName().toString().equals(RECORD_BUILDER)) {
processRecordBuilder((TypeElement) element, metaData);
} else if (annotation.getQualifiedName().toString().equals(RECORD_INTERFACE)) {
processRecordInterface((TypeElement) element, element.getAnnotation(RecordInterface.class), metaData);
} else {
String annotationClass = annotation.getQualifiedName().toString();
if ( annotationClass.equals(RECORD_BUILDER) )
{
processRecordBuilder((TypeElement)element, metaData, Optional.empty());
}
else if ( annotationClass.equals(RECORD_INTERFACE) )
{
processRecordInterface((TypeElement)element, element.getAnnotation(RecordInterface.class).addRecordBuilder(), metaData, Optional.empty());
}
else if ( annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
{
processIncludes(element, metaData, annotationClass);
}
else
{
throw new RuntimeException("Unknown annotation: " + annotation);
}
}
private void processRecordInterface(TypeElement element, RecordInterface recordInterface, RecordBuilderMetaData metaData) {
if (!element.getKind().isInterface()) {
private void processIncludes(Element element, RecordBuilderMetaData metaData, String annotationClass) {
var annotationMirrorOpt = ElementUtils.findAnnotationMirror(processingEnv, element, annotationClass);
if ( annotationMirrorOpt.isEmpty() )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation mirror for: " + annotationClass, element);
}
else
{
var values = processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirrorOpt.get());
var classes = ElementUtils.getAnnotationValue(values, "value");
if ( classes.isEmpty() )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation value for: " + annotationClass, element);
}
else
{
var packagePattern = ElementUtils.getStringAttribute(ElementUtils.getAnnotationValue(values, "packagePattern").orElse(null), "*");
var classesMirrors = ElementUtils.getClassesAttribute(classes.get());
for ( TypeMirror mirror : classesMirrors )
{
TypeElement typeElement = (TypeElement)processingEnv.getTypeUtils().asElement(mirror);
if ( typeElement == null )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get element for: " + mirror, element);
}
else
{
var packageName = buildPackageName(packagePattern, element, typeElement);
if ( annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
{
var addRecordBuilderOpt = ElementUtils.getAnnotationValue(values, "addRecordBuilder");
var addRecordBuilder = addRecordBuilderOpt.map(ElementUtils::getBooleanAttribute).orElse(true);
processRecordInterface(typeElement, addRecordBuilder, metaData, Optional.of(packageName));
}
else
{
processRecordBuilder(typeElement, metaData, Optional.of(packageName));
}
}
}
}
}
}
private String buildPackageName(String packagePattern, Element builderElement, TypeElement includedClass) {
String replaced = packagePattern.replace("*", ((PackageElement)includedClass.getEnclosingElement()).getQualifiedName().toString());
if (builderElement instanceof PackageElement) {
return replaced.replace("@", ((PackageElement)builderElement).getQualifiedName().toString());
}
return replaced.replace("@", ((PackageElement)builderElement.getEnclosingElement()).getQualifiedName().toString());
}
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageName) {
if ( !element.getKind().isInterface() )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordInterface only valid for interfaces.", element);
return;
}
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, recordInterface, metaData);
if (!internalProcessor.isValid()) {
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, addRecordBuilder, metaData, packageName);
if ( !internalProcessor.isValid() )
{
return;
}
writeRecordInterfaceJavaFile(element, internalProcessor.packageName(), internalProcessor.recordClassType(), internalProcessor.recordType(), metaData, internalProcessor::toRecord);
}
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData metaData) {
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageName) {
// we use string based name comparison for the element kind,
// as the ElementKind.RECORD enum doesn't exist on JRE releases
// older than Java 14, and we don't want to throw unexpected
// NoSuchFieldErrors
if (!"RECORD".equals(record.getKind().name())) {
if ( !"RECORD".equals(record.getKind().name()) )
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordBuilder only valid for records.", record);
return;
}
var internalProcessor = new InternalRecordBuilderProcessor(record, metaData);
var internalProcessor = new InternalRecordBuilderProcessor(record, metaData, packageName);
writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
}
@@ -105,7 +177,7 @@ public class RecordBuilderProcessor extends AbstractProcessor {
{
String fullyQualifiedName = packageName.isEmpty() ? builderClassType.name() : (packageName + "." + builderClassType.name());
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
try ( Writer writer = sourceFile.openWriter() )
try (Writer writer = sourceFile.openWriter())
{
javaFile.writeTo(writer);
}
@@ -128,7 +200,7 @@ public class RecordBuilderProcessor extends AbstractProcessor {
{
String fullyQualifiedName = packageName.isEmpty() ? classType.name() : (packageName + "." + classType.name());
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
try ( Writer writer = sourceFile.openWriter() )
try (Writer writer = sourceFile.openWriter())
{
writer.write(recordSourceCode);
}
@@ -140,11 +212,10 @@ public class RecordBuilderProcessor extends AbstractProcessor {
}
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilderMetaData metaData) {
var javaFileBuilder = JavaFile.builder(packageName, type)
.skipJavaLangImports(true)
.indent(metaData.fileIndent());
var javaFileBuilder = JavaFile.builder(packageName, type).skipJavaLangImports(true).indent(metaData.fileIndent());
var comment = metaData.fileComment();
if ((comment != null) && !comment.isEmpty()) {
if ( (comment != null) && !comment.isEmpty() )
{
javaFileBuilder.addFileComment(comment);
}
return javaFileBuilder.build();
@@ -152,7 +223,8 @@ public class RecordBuilderProcessor extends AbstractProcessor {
private void handleWriteError(TypeElement element, IOException e) {
String message = "Could not create source file";
if (e.getMessage() != null) {
if ( e.getMessage() != null )
{
message = message + ": " + e.getMessage();
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);

View File

@@ -0,0 +1,24 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* 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.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordInterface;
@RecordInterface.Include({
Thingy.class
})
public class Builder {
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* 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.soabase.recordbuilder.test;
import java.time.Instant;
public interface Customer {
String name();
String address();
Instant activeDate();
}

View File

@@ -0,0 +1,18 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* 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.soabase.recordbuilder.test;
public record Pair<T, U>(T t, U u) {}

View File

@@ -0,0 +1,18 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* 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.soabase.recordbuilder.test;
public record Point(int x, int y) {}

View File

@@ -0,0 +1,20 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* 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.soabase.recordbuilder.test;
public interface Thingy<T> {
T getIt();
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* 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.
*/
@RecordBuilder.Include(value = {Point.class, Pair.class}, packagePattern = "*.foo")
@RecordInterface.Include(value = Customer.class, addRecordBuilder = false, packagePattern = "*.bar")
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordInterface;