/* * 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.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import io.requery.CascadeAction; import io.requery.ReferentialAction; import io.requery.meta.Attribute; import io.requery.meta.Cardinality; import io.requery.meta.QueryAttribute; import io.requery.meta.QueryExpression; import io.requery.meta.Type; import io.requery.meta.TypeBuilder; import io.requery.proxy.BooleanProperty; import io.requery.proxy.ByteProperty; import io.requery.proxy.DoubleProperty; import io.requery.proxy.EntityProxy; import io.requery.proxy.FloatProperty; import io.requery.proxy.IntProperty; import io.requery.proxy.LongProperty; import io.requery.proxy.Property; import io.requery.proxy.PropertyState; import io.requery.proxy.ShortProperty; import io.requery.query.Order; import io.requery.util.function.Function; import io.requery.util.function.Supplier; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.tools.Diagnostic; import java.io.IOException; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.StringJoiner; class EntityMetaGenerator extends EntityPartGenerator { private static final String KOTLIN_ATTRIBUTE_DELEGATE = "io.requery.meta.AttributeDelegate"; private final HashSet<String> attributeNames; private final HashSet<String> expressionNames; EntityMetaGenerator(ProcessingEnvironment processingEnvironment, EntityGraph graph, EntityDescriptor entity) { super(processingEnvironment, graph, entity); attributeNames = new HashSet<>(); expressionNames = new HashSet<>(); } void generate(TypeSpec.Builder builder) { boolean metadataOnly = entity.isImmutable() || entity.isUnimplementable(); TypeName targetName = metadataOnly? ClassName.get(entity.element()) : typeName; List<QualifiedName> generatedEmbeddedTypes = new LinkedList<>(); entity.attributes().values().stream() .filter(attribute -> !attribute.isTransient()) .forEach(attribute -> { String fieldName = upperCaseUnderscoreRemovePrefixes(attribute.fieldName()); if (attribute.isForeignKey() && attribute.cardinality() != null) { // generate a foreign key attribute for use in queries but not stored in the type graph.referencingEntity(attribute) .flatMap(entity -> graph.referencingAttribute(attribute, entity)) .ifPresent(foreignKey -> { String name = fieldName + "_ID"; TypeMirror mirror = foreignKey.typeMirror(); builder.addField( generateAttribute(attribute, null, targetName, name, mirror, true) ); expressionNames.add(name); }); } if (attribute.isEmbedded()) { graph.embeddedDescriptorOf(attribute).ifPresent(embedded -> { generateEmbeddedAttributes(attribute, embedded, builder, targetName); if (!generatedEmbeddedTypes.contains(embedded.typeName())) { generatedEmbeddedTypes.add(embedded.typeName()); generateEmbeddedEntity(embedded); } }); } else { TypeMirror mirror = attribute.typeMirror(); builder.addField( generateAttribute(attribute, null, targetName, fieldName, mirror, false) ); attributeNames.add(fieldName); } }); generateType(builder, targetName); } private void generateType(TypeSpec.Builder builder, TypeName targetName) { CodeBlock.Builder block = CodeBlock.builder().add("new $T<$T>($T.class, $S)\n", TypeBuilder.class, targetName, targetName, entity.tableName()); block.add(".setBaseType($T.class)\n", ClassName.get(typeElement)) .add(".setCacheable($L)\n", entity.isCacheable()) .add(".setImmutable($L)\n", entity.isImmutable()) .add(".setReadOnly($L)\n", entity.isReadOnly()) .add(".setStateless($L)\n", entity.isStateless()) .add(".setView($L)\n", entity.isView()); String factoryName = entity.classFactoryName(); if (!Names.isEmpty(factoryName)) { block.add(".setFactory(new $L())\n", ClassName.bestGuess(factoryName)); } else if (entity.isImmutable()) { // returns this class as the builder TypeSpec.Builder supplier = TypeSpec.anonymousClassBuilder("") .addSuperinterface(parameterizedTypeName(Supplier.class, typeName)); supplier.addMethod(CodeGeneration.overridePublicMethod("get") .returns(typeName) .addStatement("return new $T()", typeName).build()); block.add(".setBuilderFactory($L)\n", supplier.build()); MethodSpec.Builder applyMethod = CodeGeneration.overridePublicMethod("apply") .addParameter(typeName, "value") .returns(targetName); // add embedded builder calls entity.attributes().values().stream() .filter(AttributeDescriptor::isEmbedded) .forEach(attribute -> graph.embeddedDescriptorOf(attribute).ifPresent(embedded -> embedded.builderType().ifPresent(type -> { String fieldName = attribute.fieldName() + "Builder"; String methodName = attribute.setterName(); applyMethod.addStatement( "value.builder.$L(value.$L.build())", methodName, fieldName); }))); applyMethod.addStatement(entity.builderType().isPresent() ? "return value.builder.build()" : "return value.build()"); TypeSpec.Builder buildFunction = TypeSpec.anonymousClassBuilder("") .addSuperinterface(parameterizedTypeName(Function.class, typeName, targetName)) .addMethod(applyMethod.build()); block.add(".setBuilderFunction($L)\n", buildFunction.build()); } else { TypeSpec.Builder typeFactory = TypeSpec.anonymousClassBuilder("") .addSuperinterface(parameterizedTypeName(Supplier.class, targetName)) .addMethod( CodeGeneration.overridePublicMethod("get") .addStatement("return new $T()", targetName) .returns(targetName) .build()); block.add(".setFactory($L)\n", typeFactory.build()); } ParameterizedTypeName proxyType = parameterizedTypeName(EntityProxy.class, targetName); TypeSpec.Builder proxyProvider = TypeSpec.anonymousClassBuilder("") .addSuperinterface(parameterizedTypeName(Function.class, targetName, proxyType)); MethodSpec.Builder proxyFunction = CodeGeneration.overridePublicMethod("apply") .addParameter(targetName, "entity") .returns(proxyType); if (entity.isImmutable() || entity.isUnimplementable()) { proxyFunction.addStatement("return new $T(entity, $L)", proxyType, TYPE_NAME); } else { proxyFunction.addStatement("return entity.$L", PROXY_NAME); } proxyProvider.addMethod(proxyFunction.build()); block.add(".setProxyProvider($L)\n", proxyProvider.build()); if (entity.tableAttributes().length > 0) { StringJoiner joiner = new StringJoiner(",", "new String[] {", "}"); for (String attribute : entity.tableAttributes()) { joiner.add("\"" + attribute + "\""); } block.add(".setTableCreateAttributes($L)\n", joiner.toString()); } if (entity.tableUniqueIndexes().length > 0) { StringJoiner joiner = new StringJoiner(",", "new String[] {", "}"); for (String attribute : entity.tableUniqueIndexes()) { joiner.add("\"" + attribute + "\""); } block.add(".setTableUniqueIndexes($L)\n", joiner.toString()); } attributeNames.forEach(name -> block.add(".addAttribute($L)\n", name)); expressionNames.forEach(name -> block.add(".addExpression($L)\n", name)); block.add(".build()"); ParameterizedTypeName type = parameterizedTypeName(Type.class, targetName); builder.addField( FieldSpec.builder(type, TYPE_NAME, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .initializer("$L", block.build()) .build()); } private void generateEmbeddedAttributes(AttributeDescriptor parent, EntityDescriptor embedded, TypeSpec.Builder builder, TypeName targetName) { // generate the embedded attributes into this type embedded.attributes().values().forEach(attribute -> { String fieldName = Names.upperCaseUnderscore(embeddedAttributeName(parent, attribute)); TypeMirror mirror = attribute.typeMirror(); builder.addField( generateAttribute(attribute, parent, targetName, fieldName, mirror, false)); attributeNames.add(fieldName); }); } private void generateEmbeddedEntity(EntityDescriptor embedded) { // generate an embedded implementation for this (the parent) entity try { new EntityGenerator(processingEnv, graph, embedded, entity).generate(); } catch (IOException e) { throw new RuntimeException(e); } } private FieldSpec generateAttribute(AttributeDescriptor attribute, AttributeDescriptor parent, TypeName targetName, String fieldName, TypeMirror mirror, boolean expression) { TypeMirror typeMirror = mirror; TypeName typeName; if (attribute.isIterable()) { typeMirror = tryFirstTypeArgument(typeMirror); typeName = parameterizedCollectionName(attribute.typeMirror()); } else if (attribute.isOptional()) { typeMirror = tryFirstTypeArgument(typeMirror); typeName = TypeName.get(typeMirror); } else { typeName = nameResolver.generatedTypeNameOf(typeMirror).orElse(null); } if (typeName == null) { typeName = boxedTypeName(typeMirror); } ParameterizedTypeName type; ClassName attributeType = null; boolean useKotlinDelegate = false; if (expression) { type = parameterizedTypeName(QueryExpression.class, typeName); } else { // if it's an association don't make it available as a query attribute boolean isQueryable = attribute.cardinality() == null || attribute.isForeignKey(); Class<?> attributeClass = isQueryable ? QueryAttribute.class : Attribute.class; attributeType = ClassName.get(attributeClass); if (isQueryable) { TypeElement delegateType = elements.getTypeElement(KOTLIN_ATTRIBUTE_DELEGATE); if (delegateType != null) { attributeType = ClassName.get(delegateType); useKotlinDelegate = true; } } type = ParameterizedTypeName.get(attributeType, targetName, typeName); } CodeBlock.Builder builder = CodeBlock.builder(); String attributeName = attribute.name(); if (parent != null && parent.isEmbedded()) { attributeName = embeddedAttributeName(parent, attribute); } if (attribute.isIterable()) { typeMirror = tryFirstTypeArgument(typeMirror); TypeName name = nameResolver.tryGeneratedTypeName(typeMirror); TypeElement collection = (TypeElement) types.asElement(attribute.typeMirror()); ParameterizedTypeName builderName = parameterizedTypeName( attribute.builderClass(), targetName, typeName, name); builder.add("\nnew $T($S, $T.class, $T.class)\n", builderName, attributeName, ClassName.get(collection), name); } else if (attribute.isMap() && attribute.cardinality() != null) { List<TypeMirror> parameters = Mirrors.listGenericTypeArguments(typeMirror); // key type TypeName keyName = TypeName.get(parameters.get(0)); // value type typeMirror = parameters.get(1); TypeName valueName = nameResolver.tryGeneratedTypeName(typeMirror); TypeElement valueElement = (TypeElement) types.asElement(attribute.typeMirror()); ParameterizedTypeName builderName = parameterizedTypeName( attribute.builderClass(), targetName, typeName, keyName, valueName); builder.add("\nnew $T($S, $T.class, $T.class, $T.class)\n", builderName, attributeName, ClassName.get(valueElement), keyName, valueName); } else { ParameterizedTypeName builderName = parameterizedTypeName( attribute.builderClass(), targetName, typeName); TypeName classType = typeName; if (typeMirror.getKind().isPrimitive()) { // if primitive just use the primitive class not the boxed version classType = TypeName.get(typeMirror); } String statement; if (Mirrors.listGenericTypeArguments(typeMirror).size() > 0) { // use the erased type and cast to class classType = TypeName.get(types.erasure(typeMirror)); statement = "\nnew $T($S, (Class)$T.class)\n"; } else { statement ="\nnew $T($S, $T.class)\n"; } builder.add(statement, builderName, attributeName, classType); } if (!expression) { generateProperties(attribute, parent, typeMirror, targetName, typeName, builder); } // attribute builder properties if (attribute.isKey()) { builder.add(".setKey(true)\n"); } builder.add(".setGenerated($L)\n", attribute.isGenerated()); builder.add(".setReadOnly($L)\n", attribute.isReadOnly()); builder.add(".setLazy($L)\n", attribute.isLazy()); builder.add(".setNullable($L)\n", attribute.isNullable()); builder.add(".setUnique($L)\n", attribute.isUnique()); if (!Names.isEmpty(attribute.defaultValue())) { builder.add(".setDefaultValue($S)\n", attribute.defaultValue()); } if (!Names.isEmpty(attribute.collate())) { builder.add(".setCollate($S)\n", attribute.collate()); } if (attribute.columnLength() != null) { builder.add(".setLength($L)\n", attribute.columnLength()); } if (!Names.isEmpty(attribute.definition())) { builder.add(".setDefinition($S)\n", attribute.definition()); } if (attribute.isVersion()) { builder.add(".setVersion($L)\n", attribute.isVersion()); } if (attribute.converterName() != null) { builder.add(".setConverter(new $L())\n", attribute.converterName()); } if (attribute.isForeignKey()) { builder.add(".setForeignKey($L)\n", attribute.isForeignKey()); Optional<EntityDescriptor> referencedType = graph.referencingEntity(attribute); referencedType.ifPresent(referenced -> { builder.add(".setReferencedClass($T.class)\n", referenced.isImmutable() ? TypeName.get(referenced.element().asType()) : nameResolver.typeNameOf(referenced)); graph.referencingAttribute(attribute, referenced).ifPresent( referencedAttribute -> { String name = upperCaseUnderscoreRemovePrefixes(referencedAttribute.fieldName()); TypeSpec provider = CodeGeneration.createAnonymousSupplier( ClassName.get(Attribute.class), CodeBlock.builder().addStatement("return $T.$L", nameResolver.typeNameOf(referenced), name).build()); builder.add(".setReferencedAttribute($L)\n", provider); }); }); } if (attribute.isIndexed()) { builder.add(".setIndexed($L)\n", attribute.isIndexed()); if (!attribute.indexNames().isEmpty()) { StringJoiner joiner = new StringJoiner(","); attribute.indexNames().forEach(name -> joiner.add("$S")); builder.add(".setIndexNames(" + joiner + ")\n", attribute.indexNames().toArray()); } } if (attribute.deleteAction() != null) { builder.add(".setDeleteAction($T.$L)\n", ClassName.get(ReferentialAction.class), attribute.deleteAction()); } if (attribute.updateAction() != null) { builder.add(".setUpdateAction($T.$L)\n", ClassName.get(ReferentialAction.class), attribute.updateAction()); } if (!attribute.cascadeActions().isEmpty()) { StringJoiner joiner = new StringJoiner(","); attribute.cascadeActions().forEach(action -> joiner.add("$T.$L")); int index = 0; ClassName cascadeClass = ClassName.get(CascadeAction.class); Object[] args = new Object[attribute.cascadeActions().size()*2]; for (CascadeAction action : attribute.cascadeActions()) { args[index++] = cascadeClass; args[index++] = action; } builder.add(".setCascadeAction(" + joiner + ")\n", args); } if (attribute.cardinality() != null) { if (!expression) { builder.add(".setCardinality($T.$L)\n", ClassName.get(Cardinality.class), attribute.cardinality()); } graph.referencingEntity(attribute).ifPresent(referenced -> { Set<AttributeDescriptor> mappings = graph.mappedAttributes(entity, attribute, referenced); if (attribute.cardinality() == Cardinality.MANY_TO_MANY) { generateJunctionType(attribute, referenced, mappings) .ifPresent(name -> builder.add(".setReferencedClass($T.class)\n", name)); } if (mappings.size() == 1) { AttributeDescriptor mapped = mappings.iterator().next(); String staticMemberName = upperCaseUnderscoreRemovePrefixes(mapped.fieldName()); TypeSpec provider = CodeGeneration.createAnonymousSupplier( ClassName.get(Attribute.class), CodeBlock.builder().addStatement("return $T.$L", nameResolver.typeNameOf(referenced), staticMemberName).build()); builder.add(".setMappedAttribute($L)\n", provider); } if (attribute.orderBy() != null) { referenced.attributes().values().stream() .filter(entry -> entry.name().equals(attribute.orderBy())) .findFirst().ifPresent(orderBy -> { String staticMemberName = upperCaseUnderscoreRemovePrefixes(orderBy.fieldName()); TypeSpec provider = CodeGeneration.createAnonymousSupplier( ClassName.get(Attribute.class), CodeBlock.builder().addStatement("return $T.$L", nameResolver.typeNameOf(referenced), staticMemberName).build()); builder.add(".setOrderByAttribute($L)\n", provider); builder.add(".setOrderByDirection($T.$L)\n", ClassName.get(Order.class), attribute.orderByDirection()); }); } }); } builder.add(".build()"); FieldSpec.Builder field = FieldSpec.builder(type, fieldName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL); if (useKotlinDelegate) { return field.initializer("new $T($L)", attributeType, builder.build()).build(); } else { return field.initializer("$L", builder.build()).build(); } } private Optional<TypeName> generateJunctionType(AttributeDescriptor attribute, EntityDescriptor referenced, Set<AttributeDescriptor> mappings) { TypeName typeName = null; Optional<AssociativeEntityDescriptor> descriptor = attribute.associativeEntity(); if (descriptor.isPresent()) { Optional<TypeMirror> mirror = descriptor.get().type(); if (mirror.isPresent()) { typeName = guessAnyTypeName(entity.typeName().packageName(), mirror.get()); } else { // generate a special type for the junction table (with attributes) graph.referencingEntity(attribute).ifPresent(referencing -> { JoinEntityGenerator generator = new JoinEntityGenerator( processingEnv, nameResolver, entity, referencing, attribute); try { generator.generate(); } catch (IOException e) { processingEnv.getMessager() .printMessage(Diagnostic.Kind.ERROR, e.toString()); throw new RuntimeException(e); } }); typeName = nameResolver.joinEntityName(descriptor.get(), entity, referenced); } } else if (mappings.size() == 1) { descriptor = mappings.iterator().next().associativeEntity(); if (descriptor.isPresent()) { Optional<TypeMirror> mirror = descriptor.get().type(); if (mirror.isPresent()) { typeName = guessAnyTypeName(entity.typeName().packageName(), mirror.get()); } else { typeName = nameResolver.joinEntityName(descriptor.get(), referenced, entity); } } } return Optional.ofNullable(typeName); } private void generateProperties(AttributeDescriptor attribute, AttributeDescriptor parent, TypeMirror typeMirror, TypeName targetName, TypeName attributeName, CodeBlock.Builder block) { String prefix = ""; if (parent != null) { prefix = parent.getterName() + "()."; } // boxed get/set using Objects Class propertyClass = propertyClassFor(typeMirror); ParameterizedTypeName propertyType = propertyName(propertyClass, targetName, attributeName); TypeSpec.Builder builder = TypeSpec.anonymousClassBuilder("") .addSuperinterface(propertyType); boolean isNullable = typeMirror.getKind().isPrimitive() && attribute.isNullable(); boolean useGetter = entity.isUnimplementable() || entity.isImmutable(); boolean useSetter = entity.isUnimplementable(); String getName = prefix + (useGetter? attribute.getterName() : attribute.fieldName()); String setName = prefix + (useSetter? attribute.setterName() : attribute.fieldName()); new GeneratedProperty(getName, setName, targetName, attributeName) .setNullable(isNullable) .setReadOnly(entity.isImmutable()) .setUseMethod(useGetter) .build(builder); // additional primitive get/set if the type is primitive if (propertyClass != Property.class) { TypeName primitiveType = TypeName.get(attribute.typeMirror()); String name = Names.upperCaseFirst(attribute.typeMirror().toString()); new GeneratedProperty(getName, setName, targetName, primitiveType) .setMethodSuffix(name) .setReadOnly(entity.isImmutable()) .setUseMethod(useGetter) .build(builder); } block.add(".setProperty($L)\n", builder.build()); block.add(".setPropertyName($S)\n", attribute.element().getSimpleName()); // property state get/set if (!entity.isStateless()) { ClassName stateClass = ClassName.get(PropertyState.class); TypeSpec.Builder stateType = TypeSpec.anonymousClassBuilder("") .addSuperinterface(parameterizedTypeName(Property.class, targetName, stateClass)); String fieldName = prefix + propertyStateFieldName(attribute); new GeneratedProperty(fieldName, targetName, stateClass).build(stateType); block.add(".setPropertyState($L)\n", stateType.build()); } if (!entity.isImmutable()) { return; } // if immutable add setter for the builder String propertyName = attribute.fieldName(); TypeName builderName = typeName; useSetter = false; String parameterSuffix = null; Optional<TypeMirror> builderType = entity.builderType(); if (builderType.isPresent()) { parameterSuffix = ".builder"; if (parent != null) { parameterSuffix = "." + parent.fieldName() + "Builder"; } propertyName = attribute.setterName(); useSetter = true; TypeElement element = elements.getTypeElement(builderType.get().toString()); if (element != null) { for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) { List<? extends VariableElement> parameters = method.getParameters(); String name = Names.removeMethodPrefixes(method.getSimpleName()); // probable setter for this attribute // (some builders have with<Property> setters so strip that if ((Names.matchesSetter("with", name)) || name.equalsIgnoreCase(attribute.fieldName()) && parameters.size() == 1) { propertyName = method.getSimpleName().toString(); break; } else if (Names.matchesSetter("set", name) || name.equalsIgnoreCase(attribute.fieldName()) && parameters.size() == 1) { propertyName = method.getSimpleName().toString(); break; } } } else { // special handling for immutables.org types // (note the below only handles the defaults) builder setter names not // prefixed, prefix boolean types with is if (ImmutableAnnotationKind.IMMUTABLE.isPresent(entity.element())) { propertyName = attribute.fieldName(); String getterName = attribute.getterName().replaceFirst("get", ""); if (attribute.typeMirror().getKind() == TypeKind.BOOLEAN) { propertyName = "is" + Names.upperCaseFirst(propertyName); } else if (Names.isAllUpper(getterName)) { propertyName = Names.lowerCaseFirst(getterName); } } } } propertyType = propertyName(propertyClass, builderName, attributeName); TypeSpec.Builder builderProperty = TypeSpec.anonymousClassBuilder("") .addSuperinterface(propertyType); new GeneratedProperty(propertyName, builderName, attributeName) .setWriteOnly(true) .setUseMethod(useSetter) .setAccessSuffix(parameterSuffix) .build(builderProperty); if (propertyClass != Property.class) { TypeName primitiveType = TypeName.get(attribute.typeMirror()); String name = Names.upperCaseFirst(attribute.typeMirror().toString()); new GeneratedProperty(propertyName, builderName, primitiveType) .setMethodSuffix(name) .setAccessSuffix(parameterSuffix) .setUseMethod(useSetter) .setWriteOnly(true) .build(builderProperty); } block.add(".setBuilderProperty($L)\n", builderProperty.build()); } private static ParameterizedTypeName propertyName(Class type, TypeName targetName, TypeName fieldName) { return type == Property.class ? ParameterizedTypeName.get(ClassName.get(type), targetName, fieldName) : ParameterizedTypeName.get(ClassName.get(type), targetName); } private static Class propertyClassFor(TypeMirror typeMirror) { if (typeMirror.getKind().isPrimitive()) { switch (typeMirror.getKind()) { case BOOLEAN: return BooleanProperty.class; case BYTE: return ByteProperty.class; case SHORT: return ShortProperty.class; case INT: return IntProperty.class; case LONG: return LongProperty.class; case FLOAT: return FloatProperty.class; case DOUBLE: return DoubleProperty.class; } } return Property.class; } private static String upperCaseUnderscoreRemovePrefixes(String name) { return Names.upperCaseUnderscore(Names.removeMemberPrefixes(name)); } }