Skip to content

Commit ebc90e0

Browse files
FEATURE: Add support for Generic mapped superclass (#3692)
* creates support for generics in mapped superclasses * Changes DeployCreateProperties to allow processing of inheritance hierarchies with generics * adds tests for ebean-querybean * bumps querybean-generator version * fixes tests by changing table names for ProductWithGenericLong and ProductWithGenericString * Restore format, this reduces the diff * Restore format, this reduces the diff --------- Co-authored-by: Rob Bygrave <robin.bygrave@gmail.com>
1 parent c4230c7 commit ebc90e0

File tree

7 files changed

+470
-21
lines changed

7 files changed

+470
-21
lines changed

ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
import jakarta.persistence.*;
1313
import java.lang.reflect.*;
14+
import java.util.HashMap;
15+
import java.util.Map;
1416

1517
import static java.lang.System.Logger.Level.*;
1618

@@ -35,7 +37,7 @@ public DeployCreateProperties(TypeManager typeManager) {
3537
* Create the appropriate properties for a bean.
3638
*/
3739
public void createProperties(DeployBeanDescriptor<?> desc) {
38-
createProperties(desc, desc.getBeanType(), 0);
40+
createProperties(desc, desc.getBeanType(), 0, new HashMap<>());
3941
desc.sortProperties();
4042
}
4143

@@ -64,7 +66,7 @@ private boolean ignoreField(Field field) {
6466
* properties the bean properties from Class. Some of these properties may not map to database
6567
* columns.
6668
*/
67-
private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, int level) {
69+
private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, int level, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
6870
if (beanType.equals(Model.class)) {
6971
// ignore all fields on model (_$dbName)
7072
return;
@@ -74,7 +76,7 @@ private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, i
7476
for (int i = 0; i < fields.length; i++) {
7577
Field field = fields[i];
7678
if (!ignoreField(field)) {
77-
DeployBeanProperty prop = createProp(desc, field, beanType);
79+
DeployBeanProperty prop = createProp(desc, field, beanType, genericTypeMap);
7880
if (prop != null) {
7981
// set a order that gives priority to inherited properties
8082
// push Id/EmbeddedId up and CreatedTimestamp/UpdatedTimestamp down
@@ -95,7 +97,7 @@ private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, i
9597
if (!superClass.equals(Object.class)) {
9698
// recursively add any properties in the inheritance hierarchy
9799
// up to the Object.class level...
98-
createProperties(desc, superClass, level + 1);
100+
createProperties(desc, superClass, level + 1, mapGenerics(beanType));
99101
}
100102
} catch (PersistenceException ex) {
101103
throw ex;
@@ -116,8 +118,10 @@ private DeployBeanProperty createManyType(DeployBeanDescriptor<?> desc, Class<?>
116118
return new DeployBeanPropertyAssocMany<>(desc, targetType, manyType);
117119
}
118120

119-
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field) {
120-
Class<?> propertyType = field.getType();
121+
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
122+
Class<?> propertyType = field.getGenericType() instanceof TypeVariable<?>
123+
? genericTypeMap.get(field.getGenericType())
124+
: field.getType();
121125
if (isSpecialScalarType(field)) {
122126
return new DeployBeanProperty(desc, propertyType, field.getGenericType());
123127
}
@@ -172,8 +176,8 @@ private boolean isTransientField(Field field) {
172176
return AnnotationUtil.has(field, Transient.class);
173177
}
174178

175-
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Class<?> beanType) {
176-
DeployBeanProperty prop = createProp(desc, field);
179+
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Class<?> beanType, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
180+
DeployBeanProperty prop = createProp(desc, field, genericTypeMap);
177181
if (prop == null) {
178182
// transient annotation on unsupported type
179183
return null;
@@ -224,4 +228,40 @@ private Class<?> determineTargetType(Field field) {
224228
// if targetType is null, then must be set in annotations
225229
return null;
226230
}
231+
232+
private Map<TypeVariable<?>, Class<?>> mapGenerics(Class<?> clazz) {
233+
Type genericSuperclass = clazz.getGenericSuperclass();
234+
if (!(genericSuperclass instanceof ParameterizedType)) {
235+
return new HashMap<>();
236+
}
237+
238+
ParameterizedType parameterized = (ParameterizedType) genericSuperclass;
239+
TypeVariable<?>[] typeVars = ((Class<?>) parameterized.getRawType()).getTypeParameters();
240+
Type[] actualTypes = parameterized.getActualTypeArguments();
241+
242+
Map<TypeVariable<?>, Class<?>> typeMap = new HashMap<>();
243+
for (int i = 0; i < typeVars.length; i++) {
244+
Type actual = actualTypes[i];
245+
Class<?> resolvedClass = resolveToClass(actual);
246+
if (resolvedClass != null) {
247+
typeMap.put(typeVars[i], resolvedClass);
248+
} else {
249+
// ignore
250+
}
251+
}
252+
return typeMap;
253+
}
254+
255+
private static Class<?> resolveToClass(Type type) {
256+
if (type instanceof Class<?>) {
257+
return (Class<?>) type;
258+
} else if (type instanceof ParameterizedType) {
259+
ParameterizedType pType = (ParameterizedType) type;
260+
Type raw = pType.getRawType();
261+
if (raw instanceof Class<?>) {
262+
return (Class<?>) raw;
263+
}
264+
}
265+
return null;
266+
}
227267
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.example.domain;
2+
3+
import io.ebean.Model;
4+
import io.ebean.annotation.WhenCreated;
5+
import io.ebean.annotation.WhenModified;
6+
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.MappedSuperclass;
9+
import jakarta.persistence.Version;
10+
import java.sql.Timestamp;
11+
12+
/**
13+
* Base domain object with Id, version, whenCreated and whenUpdated.
14+
*
15+
* <p>
16+
* Extending Model to enable the 'active record' style.
17+
*
18+
* <p>
19+
* whenCreated and whenUpdated are generally useful for maintaining external search services (like
20+
* elasticsearch) and audit.
21+
*/
22+
@MappedSuperclass
23+
public abstract class GenericBaseModel<T> extends Model {
24+
25+
@Id
26+
T id;
27+
28+
@Version
29+
Long version;
30+
31+
@WhenCreated
32+
Timestamp whenCreated;
33+
34+
@WhenModified
35+
Timestamp whenUpdated;
36+
37+
public T getId() {
38+
return id;
39+
}
40+
41+
public void setId(T id) {
42+
this.id = id;
43+
}
44+
45+
public Long getVersion() {
46+
return version;
47+
}
48+
49+
public void setVersion(Long version) {
50+
this.version = version;
51+
}
52+
53+
public Timestamp getWhenCreated() {
54+
return whenCreated;
55+
}
56+
57+
public void setWhenCreated(Timestamp whenCreated) {
58+
this.whenCreated = whenCreated;
59+
}
60+
61+
public Timestamp getWhenUpdated() {
62+
return whenUpdated;
63+
}
64+
65+
public void setWhenUpdated(Timestamp whenUpdated) {
66+
this.whenUpdated = whenUpdated;
67+
}
68+
69+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package org.example.domain;
2+
3+
import org.example.domain.ProductWithGenericLong;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.Table;
6+
import javax.validation.constraints.Size;
7+
8+
/**
9+
* Product entity bean.
10+
*/
11+
@Entity
12+
@Table(name = "long_product", schema = "foo")
13+
public class ProductWithGenericLong extends GenericBaseModel<Long> {
14+
15+
@Size(max = 20)
16+
String sku;
17+
18+
String name;
19+
20+
/**
21+
* Return sku.
22+
*/
23+
public String getSku() {
24+
return sku;
25+
}
26+
27+
/**
28+
* Set sku.
29+
*/
30+
public void setSku(String sku) {
31+
this.sku = sku;
32+
}
33+
34+
/**
35+
* Return name.
36+
*/
37+
public String getName() {
38+
return name;
39+
}
40+
41+
/**
42+
* Set name.
43+
*/
44+
public void setName(String name) {
45+
this.name = name;
46+
}
47+
48+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package org.example.domain;
2+
3+
import org.example.domain.ProductWithGenericLong;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.Table;
6+
import javax.validation.constraints.Size;
7+
8+
/**
9+
* Product entity bean.
10+
*/
11+
@Entity
12+
@Table(name = "string_product", schema = "foo")
13+
public class ProductWithGenericString extends GenericBaseModel<String> {
14+
15+
@Size(max = 20)
16+
String sku;
17+
18+
String name;
19+
20+
/**
21+
* Return sku.
22+
*/
23+
public String getSku() {
24+
return sku;
25+
}
26+
27+
/**
28+
* Set sku.
29+
*/
30+
public void setSku(String sku) {
31+
this.sku = sku;
32+
}
33+
34+
/**
35+
* Return name.
36+
*/
37+
public String getName() {
38+
return name;
39+
}
40+
41+
/**
42+
* Set name.
43+
*/
44+
public void setName(String name) {
45+
this.name = name;
46+
}
47+
48+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.querytest;
2+
3+
import io.ebean.InTuples;
4+
5+
import org.example.domain.ProductWithGenericLong;
6+
import org.example.domain.ProductWithGenericString;
7+
import org.example.domain.query.QContact;
8+
import org.example.domain.query.QProductWithGenericLong;
9+
import org.example.domain.query.QProductWithGenericString;
10+
import org.junit.jupiter.api.Test;
11+
12+
import java.time.ZonedDateTime;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
16+
public class QProductWithGenericTest {
17+
18+
@Test
19+
void findByLongId() {
20+
21+
var entity = new ProductWithGenericLong();
22+
entity.setId(42L);
23+
entity.setName("Gadget");
24+
entity.save();
25+
26+
var result = new QProductWithGenericLong()
27+
.id.eq(42L)
28+
.findOne();
29+
30+
assertThat(result).isNotNull();
31+
assertThat(result.getName()).isEqualTo("Gadget");
32+
}
33+
34+
@Test
35+
void findByStringId() {
36+
37+
var entity = new ProductWithGenericString();
38+
entity.setId("1234");
39+
entity.setName("Gadget");
40+
entity.save();
41+
42+
var result = new QProductWithGenericString()
43+
.id.eq("1234")
44+
.findOne();
45+
46+
assertThat(result).isNotNull();
47+
assertThat(result.getName()).isEqualTo("Gadget");
48+
}
49+
}

0 commit comments

Comments
 (0)