From a1cfdde86162ca6d6d200c36cd3acd598021aa2a Mon Sep 17 00:00:00 2001 From: homedirectory Date: Tue, 29 Oct 2024 10:25:15 +0200 Subject: [PATCH] #2349 Correct generation of collectional properties This commit modifies the generation of collectional properties: * Property declaration includes `final` and an initialiser. * Property accessor returns an unmodifiable view. * Property setter clears and populates the property value. --- .../audit/AuditEntityGeneratorImpl.java | 14 +- .../com/fielden/platform/audit/JavaPoet.java | 12 +- .../fielden/platform/audit/PropertySpec.java | 167 ++++++++++++++---- 3 files changed, 146 insertions(+), 47 deletions(-) diff --git a/platform-dao/src/main/java/ua/com/fielden/platform/audit/AuditEntityGeneratorImpl.java b/platform-dao/src/main/java/ua/com/fielden/platform/audit/AuditEntityGeneratorImpl.java index 64fd2d41e3..7214a44674 100644 --- a/platform-dao/src/main/java/ua/com/fielden/platform/audit/AuditEntityGeneratorImpl.java +++ b/platform-dao/src/main/java/ua/com/fielden/platform/audit/AuditEntityGeneratorImpl.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -98,6 +99,7 @@ private void generateAuditEntity( // Collectional property to model one-to-many association with the ModProp entity final var modPropEntityProp = propertyBuilder("changedProps", ParameterizedTypeName.get(javaPoet.getClassName(Set.class), modPropTypeName)) .addAnnotation(AnnotationSpecs.title("Changed Properties", "Properties changed as part of an audit event.")) + .initializer("new $T<>()", javaPoet.getClassName(HashSet.class)) .build(); a3tBuilder.addProperty(auditedEntityProp); @@ -108,14 +110,14 @@ private void generateAuditEntity( .addModifiers(PUBLIC) .returns(javaPoet.getClassName(type)) .addAnnotation(javaPoet.getAnnotation(Override.class)) - .addStatement("return %s()".formatted(auditedEntityProp.getAccessorSpec().name)) + .addStatement("return %s()".formatted(auditedEntityProp.getAccessorSpec(environment).name)) .build()); a3tBuilder.addMethod(methodBuilder("setAuditedEntity") .addModifiers(PUBLIC) .addParameter(javaPoet.getClassName(type), "entity", FINAL) .returns(auditTypeClassName) .addAnnotation(javaPoet.getAnnotation(Override.class)) - .addStatement("return %s(%s)".formatted(auditedEntityProp.getSetterSpec(auditTypeClassName).name, "entity")) + .addStatement("return %s(%s)".formatted(auditedEntityProp.getSetterSpec(environment, auditTypeClassName).name, "entity")) .build()); // Audited properties @@ -193,9 +195,9 @@ public TypeSpec build(final Processor processor) { properties.stream() .map(prop -> processor.processProperty(this, prop)) .forEach(propSpec -> { - builder.addField(propSpec.toFieldSpec()); - builder.addMethod(propSpec.getAccessorSpec()); - builder.addMethod(propSpec.getSetterSpec(className)); + builder.addField(propSpec.toFieldSpec(environment)); + builder.addMethod(propSpec.getAccessorSpec(environment)); + builder.addMethod(propSpec.getSetterSpec(environment, className)); }); builder.addMethods(methods); return builder.build(); @@ -259,7 +261,7 @@ private void generateModPropEntity( .addAnnotation(javaPoet.getAnnotation(MapTo.class)) .build(); - PropertySpec.addProperties(builder, modPropTypeClassName, environment, auditEntityProp, pdProp); + PropertySpec.addProperties(environment, builder, modPropTypeClassName, auditEntityProp, pdProp); // Abstract methods in the base type diff --git a/platform-dao/src/main/java/ua/com/fielden/platform/audit/JavaPoet.java b/platform-dao/src/main/java/ua/com/fielden/platform/audit/JavaPoet.java index 92dd8f78cb..d2f2f403fc 100644 --- a/platform-dao/src/main/java/ua/com/fielden/platform/audit/JavaPoet.java +++ b/platform-dao/src/main/java/ua/com/fielden/platform/audit/JavaPoet.java @@ -4,7 +4,6 @@ import com.squareup.javapoet.ClassName; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; -import ua.com.fielden.platform.reflection.ClassesRetriever; import javax.annotation.Nullable; import java.lang.annotation.Annotation; @@ -71,10 +70,15 @@ class $ { } else { return switch (typeName) { - // TODO findClass will throw if a class is not found. Introduce ClassesRetriever.findClassOrNull. - case ClassName className -> ClassesRetriever.findClass(className.reflectionName()); + case ClassName className -> { + try { + yield Class.forName(className.reflectionName(), false, JavaPoet.class.getClassLoader()); + } catch (final ClassNotFoundException e) { + yield null; + } + } case ParameterizedTypeName paramTypeName -> reflectType(paramTypeName.rawType); - default -> throw new UnsupportedOperationException("Type name [%s] cannot be converted to a reflected type."); + default -> throw new UnsupportedOperationException("Type name [%s] cannot be reflected."); }; } } diff --git a/platform-dao/src/main/java/ua/com/fielden/platform/audit/PropertySpec.java b/platform-dao/src/main/java/ua/com/fielden/platform/audit/PropertySpec.java index b408738b85..4dba50e77a 100644 --- a/platform-dao/src/main/java/ua/com/fielden/platform/audit/PropertySpec.java +++ b/platform-dao/src/main/java/ua/com/fielden/platform/audit/PropertySpec.java @@ -5,12 +5,14 @@ import org.apache.commons.lang3.StringUtils; import ua.com.fielden.platform.entity.annotation.IsProperty; import ua.com.fielden.platform.entity.annotation.Observable; +import ua.com.fielden.platform.entity.exceptions.InvalidArgumentException; +import ua.com.fielden.platform.utils.EntityUtils; +import javax.annotation.Nullable; import java.lang.annotation.Annotation; import java.lang.reflect.Type; -import java.util.Collection; -import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.function.BiConsumer; import static javax.lang.model.element.Modifier.*; @@ -19,15 +21,17 @@ final class PropertySpec { private final String name; private final TypeName typeName; private final List annotations; + private final @Nullable CodeBlock initializer; - private PropertySpec(final String name, final TypeName typeName, final Iterable annotations) { + private PropertySpec(final String name, final TypeName typeName, @Nullable final CodeBlock initializer, final Iterable annotations) { this.name = name; this.typeName = typeName; + this.initializer = initializer; this.annotations = ImmutableList.copyOf(annotations); } - private PropertySpec(final String name, final TypeName typeName) { - this(name, typeName, ImmutableList.of()); + private PropertySpec(final String name, final TypeName typeName, final CodeBlock initializer) { + this(name, typeName, initializer, ImmutableList.of()); } public static Builder propertyBuilder(final String name, final TypeName type) { @@ -41,23 +45,34 @@ public static Builder propertyBuilder(final String name, final Type type) { /** * Adds the specified property, along with its accessor and setter, to the specified builder. * - * @param typeName name of the type being built (whether this is true is not checked by this method) + * @param typeName name of the type being built (whether this is true is not checked by this method) */ - public static TypeSpec.Builder addProperty(final TypeSpec.Builder builder, final TypeName typeName, final PropertySpec propertySpec) { - return builder.addField(propertySpec.toFieldSpec()) - .addMethod(propertySpec.getAccessorSpec()) - .addMethod(propertySpec.getSetterSpec(typeName)); + public static TypeSpec.Builder addProperty( + final GeneratorEnvironment environment, + final TypeSpec.Builder builder, + final TypeName typeName, + final PropertySpec propertySpec) + { + return builder.addField(propertySpec.toFieldSpec(environment)) + .addMethod(propertySpec.getAccessorSpec(environment)) + .addMethod(propertySpec.getSetterSpec(environment, typeName)); } /** * Adds the specified properties, along with their accessors and setters, to the specified builder. * - * @param typeName name of the type being built (whether this is true is not checked by this method) + * @param typeName name of the type being built (whether this is true is not checked by this method) */ - public static TypeSpec.Builder addProperties(final TypeSpec.Builder builder, final TypeName typeName, final PropertySpec propertySpec, final PropertySpec... propertySpecs) { - addProperty(builder, typeName, propertySpec); + public static TypeSpec.Builder addProperties( + final GeneratorEnvironment environment, + final TypeSpec.Builder builder, + final TypeName typeName, + final PropertySpec propertySpec, + final PropertySpec... propertySpecs) + { + addProperty(environment, builder, typeName, propertySpec); for (final var spec : propertySpecs) { - addProperty(builder, typeName, spec); + addProperty(environment, builder, typeName, spec); } return builder; } @@ -66,38 +81,70 @@ public Builder toBuilder() { return new Builder(name, typeName, annotations); } - public FieldSpec toFieldSpec() { - return FieldSpec.builder(typeName, name, PRIVATE) - // TODO IsProperty.value() for collectional properties - .addAnnotation(IsProperty.class) - .addAnnotations(annotations) - .build(); + public FieldSpec toFieldSpec(final GeneratorEnvironment environment) { + final var builder = FieldSpec.builder(typeName, name, PRIVATE) + .addAnnotations(annotations); + + if (initializer != null) { + builder.initializer(initializer); + } + + ifCollectionalOrElse(environment, + (collTypeName, eltTypeName) -> { + if (initializer == null) { + throw new IllegalStateException("Collectional property must have an initialiser."); + } + + builder.addAnnotation(AnnotationSpec.builder(IsProperty.class) + .addMember("value", "$T.class", eltTypeName) + .build()) + .addModifiers(FINAL); + }, + () -> builder.addAnnotation(environment.javaPoet().getAnnotation(IsProperty.class))); + + return builder.build(); } - public MethodSpec getAccessorSpec() { - // TODO Special body for collectional properties + public MethodSpec getAccessorSpec(final GeneratorEnvironment environment) { final String prefix = TypeName.BOOLEAN.equals(typeName) ? "is" : "get"; - return MethodSpec.methodBuilder(prefix + StringUtils.capitalize(name)) + final var builder = MethodSpec.methodBuilder(prefix + StringUtils.capitalize(name)) .addModifiers(PUBLIC) - .returns(typeName) - .addStatement("return this.%s".formatted(name)) - .build(); + .returns(typeName); + + ifCollectionalOrElse(environment, + (collTypeName, eltTypeName) -> { + final var collectionsMethodName = switch (CollectionType.fromName(collTypeName.canonicalName())) { + case LIST -> "unmodifiableList"; + case SET -> "unmodifiableSet"; + }; + builder.addStatement("return $T.%s(this.%s)".formatted(collectionsMethodName, name), Collections.class); + }, + () -> builder.addStatement("return this.%s".formatted(name))); + + return builder.build(); } - public MethodSpec getSetterSpec(final TypeName declaringClassName) { - // TODO Special body for collectional properties - return MethodSpec.methodBuilder("set" + StringUtils.capitalize(name)) + public MethodSpec getSetterSpec(final GeneratorEnvironment environment, final TypeName declaringClassName) { + final var builder = MethodSpec.methodBuilder("set" + StringUtils.capitalize(name)) .addModifiers(PUBLIC) .returns(declaringClassName) .addAnnotation(Observable.class) - .addParameter(typeName, name, FINAL) - .addStatement("this.%s = %s".formatted(name, name)) - .addStatement("return this") - .build(); + .addParameter(typeName, name, FINAL); + + ifCollectionalOrElse(environment, + (collTypeName, eltTypeName) -> builder + .addStatement("this.%s.clear()".formatted(name)) + .addStatement("this.%s.addAll(%s)".formatted(name, name)) + .addStatement("return this"), + () -> builder + .addStatement("this.%s = %s".formatted(name, name)) + .addStatement("return this")); + + return builder.build(); } - public MethodSpec getSetterSpec(final Type declaringType) { - return getSetterSpec(TypeName.get(declaringType)); + public MethodSpec getSetterSpec(final GeneratorEnvironment environment, final Type declaringType) { + return getSetterSpec(environment, TypeName.get(declaringType)); } public boolean hasAnnotation(final ClassName className) { @@ -138,11 +185,48 @@ public String toString() { "annotations=" + annotations + ']'; } + private void ifCollectionalOrElse( + final GeneratorEnvironment environment, + final BiConsumer collectionalAction, + final Runnable elseAction) + { + if (typeName instanceof ParameterizedTypeName paramTypeName && paramTypeName.typeArguments.size() == 1) { + Optional.ofNullable(environment.javaPoet().reflectType(paramTypeName.rawType)) + .filter(EntityUtils::isCollectional) + .ifPresentOrElse($ -> collectionalAction.accept(paramTypeName.rawType, paramTypeName.typeArguments.getFirst()), + elseAction); + } else { + elseAction.run(); + } + } + + private enum CollectionType { + LIST (List.class.getCanonicalName()), + SET (Set.class.getCanonicalName()); + + public final String canonicalName; + + CollectionType(final String canonicalName) { + this.canonicalName = canonicalName; + } + + public static CollectionType fromName(final CharSequence name) { + for (final var collType : CollectionType.values()) { + if (collType.canonicalName.contentEquals(name)) { + return collType; + } + } + throw new InvalidArgumentException("Unrecognised collection type: %s".formatted(name)); + }; + } + + static final class Builder { private final String name; private final TypeName type; private final ImmutableList.Builder annotations; + private @Nullable CodeBlock initializer; private Builder(final String name, final TypeName type, final Iterable annotations) { this.name = name; @@ -164,8 +248,17 @@ public Builder addAnnotation(final Class annotation) { return addAnnotation(ClassName.get(annotation)); } + public Builder initializer(final CodeBlock codeBlock) { + this.initializer = codeBlock; + return this; + } + + public Builder initializer(final String format, final Object... args) { + return initializer(CodeBlock.of(format, args)); + } + public PropertySpec build() { - return new PropertySpec(name, type, annotations.build()); + return new PropertySpec(name, type, initializer, annotations.build()); } }