
com.bettercloud.bigtable.orm.process.EntitySourceGenerator Maven / Gradle / Ivy
package com.bettercloud.bigtable.orm.process;
import com.bettercloud.bigtable.orm.EntityConfiguration;
import com.bettercloud.bigtable.orm.Key;
import com.bettercloud.bigtable.orm.KeyBuilder;
import com.bettercloud.bigtable.orm.RegisterableEntity;
import com.bettercloud.bigtable.orm.StringKey;
import com.bettercloud.bigtable.orm.annotations.Column;
import com.bettercloud.bigtable.orm.annotations.Entity;
import com.bettercloud.bigtable.orm.annotations.KeyComponent;
import com.bettercloud.bigtable.orm.annotations.Table;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.CaseFormat;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.WildcardTypeName;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import java.lang.annotation.Annotation;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
class EntitySourceGenerator {
private static final String INDENT = " ";
private final Elements elementUtils;
EntitySourceGenerator(final Elements elementUtils) {
this.elementUtils = elementUtils;
}
JavaFile generateForElement(final T entityElement) throws ElementProcessingException {
final Element tableElement = Optional.ofNullable(entityElement.getEnclosingElement())
.filter(element -> element.getKind().isClass())
.orElseThrow(() -> new ElementProcessingException("@Entity types must be nested classes", entityElement));
final Table table = Optional.ofNullable(tableElement.getAnnotation(Table.class))
.orElseThrow(() -> new ElementProcessingException("@Entity types must be nested classes of a parent annotated with @Table", entityElement));
if (tableElement.getModifiers().contains(Modifier.PUBLIC)) {
throw new ElementProcessingException("@Table types must not be public", tableElement);
}
final String tableName = Optional.of(table.value())
.filter(value -> !value.isEmpty())
.orElseThrow(() -> new ElementProcessingException("@Table value must be defined", tableElement));
if (!entityElement.getModifiers().contains(Modifier.PRIVATE)) {
throw new ElementProcessingException("@Entity types must be private", entityElement);
}
if (entityElement.getModifiers().contains(Modifier.STATIC)) {
throw new ElementProcessingException("@Entity types must not be static", entityElement);
}
final String packageName = Optional.of(elementUtils.getPackageOf(entityElement))
.map(PackageElement::getQualifiedName)
.map(Name::toString)
.orElseThrow(IllegalStateException::new);
final String entityName = Optional.of(entityElement.getSimpleName())
.map(Name::toString)
.orElseThrow(IllegalStateException::new);
final ClassName entityClassName = ClassName.get(packageName, entityName);
final TypeSpec.Builder entityBuilder = TypeSpec.classBuilder(entityClassName)
.addModifiers(Modifier.PUBLIC)
.superclass(RegisterableEntity.class);
final MethodSpec.Builder equalsBuilder = MethodSpec.methodBuilder("equals")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.BOOLEAN)
.addParameter(Object.class, "o", Modifier.FINAL)
.beginControlFlow("if (this == $N)", "o")
.addStatement("return true")
.endControlFlow()
.beginControlFlow("if ($N == null || getClass() != $N.getClass())", "o", "o")
.addStatement("return false")
.endControlFlow()
.addStatement("final $T that = ($T) $N", entityClassName, entityClassName, "o");
final CodeBlock.Builder equalsReturnBuilder = CodeBlock.builder()
.add("return ");
final MethodSpec.Builder hashCodeBuilder = MethodSpec.methodBuilder("hashCode")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.INT);
final List arrayFields = new ArrayList<>();
final List objectFields = new ArrayList<>();
final MethodSpec.Builder toStringBuilder = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(String.class);
final CodeBlock.Builder toStringReturnBuilder = CodeBlock.builder()
.add("return \"$N{\"", entityName);
final ClassName columnsClassName = entityClassName.nestedClass("Columns");
final TypeSpec.Builder columnsBuilder = TypeSpec.enumBuilder(columnsClassName)
.addModifiers(Modifier.PRIVATE)
.addSuperinterface(com.bettercloud.bigtable.orm.Column.class);
final ClassName configurationClassName = entityClassName.nestedClass("Configuration");
final TypeSpec.Builder entityConfigurationBuilder = TypeSpec.classBuilder(configurationClassName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addSuperinterface(ParameterizedTypeName.get(ClassName.get(EntityConfiguration.class), entityClassName))
.addField(FieldSpec.builder(ParameterizedTypeName.get(ClassName.get(EntityConfiguration.class), entityClassName),
"INSTANCE", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("new $T()", configurationClassName)
.build())
.addField(FieldSpec.builder(String.class, "TABLE_NAME", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$S", tableName)
.build())
.addField(FieldSpec.builder(ParameterizedTypeName.get(List.class, com.bettercloud.bigtable.orm.Column.class),
"COLUMNS", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$T.asList($T.values())", Arrays.class, columnsClassName)
.build())
.addField(FieldSpec.builder(ParameterizedTypeName.get(ClassName.get(Supplier.class), entityClassName),
"FACTORY", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$T::new", entityClassName)
.build())
.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.addComment("Prevent external instantiation")
.build())
.addMethod(MethodSpec.methodBuilder("getDefaultTableName")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(String.class)
.addStatement("return $N", "TABLE_NAME")
.build())
.addMethod(MethodSpec.methodBuilder("getColumns")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(Iterable.class, com.bettercloud.bigtable.orm.Column.class))
.addStatement("return $N", "COLUMNS")
.build())
.addMethod(MethodSpec.methodBuilder("getEntityFactory")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(ClassName.get(Supplier.class), entityClassName))
.addStatement("return $N", "FACTORY")
.build());
final ClassName delegateClassName = entityClassName.nestedClass("Delegate");
final TypeSpec.Builder entityDelegateBuilder = TypeSpec.classBuilder(delegateClassName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addSuperinterface(ParameterizedTypeName.get(ClassName.get(EntityConfiguration.EntityDelegate.class), entityClassName))
.addField(entityClassName, "entity", Modifier.PRIVATE, Modifier.FINAL)
.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.addParameter(entityClassName, "entity", Modifier.FINAL)
.addStatement("this.$N = $N", "entity", "entity")
.build());
final MethodSpec.Builder getColumnValueBuilder = MethodSpec.methodBuilder("getColumnValue")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(Object.class)
.addParameter(com.bettercloud.bigtable.orm.Column.class, "column", Modifier.FINAL)
.addStatement("$T.requireNonNull(column)", Objects.class);
final MethodSpec.Builder setColumnValueBuilder = MethodSpec.methodBuilder("setColumnValue")
.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "unchecked")
.build())
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(com.bettercloud.bigtable.orm.Column.class, "column", Modifier.FINAL)
.addParameter(Object.class, "value", Modifier.FINAL);
final MethodSpec.Builder getColumnTimestampBuilder = MethodSpec.methodBuilder("getColumnTimestamp")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(Long.class)
.addParameter(com.bettercloud.bigtable.orm.Column.class, "column", Modifier.FINAL)
.addStatement("$T.requireNonNull(column)", Objects.class);
final MethodSpec.Builder setColumnTimestampBuilder = MethodSpec.methodBuilder("setColumnTimestamp")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(com.bettercloud.bigtable.orm.Column.class, "column", Modifier.FINAL)
.addParameter(Long.class, "timestamp", Modifier.FINAL);
final Map columnElements = Optional.ofNullable(entityElement.getEnclosedElements())
.orElseGet(Collections::emptyList)
.stream()
.filter(enclosedElement -> enclosedElement.getAnnotation(Column.class) != null)
.collect(Collectors.toMap(
Function.identity(),
enclosedElement -> enclosedElement.getAnnotation(Column.class),
(u, v) -> {
// Impossible
throw new IllegalStateException("Duplicate key: " + u);
},
LinkedHashMap::new // Preserves order of declared columns
));
if (columnElements.isEmpty()) {
throw new ElementProcessingException("@Entity types must declare at least one @Column field", entityElement);
}
final AtomicBoolean startedControlFlow = new AtomicBoolean(false);
final AtomicBoolean startedTimestampControlFlow = new AtomicBoolean(false);
final Set columnHashes = new HashSet<>();
for (final Map.Entry entry : columnElements.entrySet()) {
final Element columnElement = entry.getKey();
final Column column = entry.getValue();
if (columnElement.getModifiers().contains(Modifier.STATIC)) {
throw new ElementProcessingException("Static fields are not supported for @Column definitions", columnElement);
}
if (columnElement.getModifiers().contains(Modifier.FINAL)) {
throw new ElementProcessingException("Final fields are not supported for @Column definitions", columnElement);
}
final String lowerCamelCase = columnElement.getSimpleName().toString();
final String upperCamelCase = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, lowerCamelCase);
final String upperCase = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, lowerCamelCase);
final TypeMirror typeMirror = columnElement.asType();
final TypeKind kind = typeMirror.getKind();
final TypeName typeName = TypeName.get(typeMirror).box();
entityBuilder.addField(typeName, lowerCamelCase, Modifier.PRIVATE);
final String getter = "get" + upperCamelCase;
final String setter = "set" + upperCamelCase;
entityBuilder.addMethod(
MethodSpec.methodBuilder(getter)
.addModifiers(Modifier.PUBLIC)
.returns(typeName)
.addStatement("return $N", lowerCamelCase)
.build()
);
final MethodSpec.Builder setterBuilder = MethodSpec.methodBuilder(setter)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(typeName, lowerCamelCase, Modifier.FINAL)
.addStatement("this.$N = $N", lowerCamelCase, lowerCamelCase);
if (column.versioned()) {
final String timestampField = lowerCamelCase + "Timestamp";
entityBuilder.addField(Long.class, timestampField, Modifier.PRIVATE);
final String timestampGetter = "get" + upperCamelCase + "Timestamp";
final String timestampSetter = "set" + upperCamelCase + "Timestamp";
entityBuilder.addMethod(
MethodSpec.methodBuilder(timestampGetter)
.addModifiers(Modifier.PUBLIC)
.returns(Long.class)
.addStatement("return $N", timestampField)
.build()
);
setterBuilder.addStatement("this.$N = null", timestampField);
entityBuilder.addMethod(
MethodSpec.methodBuilder(setter)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(typeName, lowerCamelCase, Modifier.FINAL)
.addParameter(TypeName.LONG, "timestamp", Modifier.FINAL)
.addStatement("this.$N = $N", lowerCamelCase, lowerCamelCase)
.addStatement("this.$N = $N", timestampField, "timestamp")
.build()
);
entityBuilder.addMethod(
MethodSpec.methodBuilder(timestampSetter)
.addModifiers(Modifier.PRIVATE)
.returns(TypeName.VOID)
.addParameter(Long.class, "timestamp", Modifier.FINAL)
.addStatement("this.$N = $N", timestampField, "timestamp")
.build()
);
if (!startedTimestampControlFlow.getAndSet(true)) {
getColumnTimestampBuilder.beginControlFlow("if ($T.$L.equals(column))", columnsClassName, upperCase)
.addStatement("return $N.$L()", "entity", timestampGetter);
setColumnTimestampBuilder.beginControlFlow("if ($T.$L.equals(column))", columnsClassName, upperCase)
.addStatement("$N.$L($N)", "entity", timestampSetter, "timestamp");
} else {
getColumnTimestampBuilder.nextControlFlow("else if ($T.$L.equals(column))", columnsClassName, upperCase)
.addStatement("return $N.$L()", "entity", timestampGetter);
setColumnTimestampBuilder.nextControlFlow("else if ($T.$L.equals(column))", columnsClassName, upperCase)
.addStatement("$N.$L($N)", "entity", timestampSetter, "timestamp");
}
}
entityBuilder.addMethod(setterBuilder.build());
final String columnFamily = Optional.of(column.family())
.filter(family -> !family.isEmpty())
.orElseThrow(() -> new ElementProcessingException("@Column family must be defined", columnElement));
final String columnQualifier = Optional.of(column.qualifier())
.filter(qualifier -> !qualifier.isEmpty())
.orElse(lowerCamelCase);
if (!columnHashes.add(Objects.hash(columnFamily, columnQualifier))) {
throw new ElementProcessingException("Duplicate @Column definition", columnElement);
}
final Class> equalsType;
if (TypeKind.ARRAY.equals(kind)) {
equalsType = Arrays.class;
arrayFields.add(lowerCamelCase);
} else {
equalsType = Objects.class;
objectFields.add(lowerCamelCase);
}
final String equalsPrefix;
if (!startedControlFlow.getAndSet(true)) {
getColumnValueBuilder.beginControlFlow("if ($T.$L.equals(column))", columnsClassName, upperCase)
.addStatement("return $N.$L()", "entity", getter);
setColumnValueBuilder.beginControlFlow("if ($T.$L.equals(column))", columnsClassName, upperCase)
.addStatement("$N.$L(($T) $N)", "entity", setter, typeName, "value");
equalsPrefix = "";
toStringReturnBuilder.add("\n+ \"");
} else {
getColumnValueBuilder.nextControlFlow("else if ($T.$L.equals(column))", columnsClassName, upperCase)
.addStatement("return $N.$L()", "entity", getter);
setColumnValueBuilder.nextControlFlow("else if ($T.$L.equals(column))", columnsClassName, upperCase)
.addStatement("$N.$L(($T) $N)", "entity", setter, typeName, "value");
equalsPrefix = "\n" + "&& ";
toStringReturnBuilder.add("\n+ \", ");
}
equalsReturnBuilder.add(equalsPrefix + "$T.equals($N, $N.$N)", equalsType, lowerCamelCase, "that", lowerCamelCase);
if (TypeKind.ARRAY.equals(kind)) {
toStringReturnBuilder.add("$L=\" + $T.toString($N)", lowerCamelCase, Arrays.class, lowerCamelCase);
} else if (TypeName.get(String.class).equals(typeName)) {
toStringReturnBuilder.add("$L='\" + $N + '\\''", lowerCamelCase, lowerCamelCase);
} else {
toStringReturnBuilder.add("$L=\" + $N", lowerCamelCase, lowerCamelCase);
}
columnsBuilder.addEnumConstant(upperCase, TypeSpec.anonymousClassBuilder("$S, $S, new $T<$T>() { }, $L",
columnFamily, columnQualifier, TypeReference.class, typeName, column.versioned()).build());
}
getColumnValueBuilder.nextControlFlow("else");
getColumnValueBuilder.addStatement("throw new $T($S)", IllegalArgumentException.class, "Unrecognized column");
getColumnValueBuilder.endControlFlow();
setColumnValueBuilder.nextControlFlow("else");
setColumnValueBuilder.addStatement("throw new $T($S)", IllegalArgumentException.class, "Unrecognized column");
setColumnValueBuilder.endControlFlow();
if (startedTimestampControlFlow.get()) {
getColumnTimestampBuilder.nextControlFlow("else");
setColumnTimestampBuilder.nextControlFlow("else");
}
getColumnTimestampBuilder.addStatement("throw new $T($S)", IllegalArgumentException.class, "Invalid column");
setColumnTimestampBuilder.addStatement("throw new $T($S)", IllegalArgumentException.class, "Invalid column");
if (startedTimestampControlFlow.get()) {
getColumnTimestampBuilder.endControlFlow();
setColumnTimestampBuilder.endControlFlow();
}
entityDelegateBuilder.addMethod(getColumnValueBuilder.build());
entityDelegateBuilder.addMethod(setColumnValueBuilder.build());
entityDelegateBuilder.addMethod(getColumnTimestampBuilder.build());
entityDelegateBuilder.addMethod(setColumnTimestampBuilder.build());
entityConfigurationBuilder.addMethod(MethodSpec.methodBuilder("getDelegateForEntity")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(ClassName.get(EntityConfiguration.EntityDelegate.class), entityClassName))
.addParameter(entityClassName, "entity", Modifier.FINAL)
.addStatement("return new $T($N)", delegateClassName, "entity")
.build());
columnsBuilder.addField(String.class, "family", Modifier.PRIVATE, Modifier.FINAL);
columnsBuilder.addField(String.class, "qualifier", Modifier.PRIVATE, Modifier.FINAL);
columnsBuilder.addField(ParameterizedTypeName.get(ClassName.get(TypeReference.class), WildcardTypeName.subtypeOf(Object.class)),
"typeReference", Modifier.PRIVATE, Modifier.FINAL);
columnsBuilder.addField(TypeName.BOOLEAN, "isVersioned", Modifier.PRIVATE, Modifier.FINAL);
columnsBuilder.addMethod(MethodSpec.constructorBuilder()
.addParameter(String.class, "family")
.addParameter(String.class, "qualifier")
.addParameter(ParameterizedTypeName.get(ClassName.get(TypeReference.class), WildcardTypeName.subtypeOf(Object.class)), "typeReference")
.addParameter(TypeName.BOOLEAN, "isVersioned")
.addStatement("this.$N = $N", "family", "family")
.addStatement("this.$N = $N", "qualifier", "qualifier")
.addStatement("this.$N = $N", "typeReference", "typeReference")
.addStatement("this.$N = $N", "isVersioned", "isVersioned")
.build());
columnsBuilder.addMethod(MethodSpec.methodBuilder("getFamily")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(String.class)
.addStatement("return $N", "family")
.build());
columnsBuilder.addMethod(MethodSpec.methodBuilder("getQualifier")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(String.class)
.addStatement("return $N", "qualifier")
.build());
columnsBuilder.addMethod(MethodSpec.methodBuilder("getTypeReference")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(ClassName.get(TypeReference.class), WildcardTypeName.subtypeOf(Object.class)))
.addStatement("return $N", "typeReference")
.build());
columnsBuilder.addMethod(MethodSpec.methodBuilder("isVersioned")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.BOOLEAN)
.addStatement("return $N", "isVersioned")
.build());
equalsBuilder.addStatement(equalsReturnBuilder.build());
entityBuilder.addMethod(equalsBuilder.build());
final String objectFieldsCsv = String.join(", ", objectFields);
final CodeBlock objectsHashCodeBlock = CodeBlock.builder().add("$T.hash($L)", Objects.class, objectFieldsCsv)
.build();
if (arrayFields.isEmpty()) {
hashCodeBuilder.addStatement("return $L", objectsHashCodeBlock);
} else {
final int arrayFieldStartIndex;
final boolean shouldReturn;
if (objectFields.isEmpty()) {
final CodeBlock firstArrayHashCodeBlock = CodeBlock.builder().add("$T.hashCode($N)",
Arrays.class, arrayFields.get(0))
.build();
if (arrayFields.size() > 1) {
hashCodeBuilder.addStatement("int result = $L", firstArrayHashCodeBlock);
shouldReturn = true;
} else {
hashCodeBuilder.addStatement("return $L", firstArrayHashCodeBlock);
shouldReturn = false;
}
arrayFieldStartIndex = 1;
} else {
hashCodeBuilder.addStatement("int result = $L", objectsHashCodeBlock);
arrayFieldStartIndex = 0;
shouldReturn = true;
}
IntStream.range(arrayFieldStartIndex, arrayFields.size())
.mapToObj(arrayFields::get)
.forEachOrdered(arrayField -> hashCodeBuilder.addStatement("$N = 31 * $N + $T.hashCode($N)",
"result", "result", Arrays.class, arrayField));
if (shouldReturn) {
hashCodeBuilder.addStatement("return $N", "result");
}
}
entityBuilder.addMethod(hashCodeBuilder.build());
toStringReturnBuilder.add("\n+ '}'");
toStringBuilder.addStatement(toStringReturnBuilder.build());
entityBuilder.addMethod(toStringBuilder.build());
final Entity annotatedEntity = entityElement.getAnnotation(Entity.class);
final List keyComponents = Arrays.asList(annotatedEntity.keyComponents());
if (keyComponents.isEmpty()) {
throw new ElementProcessingException("@Entity types require at least 1 @KeyComponent", entityElement);
}
final ClassName keyBuilderClassName = entityClassName.nestedClass(entityName + "KeyBuilder");
final TypeSpec.Builder keyBuilderBuilder = TypeSpec.classBuilder(keyBuilderClassName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.addComment("Prevent external instantiation")
.build());
final Map namedBuildParameters = new HashMap<>();
final TypeName parameterizedKeyBuilderName = ParameterizedTypeName.get(ClassName.get(KeyBuilder.class), entityClassName);
final TypeName parameterizedKeyName = ParameterizedTypeName.get(ClassName.get(Key.class), entityClassName);
final Map constantComponents = IntStream.range(0, keyComponents.size())
.filter(i -> Optional.of(keyComponents.get(i))
.map(KeyComponent::constant)
.filter(s -> !s.isEmpty())
.isPresent())
.boxed()
.collect(Collectors.toMap(Function.identity(), keyComponents::get));
constantComponents.forEach((i, keyComponent) -> {
final String name = String.join("_", "COMPONENT", String.valueOf(i));
keyBuilderBuilder.addField(FieldSpec.builder(String.class, name,
Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$S", keyComponent.constant())
.build());
namedBuildParameters.put("keyComponent" + i, name);
});
final AtomicInteger stepCounter = new AtomicInteger(0);
final Map dynamicComponents = IntStream.range(0, keyComponents.size())
.filter(i -> !constantComponents.containsKey(i))
.mapToObj(i -> {
final int step = stepCounter.getAndIncrement();
final KeyComponent keyComponent = keyComponents.get(i);
namedBuildParameters.put("keyComponent" + i, keyComponent.name());
return new AbstractMap.SimpleImmutableEntry<>(step, keyComponent);
})
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(u, v) -> {
// Impossible
throw new IllegalStateException("Duplicate key: " + u);
},
LinkedHashMap::new // Preserves order of declared components
));
final Map stepClassNames = dynamicComponents.entrySet().stream()
.map(entry -> {
final String lowerCamelCase = entry.getValue().name();
final String upperCamelCase = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, lowerCamelCase);
final ClassName className = entityClassName.nestedClass("Requires" + upperCamelCase);
return new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), className);
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
dynamicComponents.forEach((step, keyComponent) -> {
final TypeMirror typeMirror = getTypeMirrorFromAnnotation(keyComponent, KeyComponent::type);
final ClassName className = stepClassNames.get(step);
final TypeName nextTypeName = Optional.ofNullable(stepClassNames.get(step + 1))
.map(TypeName.class::cast)
.orElse(parameterizedKeyBuilderName);
final String lowerCamelCase = keyComponent.name();
entityBuilder.addType(TypeSpec.interfaceBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addMethod(MethodSpec.methodBuilder(lowerCamelCase)
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.returns(nextTypeName)
.addParameter(TypeName.get(typeMirror), lowerCamelCase, Modifier.FINAL)
.build())
.build());
keyBuilderBuilder.addSuperinterface(className)
.addField(TypeName.get(typeMirror), lowerCamelCase, Modifier.PRIVATE)
.addMethod(MethodSpec.methodBuilder(lowerCamelCase)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(nextTypeName)
.addParameter(TypeName.get(typeMirror), lowerCamelCase, Modifier.FINAL)
.addStatement("this.$N = $N", lowerCamelCase, lowerCamelCase)
.addStatement("return this")
.build());
});
keyBuilderBuilder.addSuperinterface(parameterizedKeyBuilderName);
// These names are getting ridiculous
final MethodSpec.Builder keyBuilderBuildBuilder = MethodSpec.methodBuilder("build")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(parameterizedKeyName);
final String streamOfComponents = IntStream.range(0, keyComponents.size()).boxed()
.map(i -> "$keyComponent" + i + ":N")
.collect(Collectors.joining(", "));
final String keyStringName = "keyString";
namedBuildParameters.put("string", String.class);
namedBuildParameters.put("var", keyStringName);
namedBuildParameters.put("stream", Stream.class);
namedBuildParameters.put("objects", Objects.class);
namedBuildParameters.put("object", Object.class);
namedBuildParameters.put("collectors", Collectors.class);
namedBuildParameters.put("delimiter", annotatedEntity.keyDelimiter());
namedBuildParameters.put("indent", INDENT);
keyBuilderBuildBuilder.addNamedCode("final $string:T $var:L = $stream:T.of(" + streamOfComponents + ")\n"
+ "$indent:L$indent:L.peek($objects:T::requireNonNull)\n"
+ "$indent:L$indent:L.map($object:T::toString)\n"
+ "$indent:L$indent:L.collect($collectors:T.joining($delimiter:S));\n",
namedBuildParameters)
.addStatement("return new $T<>($N)", StringKey.class, keyStringName);
keyBuilderBuilder.addMethod(keyBuilderBuildBuilder.build());
final TypeName keyBuilderTypeName = Optional.ofNullable(stepClassNames.get(0))
.map(TypeName.class::cast)
.orElse(parameterizedKeyBuilderName);
entityBuilder.addMethod(MethodSpec.methodBuilder("keyBuilder")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(keyBuilderTypeName)
.addStatement("return new $T()", keyBuilderClassName)
.build());
entityBuilder.addStaticBlock(CodeBlock.builder()
.addStatement("register($T.$N, $T.class)", configurationClassName, "INSTANCE", entityClassName)
.build());
entityBuilder.addType(keyBuilderBuilder.build());
entityBuilder.addType(columnsBuilder.build());
entityBuilder.addType(entityConfigurationBuilder.build());
entityBuilder.addType(entityDelegateBuilder.build());
return JavaFile.builder(packageName, entityBuilder.build()).indent(INDENT).build();
}
/**
* This is the stupidest thing I've ever seen, but I get it.
*
* "The annotation returned by this method could contain an element whose value is of type {@link Class}.
* This value cannot be returned directly: information necessary to locate and load a class (such as the class
* loader to use) is not available, and the class might not be loadable at all. Attempting to read a {@link Class}
* object by invoking the relevant method on the returned annotation will result in a {@link MirroredTypeException},
* from which the corresponding {@link TypeMirror} may be extracted."
*
* @see https://docs.oracle.com/javase/8/docs/api/javax/lang/model/element/Element.html#getAnnotation-java.lang.Class-
*/
private static TypeMirror getTypeMirrorFromAnnotation(final T annotation,
final Function> classFunction) {
try {
classFunction.apply(annotation);
throw new IllegalStateException("Annotation function did not throw the expected MirroredTypeException");
} catch (final MirroredTypeException e) {
return e.getTypeMirror();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy