Rework how options are specified (#37)

- Remove `RecordBuilderMetaData`
- Unify how the javac options are handled
- Create `RecordBuilder.Options` to specify options
- Allow creation of custom annotations that bundle options
This commit is contained in:
Jordan Zimmerman
2021-06-22 05:06:44 +01:00
parent b7c4e22066
commit 5d7bea565f
15 changed files with 369 additions and 481 deletions

View File

@@ -21,7 +21,7 @@ _Details:_
- [Record From Interface Details](#RecordInterface-Example)
- [Generation Via Includes](#generation-via-includes)
- [Usage](#usage)
- [Customizing](#customizing)
- [Customizing](customizing.md)
- [Java 15 Versions](#java-15-versions)
## RecordBuilder Example
@@ -352,30 +352,13 @@ Depending on your IDE you are likely to need to enable Annotation Processing in
## Customizing
The names of the generated methods, etc. are determined by [RecordBuilderMetaData](https://github.com/Randgalt/record-builder/blob/master/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderMetaData.java). If you want to use your own meta data instance:
- Create a class that implements RecordBuilderMetaData
- When compiling, make sure that the compiled class is in the processor path
- Add a "metaDataClass" compiler option with the class name. E.g. `javac ... -AmetaDataClass=foo.bar.MyMetaData`
Alternatively, you can provide values for each individual meta data (or combinations):
- `javac ... -Asuffix=foo`
- `javac ... -AinterfaceSuffix=foo`
- `javac ... -AcopyMethodName=foo`
- `javac ... -AbuilderMethodName=foo`
- `javac ... -AbuildMethodName=foo`
- `javac ... -AcomponentsMethodName=foo`
- `javac ... -AwithClassName=foo`
- `javac ... -AwithClassMethodPrefix=foo`
- `javac ... -AfileComment=foo`
- `javac ... -AfileIndent=foo`
- `javac ... -AprefixEnclosingClassNames=foo`
RecordBuilder can be customized to your needs and you can even create your
own custom RecordBuilder annotations. See [Customizing RecordBuilder](customizing.md)
for details.
## Java 15 Versions
Artifacts compiled wth Java 15 are available. They are the same versions
as the Java 16 versions with `-java15` appended.
Artifacts compiled wth Java 15 are available. These versions have `-java15` appended.
Note: records are a preview feature only in Java 15. You'll need take a number of steps in order to try RecordBuilder:

85
customizing.md Normal file
View File

@@ -0,0 +1,85 @@
[◀︎ RecordBuilder](README.md) • Customizing RecordBuilder
# Customizing RecordBuilder
RecordBuilder can be customized in a number of ways. The types of customizations will change over time. See
[@RecordBuilder.Options](record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java)
for the current set of customizations and their default values.
You can:
- [Customize an entire build](#customize-an-entire-build) - all uses of `@RecordBuilder` in your project
- [Customize a single record](#customize-a-single-record) annotated with `@RecordBuilder`
- [Create a custom annotation](#create-a-custom-annotation) that specifies your options and use that instead of `@RecordBuilder`
## Customize an entire build
To customize an entire build, use javac's annotation processor options via `-A` on the command line.
The options available are the same as the attributes in [@RecordBuilder.Options](record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java).
i.e. to disable "prefixing enclosing class names", compile with:
```shell
javac -AprefixEnclosingClassNames=false ...
```
_Note: use a separate `-A` for each option._
#### Maven
If you are using Maven, specify the options in the compiler plugin:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin-version}</version>
<configuration>
<compilerArgs>
<arg>-AprefixEnclosingClassNames=false</arg>
<arg>-AfileComment="something different"</arg>
</compilerArgs>
</configuration>
</plugin>
```
#### Gradle
For Gradle, specify the options:
```groovy
compilerArgs.addAll(['-AprefixEnclosingClassNames=false', '-AfileComment="something different"'])
```
## Customize a single record
To customize a single record, add `@RecordBuilder.Options` in addition to
`@RecordBuilder`.
E.g.
```java
@RecordBuilder.Options(withClassName = "Wither")
@RecordBuilder
public record MyRecord(String s){}
```
## Create a custom annotation
Using `@RecordBuilder.Template` you can create your own RecordBuilder annotation
that uses the set of options you want. E.g. to create a custom annotation that
uses an alternate file comment and an alternate With classname:
```java
@RecordBuilder.Template(options = @RecordBuilder.Options(
fileComment = "MyCo license",
withClassName = "Wither"
))
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface MyCoRecordBuilder {
}
```
Now, you can use `@MyCoRecordBuilder` instead of `@RecordBuilder` and the record
will be built with options as specified.

View File

@@ -16,15 +16,18 @@
package io.soabase.recordbuilder.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface RecordBuilder {
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@Inherited
@interface Include {
Class<?>[] value();
@@ -38,4 +41,79 @@ public @interface RecordBuilder {
*/
String packagePattern() default "@";
}
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
@interface Options {
/**
* The builder class name will be the name of the record (prefixed with any enclosing class) plus this suffix. E.g.
* if the record name is "Foo", the builder will be named "FooBuilder".
*/
String suffix() default "Builder";
/**
* Used by {@code RecordInterface}. The generated record will have the same name as the annotated interface
* plus this suffix. E.g. if the interface name is "Foo", the record will be named "FooRecord".
*/
String interfaceSuffix() default "Record";
/**
* The name to use for the copy builder
*/
String copyMethodName() default "builder";
/**
* The name to use for the builder
*/
String builderMethodName() default "builder";
/**
* The name to use for the build method
*/
String buildMethodName() default "build";
/**
* The name to use for the downcast method
*/
String downCastMethodName() default "_downcast";
/**
* The name to use for the method that returns the record components as a stream
*/
String componentsMethodName() default "stream";
/**
* The name to use for the nested With class
*/
String withClassName() default "With";
/**
* The prefix to use for the methods in the With class
*/
String withClassMethodPrefix() default "with";
/**
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
*/
String fileComment() default "Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder";
/**
* Return the file indent to use
*/
String fileIndent() default " ";
/**
* If the record is declared inside of another class, the outer class's name will
* be prefixed to the builder name if this returns true.
*/
boolean prefixEnclosingClassNames() default true;
}
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.ANNOTATION_TYPE)
@Inherited
@interface Template {
RecordBuilder.Options options();
}
}

View File

@@ -1,144 +0,0 @@
/**
* 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.core;
public interface RecordBuilderMetaData {
/**
* If you want to use your own meta data instance:
* <ul>
* <li>create a class that implements {@code RecordBuilderMetaData}</li>
* <li>When compiling, make sure that compiled class is in the processor path</li>
* <li>Add a "metaDataClass" compiler option with the class name. E.g. {@code javac ... -AmetaDataClass=foo.bar.MyMetaData}</li>
* </ul>
*/
String JAVAC_OPTION_NAME = "metaDataClass";
/**
* The default meta data instance
*/
RecordBuilderMetaData DEFAULT = new RecordBuilderMetaData() {};
/**
* The builder class name will be the name of the record (prefixed with any enclosing class) plus this suffix. E.g.
* if the record name is "Foo", the builder will be named "FooBuilder".
*
* @return suffix
*/
default String suffix() {
return "Builder";
}
/**
* Used by {@code RecordInterface}. The generated record will have the same name as the annotated interface
* plus this suffix. E.g. if the interface name is "Foo", the record will be named "FooRecord".
*
* @return suffix
*/
default String interfaceSuffix() {
return "Record";
}
/**
* The name to use for the copy builder
*
* @return copy builder name
*/
default String copyMethodName() {
return builderMethodName();
}
/**
* The name to use for the builder
*
* @return builder name
*/
default String builderMethodName() {
return "builder";
}
/**
* The name to use for the build method
*
* @return build method
*/
default String buildMethodName() {
return "build";
}
/**
* The name to use for the downcast method
*
* @return downcast method
*/
default String downCastMethodName() {
return "_downcast";
}
/**
* The name to use for the method that returns the record components as a stream
*
* @return build method
*/
default String componentsMethodName() {
return "stream";
}
/**
* The name to use for the nested With class
*
* @return with class name
*/
default String withClassName() {
return "With";
}
/**
* The prefix to use for the methods in the With class
*
* @return prefix
*/
default String withClassMethodPrefix() {
return "with";
}
/**
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
*
* @return comment or empty
*/
default String fileComment() {
return "Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder";
}
/**
* Return the file indent to use
*
* @return file index
*/
default String fileIndent() {
return " ";
}
/**
* If the record is declared inside of another class, the outer class's name will
* be prefixed to the builder name if this returns true.
*
* @return true/false
*/
default boolean prefixEnclosingClassNames() {
return true;
}
}

View File

@@ -16,17 +16,20 @@
package io.soabase.recordbuilder.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface RecordInterface {
boolean addRecordBuilder() default true;
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
@Inherited
@interface Include {
Class<?>[] value();

View File

@@ -19,7 +19,7 @@ import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
@@ -119,7 +119,7 @@ public class ElementUtils {
return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
public static String getBuilderName(TypeElement element, RecordBuilderMetaData metaData, ClassType classType, String suffix) {
public static String getBuilderName(TypeElement element, RecordBuilder.Options metaData, ClassType classType, String suffix) {
// generate the class name
var baseName = classType.name() + suffix;
return metaData.prefixEnclosingClassNames() ? (getBuilderNamePrefix(element.getEnclosingElement()) + baseName) : baseName;

View File

@@ -24,11 +24,9 @@ import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
import io.soabase.recordbuilder.core.RecordBuilder;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
@@ -45,7 +43,7 @@ import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generate
class InternalRecordBuilderProcessor
{
private final RecordBuilderMetaData metaData;
private final RecordBuilder.Options metaData;
private final ClassType recordClassType;
private final String packageName;
private final ClassType builderClassType;
@@ -55,9 +53,9 @@ class InternalRecordBuilderProcessor
private final TypeSpec.Builder builder;
private final String uniqueVarName;
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData, Optional<String> packageNameOpt)
InternalRecordBuilderProcessor(TypeElement record, RecordBuilder.Options metaData, Optional<String> packageNameOpt)
{
this.metaData = metaData;
this.metaData = getMetaData(record, metaData);
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(record));
builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters());
@@ -106,6 +104,11 @@ class InternalRecordBuilderProcessor
return builderType;
}
private RecordBuilder.Options getMetaData(TypeElement record, RecordBuilder.Options metaData) {
var recordSpecificMetaData = record.getAnnotation(RecordBuilder.Options.class);
return (recordSpecificMetaData != null) ?recordSpecificMetaData : metaData;
}
private void addWithNestedClass()
{
/*

View File

@@ -22,7 +22,6 @@ 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 javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
@@ -59,7 +58,7 @@ class InternalRecordInterfaceProcessor {
private record Component(ExecutableElement element, Optional<String> alternateName){}
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) {
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
this.processingEnv = processingEnv;
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
recordComponents = getRecordComponents(iface);

View File

@@ -1,175 +0,0 @@
/**
* 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 io.soabase.recordbuilder.core.RecordBuilderMetaData;
import java.util.Map;
public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
/**
* @see #suffix()
*/
public static final String OPTION_SUFFIX = "suffix";
/**
* @see #interfaceSuffix()
*/
public static final String OPTION_INTERFACE_SUFFIX = "interfaceSuffix";
/**
* @see #copyMethodName()
*/
public static final String OPTION_COPY_METHOD_NAME = "copyMethodName";
/**
* @see #builderMethodName()
*/
public static final String OPTION_BUILDER_METHOD_NAME = "builderMethodName";
/**
* @see #buildMethodName()
*/
public static final String OPTION_BUILD_METHOD_NAME = "buildMethodName";
/**
* @see #downCastMethodName()
*/
public static final String OPTION_DOWN_CAST_METHOD_NAME = "downCastMethodName";
/**
* @see #componentsMethodName()
*/
public static final String OPTION_COMPONENTS_METHOD_NAME = "componentsMethodName";
/**
* @see #fileComment()
*/
public static final String OPTION_FILE_COMMENT = "fileComment";
/**
* @see #fileIndent()
*/
public static final String OPTION_FILE_INDENT = "fileIndent";
/**
* @see #prefixEnclosingClassNames()
*/
public static final String OPTION_PREFIX_ENCLOSING_CLASS_NAMES = "prefixEnclosingClassNames";
/**
* @see #withClassName()
*/
public static final String OPTION_WITH_CLASS_NAME = "withClassName";
/**
* @see #withClassMethodPrefix()
*/
public static final String OPTION_WITH_CLASS_METHOD_PREFIX = "withClassMethodPrefix";
private final String suffix;
private final String interfaceSuffix;
private final String copyMethodName;
private final String builderMethodName;
private final String buildMethodName;
private final String downCastMethodName;
private final String componentsMethodName;
private final String withClassName;
private final String withClassMethodPrefix;
private final String fileComment;
private final String fileIndent;
private final boolean prefixEnclosingClassNames;
public OptionBasedRecordBuilderMetaData(Map<String, String> options) {
suffix = options.getOrDefault(OPTION_SUFFIX, DEFAULT.suffix());
interfaceSuffix = options.getOrDefault(OPTION_INTERFACE_SUFFIX, DEFAULT.interfaceSuffix());
builderMethodName = options.getOrDefault(OPTION_BUILDER_METHOD_NAME, DEFAULT.builderMethodName());
copyMethodName = options.getOrDefault(OPTION_COPY_METHOD_NAME, DEFAULT.copyMethodName());
buildMethodName = options.getOrDefault(OPTION_BUILD_METHOD_NAME, DEFAULT.buildMethodName());
downCastMethodName = options.getOrDefault(OPTION_DOWN_CAST_METHOD_NAME, DEFAULT.downCastMethodName());
componentsMethodName = options.getOrDefault(OPTION_COMPONENTS_METHOD_NAME, DEFAULT.componentsMethodName());
withClassName = options.getOrDefault(OPTION_WITH_CLASS_NAME, DEFAULT.withClassName());
withClassMethodPrefix = options.getOrDefault(OPTION_WITH_CLASS_METHOD_PREFIX, DEFAULT.withClassMethodPrefix());
fileComment = options.getOrDefault(OPTION_FILE_COMMENT, DEFAULT.fileComment());
fileIndent = options.getOrDefault(OPTION_FILE_INDENT, DEFAULT.fileIndent());
String prefixenclosingclassnamesopt = options.getOrDefault(OPTION_PREFIX_ENCLOSING_CLASS_NAMES, String.valueOf(DEFAULT.prefixEnclosingClassNames()));
if (prefixenclosingclassnamesopt == null) {
prefixEnclosingClassNames = true;
} else {
prefixEnclosingClassNames = Boolean.parseBoolean(prefixenclosingclassnamesopt);
}
}
@Override
public String suffix() {
return suffix;
}
@Override
public String copyMethodName() {
return copyMethodName;
}
@Override
public String builderMethodName() {
return builderMethodName;
}
@Override
public String buildMethodName() {
return buildMethodName;
}
@Override
public String downCastMethodName() {
return downCastMethodName;
}
@Override
public String componentsMethodName() {
return componentsMethodName;
}
@Override
public String withClassName() {
return withClassName;
}
@Override
public String withClassMethodPrefix() {
return withClassMethodPrefix;
}
@Override
public String fileComment() {
return fileComment;
}
@Override
public String fileIndent() {
return fileIndent;
}
@Override
public boolean prefixEnclosingClassNames() {
return prefixEnclosingClassNames;
}
@Override
public String interfaceSuffix() {
return interfaceSuffix;
}
}

View File

@@ -1,53 +0,0 @@
/**
* 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 io.soabase.recordbuilder.core.RecordBuilderMetaData;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.function.Consumer;
import javax.annotation.processing.ProcessingEnvironment;
class RecordBuilderMetaDataLoader {
private final RecordBuilderMetaData metaData;
RecordBuilderMetaDataLoader(ProcessingEnvironment processingEnv, Consumer<String> logger) {
Map<String, String> options = processingEnv.getOptions();
String metaDataClassName = options.get(RecordBuilderMetaData.JAVAC_OPTION_NAME);
if ((metaDataClassName != null) && !metaDataClassName.isEmpty()) {
RecordBuilderMetaData loadedMetaData = null;
try {
Class<?> clazz = Class.forName(metaDataClassName);
loadedMetaData = (RecordBuilderMetaData) clazz.getDeclaredConstructor().newInstance();
logger.accept("Found meta data: " + clazz);
} catch (InvocationTargetException e) {
// log the thrown exception instead of the invocation target exception
logger.accept("Could not load meta data: " + metaDataClassName + " - " + e.getCause());
} catch (Exception e) {
logger.accept("Could not load meta data: " + metaDataClassName + " - " + e);
}
metaData = (loadedMetaData != null) ? loadedMetaData : RecordBuilderMetaData.DEFAULT;
} else {
metaData = new OptionBasedRecordBuilderMetaData(options);
}
}
RecordBuilderMetaData getMetaData() {
return metaData;
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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 io.soabase.recordbuilder.core.RecordBuilder;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
class RecordBuilderOptions {
private static final Map<String, Object> defaultValues = buildDefaultValues();
static RecordBuilder.Options build(Map<String, String> options) {
return (RecordBuilder.Options)Proxy.newProxyInstance(RecordBuilderOptions.class.getClassLoader(), new Class[]{RecordBuilder.Options.class}, (proxy, method, args) -> {
var name = method.getName();
var defaultValue = defaultValues.get(name);
var option = options.get(name);
if (option != null) {
if (defaultValue instanceof String) {
return option;
}
if (defaultValue instanceof Boolean) {
return Boolean.parseBoolean(option);
}
if (defaultValue instanceof Integer) {
return Integer.parseInt(option);
}
if (defaultValue instanceof Long) {
return Long.parseLong(option);
}
if (defaultValue instanceof Double) {
return Double.parseDouble(option);
}
throw new IllegalArgumentException("Unhandled option type: " + defaultValue.getClass());
}
return defaultValue;
});
}
private static Map<String, Object> buildDefaultValues() {
var workMap = new HashMap<String, Object>();
for ( Method method : RecordBuilder.Options.class.getDeclaredMethods()) {
workMap.put(method.getName(), method.getDefaultValue());
}
workMap.put("toString", "Generated RecordBuilder.Options");
return Map.copyOf(workMap);
}
private RecordBuilderOptions() {
}
}

View File

@@ -19,8 +19,8 @@ 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.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Generated;
@@ -32,13 +32,16 @@ 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;
public class RecordBuilderProcessor extends AbstractProcessor {
public class RecordBuilderProcessor
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();
@@ -48,18 +51,21 @@ public class RecordBuilderProcessor extends AbstractProcessor {
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 true;
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(RECORD_BUILDER, RECORD_BUILDER_INCLUDE, RECORD_INTERFACE, RECORD_INTERFACE_INCLUDE);
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()
@@ -67,66 +73,57 @@ public class RecordBuilderProcessor extends AbstractProcessor {
return SourceVersion.latest();
}
private void process(TypeElement annotation, Element element) {
var metaData = new RecordBuilderMetaDataLoader(processingEnv, s -> processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
private void process(TypeElement annotation, Element element)
{
String annotationClass = annotation.getQualifiedName().toString();
if ( annotationClass.equals(RECORD_BUILDER) )
{
processRecordBuilder((TypeElement)element, metaData, Optional.empty());
if (annotationClass.equals(RECORD_BUILDER)) {
var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
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_INTERFACE)) {
var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
processRecordInterface((TypeElement) element, element.getAnnotation(RecordInterface.class).addRecordBuilder(), metaData, Optional.empty());
}
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
{
throw new RuntimeException("Unknown annotation: " + annotation);
} else {
var recordBuilderTemplate = annotation.getAnnotation(RecordBuilder.Template.class);
if (recordBuilderTemplate != null) {
processRecordBuilder((TypeElement) element, recordBuilderTemplate.options(), Optional.empty());
}
}
}
private void processIncludes(Element element, RecordBuilderMetaData metaData, String annotationClass) {
private void processIncludes(Element element, RecordBuilder.Options metaData, String annotationClass)
{
var annotationMirrorOpt = ElementUtils.findAnnotationMirror(processingEnv, element, annotationClass);
if ( annotationMirrorOpt.isEmpty() )
{
if (annotationMirrorOpt.isEmpty()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation mirror for: " + annotationClass, element);
}
else
{
else {
var values = processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirrorOpt.get());
var classes = ElementUtils.getAnnotationValue(values, "value");
if ( classes.isEmpty() )
{
if (classes.isEmpty()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not get annotation value for: " + annotationClass, element);
}
else
{
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 )
{
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
{
else {
var packageName = buildPackageName(packagePattern, element, typeElement);
if (packageName != null)
{
if ( annotationClass.equals(RECORD_INTERFACE_INCLUDE) )
{
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));
}
else
{
else {
processRecordBuilder(typeElement, metaData, Optional.of(packageName));
}
}
@@ -136,50 +133,51 @@ public class RecordBuilderProcessor extends AbstractProcessor {
}
}
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;
}
String replaced = packagePattern.replace("*", includedClassPackage.getQualifiedName().toString());
if (builderElement instanceof PackageElement) {
return replaced.replace("@", ((PackageElement)builderElement).getQualifiedName().toString());
return replaced.replace("@", ((PackageElement) builderElement).getQualifiedName().toString());
}
return replaced.replace("@", ((PackageElement)builderElement.getEnclosingElement()).getQualifiedName().toString());
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;
}
if (includedClass.getEnclosingElement() instanceof PackageElement) {
return (PackageElement)includedClass.getEnclosingElement();
return (PackageElement) includedClass.getEnclosingElement();
}
return findPackageElement(actualElement, includedClass.getEnclosingElement());
}
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageName) {
if ( !element.getKind().isInterface() )
{
private void processRecordInterface(TypeElement element, boolean addRecordBuilder, RecordBuilder.Options 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, addRecordBuilder, metaData, packageName);
if ( !internalProcessor.isValid() )
{
if (!internalProcessor.isValid()) {
return;
}
writeRecordInterfaceJavaFile(element, internalProcessor.packageName(), internalProcessor.recordClassType(), internalProcessor.recordType(), metaData, internalProcessor::toRecord);
}
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData 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
// 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;
}
@@ -187,26 +185,25 @@ public class RecordBuilderProcessor extends AbstractProcessor {
writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
}
private void writeRecordBuilderJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilderMetaData 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();
try
{
try {
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);
}
}
catch ( IOException e )
{
catch (IOException e) {
handleWriteError(record, e);
}
}
private void writeRecordInterfaceJavaFile(TypeElement element, String packageName, ClassType classType, TypeSpec type, RecordBuilderMetaData 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();
@@ -214,35 +211,32 @@ public class RecordBuilderProcessor extends AbstractProcessor {
String recordSourceCode = toRecordProc.apply(classSourceCode);
Filer filer = processingEnv.getFiler();
try
{
try {
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);
}
}
catch ( IOException e )
{
catch (IOException e) {
handleWriteError(element, e);
}
}
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilderMetaData 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() )
{
if ((comment != null) && !comment.isEmpty()) {
javaFileBuilder.addFileComment(comment);
}
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 )
{
if (e.getMessage() != null) {
message = message + ": " + e.getMessage();
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);

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 io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder.Template(options = @RecordBuilder.Options(
fileComment = "This is a test",
withClassName = "Com"
))
public @interface MyTemplate
{
}

View File

@@ -18,5 +18,6 @@ package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder
@RecordBuilder.Options(prefixEnclosingClassNames = false)
public record SimpleRecord(int i, String s) {
}

View File

@@ -0,0 +1,23 @@
/**
* 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;
@MyTemplate
public record TemplateTest(String text, Instant date) implements TemplateTestBuilder.Com
{
}