io.requery.processor.EntityGenerator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of requery-processor Show documentation
Show all versions of requery-processor Show documentation
A light but powerful object mapper and SQL generator for Java/Android
The newest version!
/*
* Copyright 2017 requery.io
*
* 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.requery.processor;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.WildcardTypeName;
import io.requery.Entity;
import io.requery.Persistable;
import io.requery.PropertyNameStyle;
import io.requery.meta.Attribute;
import io.requery.proxy.EntityProxy;
import io.requery.proxy.PreInsertListener;
import io.requery.proxy.PropertyState;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;
/**
* Generates a java class file from an abstract class marked with the {@link Entity} annotation.
* This class contains metadata about the table/column mapping that was annotated in the super
* class. Also contains generated getter/setters for fields in the super class that will be
* persisted.
*
* @author Nikhil Purushe
*/
class EntityGenerator extends EntityPartGenerator implements SourceGenerator {
private final EntityDescriptor parent;
private final Set typeExtensions;
private final Set memberExtensions;
EntityGenerator(ProcessingEnvironment processingEnv,
EntityGraph graph,
EntityDescriptor entity,
EntityDescriptor parent) {
super(processingEnv, graph, entity);
this.parent = parent;
typeExtensions = new HashSet<>();
memberExtensions = new HashSet<>();
// android extensions
typeExtensions.add(new AndroidParcelableExtension(types));
AndroidObservableExtension observable =
new AndroidObservableExtension(entity, processingEnv);
typeExtensions.add(observable);
memberExtensions.add(observable);
}
@Override
public void generate() throws IOException {
ClassName entityTypeName = entity.isEmbedded() ?
nameResolver.embeddedTypeNameOf(entity, parent) : typeName;
TypeSpec.Builder builder = TypeSpec.classBuilder(entityTypeName)
.addModifiers(Modifier.PUBLIC)
.addOriginatingElement(typeElement);
boolean metadataOnly = entity.isImmutable() || entity.isUnimplementable();
if (typeElement.getKind().isInterface()) {
builder.addSuperinterface(ClassName.get(typeElement));
builder.addSuperinterface(ClassName.get(Persistable.class));
} else if (!metadataOnly) {
builder.superclass(ClassName.get(typeElement));
builder.addSuperinterface(ClassName.get(Persistable.class));
}
CodeGeneration.addGeneratedAnnotation(processingEnv, builder);
if (!entity.isEmbedded()) {
EntityMetaGenerator meta = new EntityMetaGenerator(processingEnv, graph, entity);
meta.generate(builder);
}
if (!metadataOnly) {
generateConstructors(builder);
generateMembers(builder);
generateProxyMethods(builder);
if (!entity.isEmbedded()) {
generateEquals(builder);
generateHashCode(builder);
generateToString(builder);
}
if (entity.isCopyable()) {
generateCopy(builder);
}
} else {
// private constructor
builder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE).build());
generateMembers(builder); // members for builder if needed
generateImmutableTypeBuildMethod(builder);
}
typeExtensions.forEach(extension -> extension.generate(entity, builder));
CodeGeneration.writeType(processingEnv, typeName.packageName(), builder.build());
}
private Modifier[] generatedMemberModifiers(Modifier... modifiers) {
Modifier visibility = null;
switch (entity.propertyVisibility()) {
case PUBLIC:
visibility = Modifier.PUBLIC;
break;
case PRIVATE:
if (entity.isEmbedded()) {
visibility = Modifier.PROTECTED;
} else {
visibility = Modifier.PRIVATE;
}
break;
case PACKAGE:
break;
}
ArrayList list = new ArrayList<>();
if (visibility != null) {
list.add(visibility);
}
Collections.addAll(list, modifiers);
return list.toArray(new Modifier[list.size()]);
}
private void generateMembers(TypeSpec.Builder builder) {
// generate property states
if (!entity.isStateless()) {
entity.attributes().stream()
.filter(attribute -> !attribute.isTransient())
.forEach(attribute -> {
TypeName stateType = ClassName.get(PropertyState.class);
builder.addField(FieldSpec
.builder(stateType, propertyStateFieldName(attribute),
generatedMemberModifiers())
.build());
});
}
if (entity.isEmbedded() && !(entity.isImmutable() || entity.isUnimplementable())) {
entity.attributes().stream()
.filter(attribute -> !attribute.isTransient())
.forEach(attribute -> {
ParameterizedTypeName attributeType = ParameterizedTypeName.get(
ClassName.get(Attribute.class), nameResolver.typeNameOf(parent),
resolveAttributeType(attribute));
builder.addField(FieldSpec
.builder(attributeType,
attributeFieldName(attribute),
generatedMemberModifiers(Modifier.FINAL))
.build());
});
}
// only generate for interfaces or if the entity is immutable but has no builder
boolean generateMembers = typeElement.getKind().isInterface() ||
!entity.builderType().isPresent();
Set existingFieldNames = entity.attributes().stream()
.map(AttributeDescriptor::element)
.filter(it -> it.getKind() == ElementKind.FIELD)
.map(it -> it.getSimpleName().toString())
.collect(Collectors.toSet());
if (generateMembers) {
for (AttributeDescriptor attribute : entity.attributes()) {
Element element = attribute.element();
if (element.getKind() == ElementKind.METHOD) {
ExecutableElement methodElement = (ExecutableElement) element;
TypeMirror typeMirror = methodElement.getReturnType();
TypeName fieldName;
if (attribute.isIterable()) {
fieldName = parameterizedCollectionName(typeMirror);
} else if (attribute.isOptional()) {
typeMirror = tryFirstTypeArgument(attribute.typeMirror());
fieldName = TypeName.get(typeMirror);
} else {
fieldName = nameResolver.tryGeneratedTypeName(typeMirror);
}
if (entity.isImmutable() || !existingFieldNames.contains(attribute.fieldName())) {
builder.addField(FieldSpec
.builder(fieldName, attribute.fieldName(), generatedMemberModifiers())
.build());
}
}
}
}
if (entity.isImmutable()) {
generateBuilder(builder, entity, "builder");
entity.attributes().stream()
.filter(AttributeDescriptor::isEmbedded)
.forEach(attribute -> graph.embeddedDescriptorOf(attribute).ifPresent(embedded ->
embedded.builderType().ifPresent(type -> {
String fieldName = attribute.fieldName() + "Builder";
generateBuilder(builder, embedded, fieldName);
})));
}
}
private void generateBuilder(TypeSpec.Builder builder, EntityDescriptor entity, String fieldName) {
final String packageName = entity.typeName().packageName();
if (entity.builderFactoryMethod().isPresent()) {
String returnType = entity.builderFactoryMethod().get().getReturnType().toString();
TypeName fieldType = ClassName.get(packageName, returnType);
TypeName factoryType = TypeName.get(entity.element().asType());
String methodName = entity.builderFactoryMethod()
.map(method -> method.getSimpleName().toString())
.orElse("");
builder.addField(initializeBuilder(fieldName, fieldType, factoryType, methodName));
} else if (ImmutableAnnotationKind.IMMUTABLE.isPresent(entity.element())) {
// just a best guess as this class may not be generated yet, maybe can check the
// annotation style ourselves
String simpleName = "Immutable" + entity.element().getSimpleName().toString();
TypeName fieldType = ClassName.get(packageName, simpleName + ".Builder");
TypeName factoryType = ClassName.get(packageName, simpleName);
builder.addField(initializeBuilder(fieldName, fieldType, factoryType, "builder"));
} else if (entity.builderType().isPresent()) {
// work around missing package prefix of a generated builder type
TypeName builderName = guessAnyTypeName(packageName, entity.builderType().get());
builder.addField(initializeBuilder(fieldName, builderName, builderName, null));
}
}
private FieldSpec initializeBuilder(String fieldName, TypeName fieldType,
TypeName builderType, String methodName) {
FieldSpec.Builder builder = FieldSpec.builder(fieldType, fieldName, Modifier.PROTECTED);
if (methodName != null) {
builder.initializer("$T.$L()", builderType, methodName);
} else {
builder.initializer("new $T()", builderType);
}
return builder.build();
}
private void generateConstructors(TypeSpec.Builder builder) {
// copy the existing constructors
for (ExecutableElement constructor : ElementFilter.constructorsIn(
typeElement.getEnclosedElements())) {
// constructor params
List extends VariableElement> parameters = constructor.getParameters();
if (!parameters.isEmpty()) {
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder();
constructorBuilder.addModifiers(constructor.getModifiers());
List parameterNames = new ArrayList<>();
for (VariableElement parameter : parameters) {
Modifier[] modifiers = parameter.getModifiers().toArray(
new Modifier[parameter.getModifiers().size()]);
String parameterName = parameter.getSimpleName().toString();
parameterNames.add(parameterName);
ParameterSpec.Builder parameterBuilder = ParameterSpec.builder(
TypeName.get(parameter.asType()), parameterName, modifiers);
constructorBuilder.addParameter(parameterBuilder.build());
}
// super parameter/arguments
StringJoiner joiner = new StringJoiner(",", "(", ")");
parameterNames.forEach(joiner::add);
constructorBuilder.addStatement("super" + joiner.toString());
builder.addMethod(constructorBuilder.build());
}
}
}
private void generateProxyMethods(TypeSpec.Builder builder) {
// add proxy field
TypeName entityType = entity.isEmbedded() ? nameResolver.typeNameOf(parent) : typeName;
TypeName proxyName = parameterizedTypeName(EntityProxy.class, entityType);
FieldSpec.Builder proxyField = FieldSpec.builder(proxyName, PROXY_NAME,
generatedMemberModifiers(Modifier.FINAL, Modifier.TRANSIENT));
if (!entity.isEmbedded()) {
proxyField.initializer("new $T(this, $L)", proxyName, TYPE_NAME);
}
builder.addField(proxyField.build());
for (AttributeDescriptor attribute : entity.attributes()) {
boolean useField = attribute.isTransient() || attribute.isEmbedded();
TypeMirror typeMirror = attribute.typeMirror();
TypeName unboxedTypeName;
if (attribute.isIterable()) {
unboxedTypeName = parameterizedCollectionName(typeMirror);
} else if (attribute.isOptional()) {
unboxedTypeName = TypeName.get(tryFirstTypeArgument(attribute.typeMirror()));
} else if (attribute.isEmbedded()) {
EntityDescriptor embedded = graph.embeddedDescriptorOf(attribute)
.orElseThrow(IllegalStateException::new);
unboxedTypeName = nameResolver.embeddedTypeNameOf(embedded, entity);
} else {
unboxedTypeName = nameResolver.tryGeneratedTypeName(typeMirror);
}
String attributeName = attribute.fieldName();
String getterName = attribute.getterName();
String fieldName = Names.upperCaseUnderscore(Names.removeMemberPrefixes(attributeName));
if (entity.isEmbedded()) {
fieldName = attributeFieldName(attribute);
}
// getter
MethodSpec.Builder getter = MethodSpec.methodBuilder(getterName)
.addModifiers(Modifier.PUBLIC)
.returns(attribute.isOptional() ? TypeName.get(typeMirror) : unboxedTypeName);
if (Mirrors.overridesMethod(types, typeElement, getterName)) {
getter.addAnnotation(Override.class);
}
memberExtensions.forEach(extension -> extension.addToGetter(attribute, getter));
if (useField) {
if (attribute.isEmbedded()) {
// have to cast to embedded type
getter.addStatement("return ($T)this.$L", unboxedTypeName, attributeName);
} else {
getter.addStatement("return this.$L", attributeName);
}
} else if (attribute.isOptional()) {
String ofNullable = "ofNullable";
if ("com.google.common.base.Optional".equals(attribute.optionalClass())) {
ofNullable = "fromNullable";
}
getter.addStatement("return $T.$L($L.get($L))",
ClassName.bestGuess(attribute.optionalClass()),
ofNullable, PROXY_NAME, fieldName);
} else {
getter.addStatement("return $L.get($L)", PROXY_NAME, fieldName);
}
builder.addMethod(getter.build());
// setter
String setterName = attribute.setterName();
// if read only don't generate a public setter
boolean readOnly = entity.isReadOnly() || attribute.isReadOnly();
// edge case check if it's interface and we need to implement the setter
if (entity.element().getKind().isInterface() &&
ElementFilter.methodsIn(entity.element().getEnclosedElements()).stream()
.anyMatch(element -> element.getSimpleName().toString().equals(setterName))) {
readOnly = false;
}
if (!readOnly) {
TypeName setTypeName = unboxedTypeName;
boolean castType = false;
// use wildcard generic collection type if necessary
if (setTypeName instanceof ParameterizedTypeName) {
ParameterizedTypeName parameterizedName = (ParameterizedTypeName) setTypeName;
List arguments = parameterizedName.typeArguments;
List wildcards = new ArrayList<>();
for (TypeName argument : arguments) {
if (!(argument instanceof WildcardTypeName)) {
Elements elements = processingEnv.getElementUtils();
TypeElement element = elements.getTypeElement(argument.toString());
if (element != null && element.getKind() == ElementKind.INTERFACE) {
wildcards.add(WildcardTypeName.subtypeOf(argument));
} else {
wildcards.add(argument);
}
} else {
wildcards.add(argument);
}
}
TypeName[] array = new TypeName[wildcards.size()];
setTypeName = ParameterizedTypeName.get(parameterizedName.rawType,
wildcards.toArray(array));
castType = true;
}
String paramName = Names.lowerCaseFirst(Names.removeMemberPrefixes(attributeName));
MethodSpec.Builder setter = MethodSpec.methodBuilder(setterName)
.addModifiers(Modifier.PUBLIC)
.addParameter(setTypeName, paramName);
if (useField) {
setter.addStatement("this.$L = $L", attributeName, paramName);
} else {
if (castType) {
setter.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "unchecked").build());
setter.addStatement("$L.set($L, ($T)$L)",
PROXY_NAME, fieldName, unboxedTypeName, paramName);
} else {
setter.addStatement("$L.set($L, $L)", PROXY_NAME, fieldName, paramName);
}
}
memberExtensions.forEach(extension -> extension.addToSetter(attribute, setter));
PropertyNameStyle style = entity.propertyNameStyle();
if (style == PropertyNameStyle.FLUENT || style == PropertyNameStyle.FLUENT_BEAN) {
setter.addStatement("return this");
setter.returns(typeName);
}
builder.addMethod(setter.build());
}
}
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC);
if (entity.isEmbedded()) {
constructor.addParameter(ParameterSpec.builder(proxyName, "proxy").build());
constructor.addStatement("this.$L = proxy", PROXY_NAME);
entity.attributes().stream()
.filter(attribute -> !attribute.isTransient())
.forEach(attribute -> {
ParameterizedTypeName attributeType = ParameterizedTypeName.get(
ClassName.get(Attribute.class), nameResolver.typeNameOf(parent),
resolveAttributeType(attribute));
constructor.addParameter(ParameterSpec
.builder(attributeType, attribute.name()).build());
constructor.addStatement("this.$L = $L",
attributeFieldName(attribute), attribute.name());
});
}
generateListeners(constructor);
// initialize the generated embedded entities
entity.attributes().stream()
.filter(AttributeDescriptor::isEmbedded)
.forEach(attribute -> graph.embeddedDescriptorOf(attribute).ifPresent(embedded -> {
ClassName embeddedName = nameResolver.embeddedTypeNameOf(embedded, entity);
String format = embedded.attributes().stream().map(attr ->
Names.upperCaseUnderscore(embeddedAttributeName(attribute, attr)))
.collect(Collectors.joining(", ", "$L = new $T($L, ", ")"));
constructor.addStatement(format, attribute.fieldName(), embeddedName, PROXY_NAME);
}));
builder.addMethod(constructor.build());
}
private void generateEquals(TypeSpec.Builder builder) {
boolean overridesEquals = Mirrors.overridesMethod(types, typeElement, "equals");
if (!overridesEquals) {
MethodSpec.Builder equals = CodeGeneration.overridePublicMethod("equals")
.addParameter(TypeName.OBJECT, "obj")
.returns(TypeName.BOOLEAN)
.addStatement("return obj instanceof $T && (($T)obj).$L.equals(this.$L)",
typeName, typeName, PROXY_NAME, PROXY_NAME);
builder.addMethod(equals.build());
}
}
private void generateHashCode(TypeSpec.Builder builder) {
if (!Mirrors.overridesMethod(types, typeElement, "hashCode")) {
MethodSpec.Builder hashCode = CodeGeneration.overridePublicMethod("hashCode")
.returns(TypeName.INT)
.addStatement("return $L.hashCode()", PROXY_NAME);
builder.addMethod(hashCode.build());
}
}
private void generateToString(TypeSpec.Builder builder) {
if (!Mirrors.overridesMethod(types, typeElement, "toString")) {
MethodSpec.Builder equals = CodeGeneration.overridePublicMethod("toString")
.returns(String.class)
.addStatement("return $L.toString()", PROXY_NAME);
builder.addMethod(equals.build());
}
}
private void generateCopy(TypeSpec.Builder builder) {
if (!Mirrors.overridesMethod(types, typeElement, "copy")) {
MethodSpec.Builder copy = MethodSpec.methodBuilder("copy")
.addModifiers(Modifier.PUBLIC)
.returns(typeName)
.addStatement("return $L.copy()", PROXY_NAME);
builder.addMethod(copy.build());
}
}
private void generateListeners(MethodSpec.Builder constructor) {
for (Map.Entry entry :
entity.listeners().entrySet()) {
for (Annotation annotation : entry.getValue().listenerAnnotations()) {
String annotationName = annotation.annotationType().getSimpleName()
.replace("Persist", "Insert").replace("Remove", "Delete");
String methodName = Names.lowerCaseFirst(annotationName);
// avoid hardcoding the package name here
Element listener = elements.getTypeElement(
PreInsertListener.class.getCanonicalName());
PackageElement packageElement = elements.getPackageOf(listener);
// generate the listener name
String packageName = packageElement.getQualifiedName().toString();
ClassName listenerName = ClassName.get(packageName, annotationName + "Listener");
ParameterizedTypeName getterType =
ParameterizedTypeName.get(listenerName, typeName);
TypeSpec.Builder listenerBuilder = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(getterType)
.addMethod(CodeGeneration.overridePublicMethod(methodName)
.addParameter(typeName, "entity")
.addStatement("$L()", entry.getKey().getSimpleName())
.build());
constructor.addStatement("$L.modifyListeners().add$L($L)", PROXY_NAME,
annotationName + "Listener", listenerBuilder.build());
}
}
}
private void generateImmutableTypeBuildMethod(TypeSpec.Builder builder) {
if (entity.isImmutable() && !entity.builderType().isPresent() &&
entity.factoryMethod().isPresent()) {
String methodName = entity.factoryMethod()
.map(element -> element.getSimpleName().toString()).orElse("");
List argumentNames = entity.factoryArguments();
StringJoiner joiner = new StringJoiner(",");
argumentNames.forEach(name -> joiner.add("$L"));
MethodSpec.Builder build = MethodSpec.methodBuilder("build")
.returns(ClassName.get(entity.element()));
if (methodName.equals("")) {
Object[] args = new Object[1 + argumentNames.size()];
args[0] = ClassName.get(entity.element());
System.arraycopy(argumentNames.toArray(), 0, args, 1, argumentNames.size());
build.addStatement("return new $T(" + joiner.toString() + ")", args).build();
} else {
Object[] args = new Object[2 + argumentNames.size()];
args[0] = ClassName.get(entity.element());
args[1] = methodName;
System.arraycopy(argumentNames.toArray(), 0, args, 2, argumentNames.size());
build.addStatement("return $T.$L(" + joiner.toString() + ")", args).build();
}
builder.addMethod(build.build());
}
}
}