Optionally include all records from a list of packages

Closes #67
This commit is contained in:
Jordan Zimmerman
2021-09-22 10:34:46 -05:00
parent 71e22042e9
commit 0d2fe2ffe7
13 changed files with 321 additions and 69 deletions

View File

@@ -286,6 +286,9 @@ public void Placeholder {
}
```
`@RecordBuilder.Include` also supports a `packages` attribute that includes all records
in the listed packages.
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).

View File

@@ -25,12 +25,32 @@ public @interface RecordBuilder {
@Retention(RetentionPolicy.SOURCE)
@Inherited
@interface Include {
Class<?>[] value();
/**
* @return list of classes to include
*/
Class<?>[] value() default {};
/**
* Synonym for {@code value()}. When using the other attributes it maybe more clear to
* use {@code classes()} instead of {@code value()}. Note: both attributes are applied
* (i.e. a union of classes from both attributes).
*
* @return list of classes
*/
Class<?>[] classes() default {};
/**
* Optional list of package names. All records in the packages will get processed as
* if they were listed as classes to include.
*
* @return list of package names
*/
String[] packages() default {};
/**
* 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
* is replaced with the package of the {@code Include} annotation. '*' is replaced with
* the package of the included class.
*
* @return package pattern

View File

@@ -27,14 +27,31 @@ public @interface RecordInterface {
@Retention(RetentionPolicy.SOURCE)
@Inherited
@interface Include {
Class<?>[] value();
/**
* @return list of classes to include
*/
Class<?>[] value() default {};
/**
* Synonym for {@code value()}. When using the other attributes it maybe more clear to
* use {@code classes()} instead of {@code value()}. Note: both attributes are applied
* (i.e. a union of classes from both attributes).
*
* @return list of classes
*/
Class<?>[] classes() default {};
/**
* If true the generated record is annotated with {@code @RecordBuilder}
*
* @return true/false
*/
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
* is replaced with the package of the {@code Include} annotation. '*' is replaced with
* the package of the included class.
*
* @return package pattern

View File

@@ -52,12 +52,19 @@ public class ElementUtils {
}
@SuppressWarnings("unchecked")
public static List<TypeMirror> getClassesAttribute(AnnotationValue attribute)
public static List<TypeMirror> getAttributeTypeMirrorList(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());
}
@SuppressWarnings("unchecked")
public static List<String> getAttributeStringList(AnnotationValue attribute)
{
List<? extends AnnotationValue> values = (attribute != null) ? (List<? extends AnnotationValue>)attribute.getValue() : Collections.emptyList();
return values.stream().map(v -> (String)v.getValue()).collect(Collectors.toList());
}
public static boolean getBooleanAttribute(AnnotationValue attribute)
{
Object value = (attribute != null) ? attribute.getValue() : null;

View File

@@ -0,0 +1,96 @@
/**
* 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.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
class IncludeHelper {
private final boolean isValid;
private final List<TypeElement> classTypeElements;
private final Map<? extends ExecutableElement, ? extends AnnotationValue> annotationValues;
IncludeHelper(ProcessingEnvironment processingEnv, Element element, AnnotationMirror annotationMirror, boolean packagesSupported) {
annotationValues = processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirror);
var value = ElementUtils.getAnnotationValue(annotationValues, "value");
var classes = ElementUtils.getAnnotationValue(annotationValues, "classes");
var packages = ElementUtils.getAnnotationValue(annotationValues, "packages");
var isValid = true;
var classTypeElements = new ArrayList<TypeElement>();
if (value.isPresent() || classes.isPresent() || packages.isPresent()) {
var valueList = value.map(ElementUtils::getAttributeTypeMirrorList).orElseGet(List::of);
var classesList = classes.map(ElementUtils::getAttributeTypeMirrorList).orElseGet(List::of);
var packagesList = packages.map(ElementUtils::getAttributeStringList).orElseGet(List::of);
if (valueList.isEmpty() && classesList.isEmpty() && packagesList.isEmpty()) {
if (packagesSupported) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "At least one of \"value\", \"classes\" or \"packages\" required", element);
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "At least one of \"value\" or \"classes\" required", element);
}
isValid = false;
}
isValid = processList(processingEnv, isValid, element, valueList, classTypeElements);
isValid = processList(processingEnv, isValid, element, classesList, classTypeElements);
packages.ifPresent(annotationValue -> processPackages(processingEnv, classTypeElements, packagesList));
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not read attribute for annotation", element);
isValid = false;
}
this.isValid = isValid;
this.classTypeElements = List.copyOf(classTypeElements);
}
Map<? extends ExecutableElement, ? extends AnnotationValue> getAnnotationValues() {
return annotationValues;
}
boolean isValid() {
return isValid;
}
List<TypeElement> getClassTypeElements() {
return classTypeElements;
}
private boolean processList(ProcessingEnvironment processingEnv, boolean isValid, Element element, List<TypeMirror> list, ArrayList<TypeElement> classTypeElements) {
for (var typeMirror : list) {
TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement(typeMirror);
if (typeElement == null) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get element for: " + typeMirror, element);
isValid = false;
} else {
classTypeElements.add(typeElement);
}
}
return isValid;
}
private void processPackages(ProcessingEnvironment processingEnv, List<TypeElement> classTypeElements, List<String> packagesList) {
for (var packageName : packagesList) {
var packageElement = processingEnv.getElementUtils().getPackageElement(packageName);
for (var child : packageElement.getEnclosedElements()) {
if (child.getKind() == ElementKind.RECORD) {
classTypeElements.add((TypeElement) child);
}
}
}
}
}

View File

@@ -29,10 +29,8 @@ 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;
@@ -40,8 +38,7 @@ import java.util.Set;
import java.util.function.Function;
public class RecordBuilderProcessor
extends AbstractProcessor
{
extends AbstractProcessor {
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();
@@ -51,21 +48,18 @@ public class RecordBuilderProcessor
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)
{
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> process(annotation, element)));
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes()
{
public Set<String> getSupportedAnnotationTypes() {
return Set.of("*");
}
@Override
public SourceVersion getSupportedSourceVersion()
{
public SourceVersion getSupportedSourceVersion() {
// we don't directly return RELEASE_14 as that may
// not exist in prior releases
// if we're running on an older release, returning latest()
@@ -73,18 +67,15 @@ public class RecordBuilderProcessor
return SourceVersion.latest();
}
private void process(TypeElement annotation, Element element)
{
private void process(TypeElement annotation, Element element) {
String annotationClass = annotation.getQualifiedName().toString();
if (annotationClass.equals(RECORD_BUILDER)) {
var typeElement = (TypeElement) element;
processRecordBuilder(typeElement, getMetaData(typeElement), Optional.empty());
}
else if (annotationClass.equals(RECORD_INTERFACE)) {
} else if (annotationClass.equals(RECORD_INTERFACE)) {
var typeElement = (TypeElement) element;
processRecordInterface(typeElement, element.getAnnotation(RecordInterface.class).addRecordBuilder(), getMetaData(typeElement), Optional.empty(), false);
}
else if (annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE)) {
} else if (annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE)) {
var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
processIncludes(element, metaData, annotationClass);
} else {
@@ -104,37 +95,24 @@ public class RecordBuilderProcessor
return (recordSpecificMetaData != null) ? recordSpecificMetaData : RecordBuilderOptions.build(processingEnv.getOptions());
}
private void processIncludes(Element element, RecordBuilder.Options metaData, String annotationClass)
{
private void processIncludes(Element element, RecordBuilder.Options metaData, String annotationClass) {
var isRecordBuilderInclude = annotationClass.equals(RECORD_BUILDER_INCLUDE);
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 (packageName != null) {
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), false);
}
else {
processRecordBuilder(typeElement, metaData, Optional.of(packageName));
}
} else {
var includeHelper = new IncludeHelper(processingEnv, element, annotationMirrorOpt.get(), isRecordBuilderInclude);
if (includeHelper.isValid()) {
var packagePattern = ElementUtils.getStringAttribute(ElementUtils.getAnnotationValue(includeHelper.getAnnotationValues(), "packagePattern").orElse(null), "*");
for (var typeElement : includeHelper.getClassTypeElements()) {
var packageName = buildPackageName(packagePattern, element, typeElement);
if (packageName != null) {
if (isRecordBuilderInclude) {
processRecordBuilder(typeElement, metaData, Optional.of(packageName));
} else {
var addRecordBuilderOpt = ElementUtils.getAnnotationValue(includeHelper.getAnnotationValues(), "addRecordBuilder");
var addRecordBuilder = addRecordBuilderOpt.map(ElementUtils::getBooleanAttribute).orElse(true);
processRecordInterface(typeElement, addRecordBuilder, metaData, Optional.of(packageName), false);
}
}
}
@@ -142,8 +120,7 @@ public class RecordBuilderProcessor
}
}
private String buildPackageName(String packagePattern, Element builderElement, TypeElement includedClass)
{
private String buildPackageName(String packagePattern, Element builderElement, TypeElement includedClass) {
PackageElement includedClassPackage = findPackageElement(includedClass, includedClass);
if (includedClassPackage == null) {
return null;
@@ -155,8 +132,7 @@ public class RecordBuilderProcessor
return replaced.replace("@", ((PackageElement) builderElement.getEnclosingElement()).getQualifiedName().toString());
}
private PackageElement findPackageElement(Element actualElement, Element includedClass)
{
private PackageElement findPackageElement(Element actualElement, Element includedClass) {
if (includedClass == null) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Element has not package", actualElement);
return null;
@@ -167,8 +143,7 @@ public class RecordBuilderProcessor
return findPackageElement(actualElement, includedClass.getEnclosingElement());
}
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageName, boolean fromTemplate)
{
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageName, boolean fromTemplate) {
if (!element.getKind().isInterface()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordInterface only valid for interfaces.", element);
return;
@@ -180,8 +155,7 @@ public class RecordBuilderProcessor
writeRecordInterfaceJavaFile(element, internalProcessor.packageName(), internalProcessor.recordClassType(), internalProcessor.recordType(), metaData, internalProcessor::toRecord);
}
private void processRecordBuilder(TypeElement record, RecordBuilder.Options metaData, Optional<String> packageName)
{
private void processRecordBuilder(TypeElement record, RecordBuilder.Options 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
@@ -194,8 +168,7 @@ public class RecordBuilderProcessor
writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
}
private void writeRecordBuilderJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilder.Options metaData)
{
private void writeRecordBuilderJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilder.Options metaData) {
// produces the Java file
JavaFile javaFile = javaFileBuilder(packageName, builderType, metaData);
Filer filer = processingEnv.getFiler();
@@ -205,14 +178,12 @@ public class RecordBuilderProcessor
try (Writer writer = sourceFile.openWriter()) {
javaFile.writeTo(writer);
}
}
catch (IOException e) {
} catch (IOException e) {
handleWriteError(record, e);
}
}
private void writeRecordInterfaceJavaFile(TypeElement element, String packageName, ClassType classType, TypeSpec type, RecordBuilder.Options metaData, Function<String, String> toRecordProc)
{
private void writeRecordInterfaceJavaFile(TypeElement element, String packageName, ClassType classType, TypeSpec type, RecordBuilder.Options metaData, Function<String, String> toRecordProc) {
JavaFile javaFile = javaFileBuilder(packageName, type, metaData);
String classSourceCode = javaFile.toString();
@@ -226,14 +197,12 @@ public class RecordBuilderProcessor
try (Writer writer = sourceFile.openWriter()) {
writer.write(recordSourceCode);
}
}
catch (IOException e) {
} catch (IOException e) {
handleWriteError(element, e);
}
}
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilder.Options metaData)
{
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilder.Options metaData) {
var javaFileBuilder = JavaFile.builder(packageName, type).skipJavaLangImports(true).indent(metaData.fileIndent());
var comment = metaData.fileComment();
if ((comment != null) && !comment.isEmpty()) {
@@ -242,8 +211,7 @@ public class RecordBuilderProcessor
return javaFileBuilder.build();
}
private void handleWriteError(TypeElement element, IOException e)
{
private void handleWriteError(TypeElement element, IOException e) {
String message = "Could not create source file";
if (e.getMessage() != null) {
message = message + ": " + e.getMessage();

View File

@@ -0,0 +1,25 @@
/**
* 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.includes;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder.Include(
packages = "io.soabase.recordbuilder.test.includes.pack",
classes = JustATest.class
)
public class IncludeFactory {
}

View File

@@ -0,0 +1,19 @@
/**
* 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.includes;
public record JustATest(int i) {
}

View File

@@ -0,0 +1,19 @@
/**
* 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.includes.pack;
public interface AlsoIgnoreMe {
}

View File

@@ -0,0 +1,19 @@
/**
* 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.includes.pack;
public class IgnoreMe {
}

View File

@@ -0,0 +1,19 @@
/**
* 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.includes.pack;
public record PackRecord1(String name) {
}

View File

@@ -0,0 +1,19 @@
/**
* 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.includes.pack;
public record PackRecord2(String name, int age) {
}

View File

@@ -0,0 +1,21 @@
/**
* 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.includes.pack;
import java.time.Instant;
public record PackRecord3(Instant time, int age) {
}