Skip to content

Commit

Permalink
#2349 Correct generation of collectional properties
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
homedirectory committed Oct 29, 2024
1 parent f27224c commit a1cfdde
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.");
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

Expand All @@ -19,15 +21,17 @@ final class PropertySpec {
private final String name;
private final TypeName typeName;
private final List<AnnotationSpec> annotations;
private final @Nullable CodeBlock initializer;

private PropertySpec(final String name, final TypeName typeName, final Iterable<? extends AnnotationSpec> annotations) {
private PropertySpec(final String name, final TypeName typeName, @Nullable final CodeBlock initializer, final Iterable<? extends AnnotationSpec> 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) {
Expand All @@ -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;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -138,11 +185,48 @@ public String toString() {
"annotations=" + annotations + ']';
}

private void ifCollectionalOrElse(
final GeneratorEnvironment environment,
final BiConsumer<? super ClassName, ? super TypeName> 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<AnnotationSpec> annotations;
private @Nullable CodeBlock initializer;

private Builder(final String name, final TypeName type, final Iterable<? extends AnnotationSpec> annotations) {
this.name = name;
Expand All @@ -164,8 +248,17 @@ public Builder addAnnotation(final Class<? extends Annotation> 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());
}

}
Expand Down

0 comments on commit a1cfdde

Please sign in to comment.