diff --git a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java index ce56f916..232e5929 100644 --- a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java +++ b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java @@ -335,4 +335,23 @@ enum BuilderMode { STANDARD, STAGED, STANDARD_AND_STAGED, } + + /** + * Apply to record components to specify a field initializer for the generated builder + */ + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.FIELD) + @Inherited + @interface Initializer { + /** + * The name of a public static method or a public static final field in the source to use as the initializer + */ + String value(); + + /** + * The source class that contains the method/field specified by {@code value()}. If not specified, the target + * record is the source class. + */ + Class source() default Object.class; + } } diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/ElementUtils.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/ElementUtils.java index 535cba29..cb6fcec9 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/ElementUtils.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/ElementUtils.java @@ -20,15 +20,9 @@ import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeVariableName; import io.soabase.recordbuilder.core.RecordBuilder; + 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.ElementKind; -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.element.*; import javax.lang.model.type.TypeMirror; import java.util.Collections; import java.util.List; @@ -56,6 +50,13 @@ public static List getAttributeTypeMirrorList(AnnotationValue attrib return values.stream().map(v -> (TypeMirror) v.getValue()).collect(Collectors.toList()); } + public static Optional getAttributeTypeMirror(AnnotationValue attribute) { + if (attribute == null) { + return Optional.empty(); + } + return Optional.ofNullable((TypeMirror) attribute.getValue()); + } + @SuppressWarnings("unchecked") public static List getAttributeStringList(AnnotationValue attribute) { List values = (attribute != null) diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InitializerUtil.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InitializerUtil.java new file mode 100644 index 00000000..0af13a74 --- /dev/null +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InitializerUtil.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019 The original author or authors + * + * 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 com.squareup.javapoet.CodeBlock; +import io.soabase.recordbuilder.core.RecordBuilder; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.*; +import javax.tools.Diagnostic; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.soabase.recordbuilder.processor.ElementUtils.getAnnotationValue; +import static io.soabase.recordbuilder.processor.ElementUtils.getStringAttribute; + +class InitializerUtil { + static Map detectInitializers(ProcessingEnvironment processingEnv, TypeElement record) { + return record.getEnclosedElements().stream().flatMap(element -> { + var annotation = ElementUtils.findAnnotationMirror(processingEnv, element, + RecordBuilder.Initializer.class.getName().replace("$", ".")); + var value = annotation.flatMap(mirror -> getAnnotationValue(mirror.getElementValues(), "value")); + if (value.isEmpty()) { + return Stream.of(); + } + + var source = annotation.flatMap(mirror -> getAnnotationValue(mirror.getElementValues(), "source")); + + var name = getStringAttribute(value.get(), ""); + var sourceElement = findSourceElement(processingEnv, record, source); + + Optional initializer = sourceElement.getEnclosedElements().stream() + .filter(enclosedElement -> enclosedElement.getSimpleName().toString().equals(name)) + .flatMap(enclosedElement -> { + if ((enclosedElement.getKind() == ElementKind.METHOD) + && isValid(processingEnv, element, (ExecutableElement) enclosedElement)) { + return Stream.of(CodeBlock.builder().add("$T.$L()", sourceElement, name).build()); + } + + if ((enclosedElement.getKind() == ElementKind.FIELD) + && isValid(processingEnv, element, (VariableElement) enclosedElement)) { + return Stream.of(CodeBlock.builder().add("$T.$L", sourceElement, name).build()); + } + + return Stream.of(); + }).findFirst(); + + if (initializer.isEmpty()) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, + "No matching public static field or method found for initializer named: " + name, element); + } + + return initializer.map(codeBlock -> Map.entry(element.getSimpleName().toString(), codeBlock)).stream(); + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static boolean isValid(ProcessingEnvironment processingEnv, Element element, + ExecutableElement executableElement) { + if (executableElement.getModifiers().contains(Modifier.PUBLIC) + && executableElement.getModifiers().contains(Modifier.STATIC)) { + return processingEnv.getTypeUtils().isSameType(executableElement.getReturnType(), element.asType()); + } + return false; + } + + private static boolean isValid(ProcessingEnvironment processingEnv, Element element, + VariableElement variableElement) { + if (variableElement.getModifiers().contains(Modifier.PUBLIC) + && variableElement.getModifiers().contains(Modifier.STATIC) + && variableElement.getModifiers().contains(Modifier.FINAL)) { + return processingEnv.getTypeUtils().isSameType(variableElement.asType(), element.asType()); + } + return false; + } + + private static TypeElement findSourceElement(ProcessingEnvironment processingEnv, TypeElement record, + Optional source) { + return source.flatMap(ElementUtils::getAttributeTypeMirror) + .map(sourceMirror -> (TypeElement) processingEnv.getTypeUtils().asElement(sourceMirror)).orElse(record); + } +} diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java index ca47feab..320cc71d 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java @@ -57,6 +57,7 @@ class InternalRecordBuilderProcessor { private static final TypeVariableName rType = TypeVariableName.get("R"); private final ProcessingEnvironment processingEnv; private final Modifier constructorVisibilityModifier; + private final Map initializers; InternalRecordBuilderProcessor(ProcessingEnvironment processingEnv, TypeElement record, RecordBuilder.Options metaData, Optional packageNameOpt) { @@ -73,6 +74,7 @@ class InternalRecordBuilderProcessor { notNullPattern = Pattern.compile(metaData.interpretNotNullsPattern()); collectionBuilderUtils = new CollectionBuilderUtils(recordComponents, this.metaData); constructorVisibilityModifier = metaData.publicBuilderConstructors() ? Modifier.PUBLIC : Modifier.PRIVATE; + initializers = InitializerUtil.detectInitializers(processingEnv, record); builder = TypeSpec.classBuilder(builderClassType.name()).addAnnotation(generatedRecordBuilderAnnotation) .addModifiers(metaData.builderClassModifiers()).addTypeVariables(typeVariables); @@ -760,13 +762,18 @@ private void add1Field(ClassType component) { * private T p; */ var fieldSpecBuilder = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE); - if (metaData.emptyDefaultForOptional()) { + + CodeBlock initializer = initializers.get(component.name()); + if (initializer != null) { + fieldSpecBuilder.initializer(initializer); + } else if (metaData.emptyDefaultForOptional()) { Optional thisOptionalType = OptionalType.fromClassType(component); if (thisOptionalType.isPresent()) { var codeBlock = CodeBlock.builder().add("$T.empty()", thisOptionalType.get().typeName()).build(); fieldSpecBuilder.initializer(codeBlock); } } + builder.addField(fieldSpecBuilder.build()); } diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/Initialized.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/Initialized.java new file mode 100644 index 00000000..df7a5778 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/Initialized.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 The original author or authors + * + * 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; +import io.soabase.recordbuilder.core.RecordBuilderFull; + +import java.util.Optional; + +@RecordBuilderFull +public record Initialized(@RecordBuilder.Initializer("DEFAULT_NAME") String name, + @RecordBuilder.Initializer("defaultAge") int age, Optional dummy, + @RecordBuilder.Initializer(value = "defaultAltName", source = AltSource.class) Optional altName) { + + public static final String DEFAULT_NAME = "hey"; + + public static int defaultAge() { + return 18; + } + + public static class AltSource { + public static Optional defaultAltName() { + return Optional.of("alt"); + } + } +} diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestInitialized.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestInitialized.java new file mode 100644 index 00000000..eb77f502 --- /dev/null +++ b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestInitialized.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 The original author or authors + * + * 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 org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +class TestInitialized { + @Test + void testInitialized() { + var initialized = InitializedBuilder.builder().build(); + Assertions.assertEquals(Initialized.DEFAULT_NAME, initialized.name()); + Assertions.assertEquals(Initialized.defaultAge(), initialized.age()); + Assertions.assertEquals(Initialized.AltSource.defaultAltName(), initialized.altName()); + Assertions.assertEquals(Optional.empty(), initialized.dummy()); + } +}