Scalar type querybeans generics error (#2082)

* Add failing test case for generic querybeans

Types with generics arguments have not been processed correctly, they
will lose their generics. This is not critical in Java, as the language
has to be backwards compatible with Java 1.4, but will still become
highlighted as a warning in all common IDEs.

In Kotlin however, it's not a warning but a compile error.

* Fix problem with generics in scalar type arg

This fixes the issue introduced with ScalarType aware querybeans.
This commit is contained in:
Snōwball
2020-10-16 09:57:32 +13:00
committed by GitHub
parent ee73fd47d5
commit bcc8040afe
27 changed files with 427 additions and 143 deletions
+2 -2
View File
@@ -48,14 +48,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddlgen</artifactId>
<version>12.5.0</version>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>12.5.0</version>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
@@ -5,6 +5,9 @@ import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.validation.constraints.Size;
import org.example.domain.otherpackage.GenericType;
import org.example.domain.otherpackage.GenericTypeArgument;
/**
* Address entity bean.
*/
@@ -24,8 +27,10 @@ public class Address extends BaseModel {
@ManyToOne
Country country;
GenericType<GenericTypeArgument> metadata;
/**
* Create a copy of the address. Used to provide a 'snapshot' of
* Create a copy of the address. Used to provide a 'snapshot' of
* the shippingAddress for a give order.
*/
public Address createCopy() {
@@ -36,7 +41,7 @@ public class Address extends BaseModel {
copy.setCountry(country);
return copy;
}
public String toString() {
return id + " " + line1 + " " + line2 + " " + city + " " + country;
}
@@ -0,0 +1,17 @@
package org.example.domain.otherpackage;
public class GenericType<T> {
private T data;
public GenericType(final T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(final T data) {
this.data = data;
}
}
@@ -0,0 +1,6 @@
package org.example.domain.otherpackage;
public enum GenericTypeArgument {
FOO,
BAR;
}
@@ -0,0 +1,17 @@
package org.example.domain.otherpackage;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
@Converter
public class GenericTypeAttributeConverter implements AttributeConverter<GenericType<GenericTypeArgument>, String> {
@Override
public String convertToDatabaseColumn(final GenericType<GenericTypeArgument> attribute) {
return attribute.getData().name();
}
@Override
public GenericType<GenericTypeArgument> convertToEntityAttribute(final String dbData) {
return new GenericType<>(GenericTypeArgument.valueOf(dbData));
}
}
+109 -1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
@@ -9,6 +10,10 @@
<artifactId>kotlin-querybean-generator</artifactId>
<properties>
<kotlin.version>1.4.10</kotlin.version>
</properties>
<dependencies>
<dependency>
@@ -17,16 +22,100 @@
<version>3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddlgen</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
<execution>
<id>test-kapt</id>
<goals>
<goal>test-kapt</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/test/kotlin</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>${project.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<executions>
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>java-test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<source>1.8</source>
<target>1.8</target>
@@ -34,6 +123,25 @@
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>12.5.0</version>
<executions>
<execution>
<id>test</id>
<phase>process-test-classes</phase>
<configuration>
<transformArgs>debug=1</transformArgs>
</configuration>
<goals>
<goal>testEnhance</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -333,9 +333,9 @@ class ProcessingContext implements Constants {
return result;
} else {
if (typeInstanceOf(typeMirror, "java.lang.Comparable")) {
return new PropertyTypeScalarComparable(typeDef(typeMirror));
return new PropertyTypeScalarComparable(typeMirror.toString());
} else {
return new PropertyTypeScalar(typeDef(typeMirror));
return new PropertyTypeScalar(typeMirror.toString());
}
}
}
@@ -1,17 +1,18 @@
package io.ebean.querybean.generator;
import java.util.Map.Entry;
import java.util.Set;
/**
* Property type for associated beans (OneToMany, ManyToOne etc).
* Property type for fields handled by ScalarTypes
*/
class PropertyTypeScalar extends PropertyType {
/**
* The package name for this associated query bean.
*/
private final String assocPackage;
private final String attributeSimpleName;
private final Set<String> assocImports;
private final String attributeCompleteSignature;
/**
* Construct given the associated bean type name and package.
@@ -19,20 +20,26 @@ class PropertyTypeScalar extends PropertyType {
* @param attributeClass the type in the database bean that will be serialized via ScalarType
*/
PropertyTypeScalar(String attributeClass) {
super("PScalar");
int split = attributeClass.lastIndexOf('.');
this.assocPackage = attributeClass.substring(0, split);
this.attributeSimpleName = attributeClass.substring(split + 1);
this("PScalar", attributeClass);
}
protected PropertyTypeScalar(String propertyType, String attributeClass) {
super(propertyType);
final Entry<String, Set<String>> signature = Split.genericsSplit(attributeClass);
this.attributeCompleteSignature = signature.getKey();
this.assocImports = signature.getValue();
}
@Override
String getTypeDefn(String shortName, boolean assoc) {
if (assoc) {
// PScalarType<R, PhoneNumber>
return "PScalar<R, " + attributeSimpleName + ">";
return propertyType + "<R, " + attributeCompleteSignature + ">";
} else {
// PScalarType<QCustomer, PhoneNumber>
return "PScalar<Q" + shortName + ", " + attributeSimpleName + ">";
return propertyType + "<Q" + shortName + ", " + attributeCompleteSignature + ">";
}
}
@@ -42,7 +49,7 @@ class PropertyTypeScalar extends PropertyType {
@Override
void addImports(Set<String> allImports) {
super.addImports(allImports);
allImports.add(assocPackage + "." + attributeSimpleName);
allImports.addAll(assocImports);
}
}
@@ -1,17 +1,9 @@
package io.ebean.querybean.generator;
import java.util.Set;
/**
* Property type for associated beans (OneToMany, ManyToOne etc).
* Property type for fields handled by ScalarTypes
*/
class PropertyTypeScalarComparable extends PropertyType {
/**
* The package name for this associated query bean.
*/
private final String assocPackage;
private final String attributeSimpleName;
class PropertyTypeScalarComparable extends PropertyTypeScalar {
/**
* Construct given the associated bean type name and package.
@@ -19,30 +11,6 @@ class PropertyTypeScalarComparable extends PropertyType {
* @param attributeClass the type in the database bean that will be serialized via ScalarType
*/
PropertyTypeScalarComparable(String attributeClass) {
super("PScalarComparable");
int split = attributeClass.lastIndexOf('.');
this.assocPackage = attributeClass.substring(0, split);
this.attributeSimpleName = attributeClass.substring(split + 1);
super("PScalarComparable", attributeClass);
}
@Override
String getTypeDefn(String shortName, boolean assoc) {
if (assoc) {
// PScalarType<R, PhoneNumber>
return "PScalarComparable<R, " + attributeSimpleName + ">";
} else {
// PScalarType<QCustomer, PhoneNumber>
return "PScalarComparable<Q" + shortName + ", " + attributeSimpleName + ">";
}
}
/**
* All required imports to the allImports set.
*/
@Override
void addImports(Set<String> allImports) {
super.addImports(allImports);
allImports.add(assocPackage + "." + attributeSimpleName);
}
}
@@ -1,5 +1,11 @@
package io.ebean.querybean.generator;
import java.util.AbstractMap.SimpleEntry;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
/**
* Helper for splitting package and class name.
*/
@@ -41,4 +47,20 @@ class Split {
return fullType;
}
static Entry<String, Set<String>> genericsSplit(String signature) {
StringBuilder simpleSignature = new StringBuilder();
final StringTokenizer tokenizer = new StringTokenizer(signature, ",<> ", true);
Set<String> assocImports = new HashSet<>();
while (tokenizer.hasMoreTokens()) {
final String token = tokenizer.nextToken();
if (token.length() == 1 && ",<> ".indexOf(token.charAt(0)) >= 0) {
simpleSignature.append(token);
} else {
simpleSignature.append(Split.shortName(token));
assocImports.add(token);
}
}
return new SimpleEntry<>(simpleSignature.toString(), assocImports);
}
}
@@ -1,40 +0,0 @@
package io.ebean.querybean.generator;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
public class SplitTest {
@Test
public void trimType() {
assertEquals(Split.trimType("com.foo.domain.Customer"), "com.foo.domain.Customer");
assertEquals(Split.trimType("? extends com.foo.domain.Customer"), "com.foo.domain.Customer");
}
@Test
public void shortName() {
assertEquals(Split.shortName("com.foo.domain.Customer"), "Customer");
assertEquals(Split.shortName("Customer"), "Customer");
}
@Test
public void split_normal() {
String[] split = Split.split("com.foo.domain.Customer");
assertEquals(split[0], "com.foo.domain");
assertEquals(split[1], "Customer");
}
@Test
public void split_noPackage() {
String[] split = Split.split("Customer");
assertNull(split[0]);
assertEquals(split[1], "Customer");
}
}
@@ -0,0 +1,47 @@
package io.ebean.querybean.generator
import org.testng.Assert.assertEquals
import org.testng.Assert.assertNull
import org.testng.annotations.Test
import java.lang.Exception
import java.util.Arrays
import java.util.HashSet
class SplitTest {
@Test
fun trimType() {
assertEquals(Split.trimType("com.foo.domain.Customer"), "com.foo.domain.Customer")
assertEquals(Split.trimType("? extends com.foo.domain.Customer"), "com.foo.domain.Customer")
}
@Test
fun shortName() {
assertEquals(Split.shortName("com.foo.domain.Customer"), "Customer")
assertEquals(Split.shortName("Customer"), "Customer")
}
@Test
fun split_normal() {
val split = Split.split("com.foo.domain.Customer")
assertEquals(split[0], "com.foo.domain")
assertEquals(split[1], "Customer")
}
@Test
fun split_noPackage() {
val split = Split.split("Customer")
assertNull(split[0])
assertEquals(split[1], "Customer")
}
@Test
@Throws(Exception::class)
fun split_generics() {
assertEquals("Foo<Bar, XFoo<XBar>>", Split.genericsSplit("com.Foo<com.Bar, org.XFoo<org.XBar>>").key)
assertEquals(
HashSet(Arrays.asList("com.Foo", "com.Bar", "org.XFoo", "org.XBar")),
Split.genericsSplit("com.Foo<com.Bar, org.XFoo<org.XBar>>").value
)
assertEquals("Foo", Split.genericsSplit("com.bar.Foo").key)
}
}
@@ -0,0 +1,24 @@
package org.example.domain;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.validation.constraints.Size;
import org.example.otherpackage.GenericType;
import org.example.otherpackage.GenericTypeArgument;
/**
* Address entity bean.
*/
@Entity
@Table(name = "o_address")
class Address(
@Size(max = 100)
var line1: String,
@Size(max = 100)
var line2: String,
@Size(max = 100)
var city: String,
// Dummy metadata field just to test generation
val metadata: GenericType<GenericTypeArgument>
) : BaseModel()
@@ -0,0 +1,26 @@
package org.example.domain;
import java.time.Instant;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
import io.ebean.Model;
import io.ebean.annotation.WhenCreated;
import io.ebean.annotation.WhenModified;
@MappedSuperclass
abstract class BaseModel : Model() {
@Id
val id: Long? = null
@Version
val version: Int = 0
@WhenCreated
val whenCreated: Instant? = null
@WhenModified
val whenModified: Instant? = null
}
@@ -0,0 +1,3 @@
package org.example.otherpackage
interface Email<T> : Comparable<T>
@@ -0,0 +1,5 @@
package org.example.otherpackage
class GenericType<T>(
val data: T
)
@@ -0,0 +1,6 @@
package org.example.otherpackage
enum class GenericTypeArgument {
FOO,
BAR,
}
@@ -0,0 +1,15 @@
package org.example.otherpackage
import javax.persistence.AttributeConverter
import javax.persistence.Converter
@Converter
class GenericTypeAttributeConverter : AttributeConverter<GenericType<GenericTypeArgument>, String> {
override fun convertToDatabaseColumn(attribute: GenericType<GenericTypeArgument>): String {
return attribute.data.name
}
override fun convertToEntityAttribute(dbData: String): GenericType<GenericTypeArgument> {
return GenericType(GenericTypeArgument.valueOf(dbData))
}
}
@@ -0,0 +1,15 @@
package org.example.otherpackage
import javax.persistence.AttributeConverter
import javax.persistence.Converter
@Converter
class PhoneAttributeConverter : AttributeConverter<PhoneNumber, String> {
override fun convertToDatabaseColumn(attribute: PhoneNumber): String {
return attribute.msisdn
}
override fun convertToEntityAttribute(dbData: String): PhoneNumber {
return PhoneNumber(dbData)
}
}
@@ -0,0 +1,5 @@
package org.example.otherpackage
class PhoneNumber(
val msisdn: String
)
@@ -0,0 +1,9 @@
package org.example.otherpackage
class ValidEmail(
val emailAddress: String
) : Email<ValidEmail> {
override fun compareTo(other: ValidEmail): Int {
return emailAddress.compareTo(other.emailAddress)
}
}
@@ -0,0 +1,15 @@
package org.example.otherpackage
import javax.persistence.AttributeConverter
import javax.persistence.Converter
@Converter
class ValidEmailAttributeConverter : AttributeConverter<ValidEmail, String> {
override fun convertToDatabaseColumn(attribute: ValidEmail): String {
return attribute.emailAddress
}
override fun convertToEntityAttribute(dbData: String): ValidEmail {
return ValidEmail(dbData)
}
}
@@ -298,9 +298,9 @@ class ProcessingContext implements Constants {
return result;
} else {
if (typeInstanceOf(typeMirror, "java.lang.Comparable")) {
return new PropertyTypeScalarComparable(typeDef(typeMirror));
return new PropertyTypeScalarComparable(typeMirror.toString());
} else {
return new PropertyTypeScalar(typeDef(typeMirror));
return new PropertyTypeScalar(typeMirror.toString());
}
}
}
@@ -1,17 +1,17 @@
package io.ebean.querybean.generator;
import java.util.Map.Entry;
import java.util.Set;
/**
* Property type for associated beans (OneToMany, ManyToOne etc).
* Property type for fields handled by ScalarTypes
*/
class PropertyTypeScalar extends PropertyType {
/**
* The package name for this associated query bean.
*/
private final String assocPackage;
private final String attributeSimpleName;
private final Set<String> assocImports;
private final String attributeCompleteSignature;
/**
* Construct given the associated bean type name and package.
@@ -19,20 +19,26 @@ class PropertyTypeScalar extends PropertyType {
* @param attributeClass the type in the database bean that will be serialized via ScalarType
*/
PropertyTypeScalar(String attributeClass) {
super("PScalar");
int split = attributeClass.lastIndexOf('.');
this.assocPackage = attributeClass.substring(0, split);
this.attributeSimpleName = attributeClass.substring(split + 1);
this("PScalar", attributeClass);
}
protected PropertyTypeScalar(String propertyType, String attributeClass) {
super(propertyType);
final Entry<String, Set<String>> signature = Split.genericsSplit(attributeClass);
this.attributeCompleteSignature = signature.getKey();
this.assocImports = signature.getValue();
}
@Override
String getTypeDefn(String shortName, boolean assoc) {
if (assoc) {
// PScalarType<R, PhoneNumber>
return "PScalar<R, " + attributeSimpleName + ">";
return propertyType + "<R, " + attributeCompleteSignature + ">";
} else {
// PScalarType<QCustomer, PhoneNumber>
return "PScalar<Q" + shortName + ", " + attributeSimpleName + ">";
return propertyType + "<Q" + shortName + ", " + attributeCompleteSignature + ">";
}
}
@@ -42,7 +48,6 @@ class PropertyTypeScalar extends PropertyType {
@Override
void addImports(Set<String> allImports) {
super.addImports(allImports);
allImports.add(assocPackage + "." + attributeSimpleName);
allImports.addAll(assocImports);
}
}
@@ -1,17 +1,9 @@
package io.ebean.querybean.generator;
import java.util.Set;
/**
* Property type for associated beans (OneToMany, ManyToOne etc).
*/
class PropertyTypeScalarComparable extends PropertyType {
/**
* The package name for this associated query bean.
*/
private final String assocPackage;
private final String attributeSimpleName;
class PropertyTypeScalarComparable extends PropertyTypeScalar {
/**
* Construct given the associated bean type name and package.
@@ -19,30 +11,6 @@ class PropertyTypeScalarComparable extends PropertyType {
* @param attributeClass the type in the database bean that will be serialized via ScalarType
*/
PropertyTypeScalarComparable(String attributeClass) {
super("PScalarComparable");
int split = attributeClass.lastIndexOf('.');
this.assocPackage = attributeClass.substring(0, split);
this.attributeSimpleName = attributeClass.substring(split + 1);
super("PScalarComparable", attributeClass);
}
@Override
String getTypeDefn(String shortName, boolean assoc) {
if (assoc) {
// PScalarType<R, PhoneNumber>
return "PScalarComparable<R, " + attributeSimpleName + ">";
} else {
// PScalarType<QCustomer, PhoneNumber>
return "PScalarComparable<Q" + shortName + ", " + attributeSimpleName + ">";
}
}
/**
* All required imports to the allImports set.
*/
@Override
void addImports(Set<String> allImports) {
super.addImports(allImports);
allImports.add(assocPackage + "." + attributeSimpleName);
}
}
@@ -1,5 +1,11 @@
package io.ebean.querybean.generator;
import java.util.AbstractMap.SimpleEntry;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
/**
* Helper for splitting package and class name.
*/
@@ -31,4 +37,21 @@ class Split {
return className.substring(startPos + 1);
}
static Entry<String, Set<String>> genericsSplit(String signature) {
StringBuilder simpleSignature = new StringBuilder();
final StringTokenizer tokenizer = new StringTokenizer(signature, ",<> ", true);
Set<String> assocImports = new HashSet<>();
while (tokenizer.hasMoreTokens()) {
final String token = tokenizer.nextToken();
if (token.length() == 1 && ",<> ".indexOf(token.charAt(0)) >= 0) {
simpleSignature.append(token);
} else {
simpleSignature.append(Split.shortName(token));
assocImports.add(token);
}
}
return new SimpleEntry<>(simpleSignature.toString(), assocImports);
}
}
@@ -1,10 +1,13 @@
package io.ebean.querybean.generator;
import org.testng.annotations.Test;
import static java.util.Arrays.asList;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
import java.util.HashSet;
import org.testng.annotations.Test;
public class SplitTest {
@Test
@@ -32,4 +35,9 @@ public class SplitTest {
assertEquals(split[1], "Customer");
}
}
@Test
public void split_generics() throws Exception {
assertEquals("Foo<Bar, XFoo<XBar>>", Split.genericsSplit("com.Foo<com.Bar, org.XFoo<org.XBar>>").getKey());
assertEquals(new HashSet<>(asList("com.Foo", "com.Bar", "org.XFoo", "org.XBar")), Split.genericsSplit("com.Foo<com.Bar, org.XFoo<org.XBar>>").getValue());
}
}