Skip to content

Commit f87d007

Browse files
authored
fix: jpa working again with external libraries (since entities are no… (#111)
* fix: jpa working again with external libraries (since entities are not in entity manager managed classes)
1 parent 8080727 commit f87d007

File tree

10 files changed

+137
-61
lines changed

10 files changed

+137
-61
lines changed

tzatziki-common/pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@
102102
<groupId>org.assertj</groupId>
103103
<artifactId>assertj-core</artifactId>
104104
</dependency>
105+
<dependency>
106+
<groupId>org.reflections</groupId>
107+
<artifactId>reflections</artifactId>
108+
<version>0.10.2</version>
109+
<exclusions>
110+
<exclusion>
111+
<artifactId>slf4j-api</artifactId>
112+
<groupId>org.slf4j</groupId>
113+
</exclusion>
114+
</exclusions>
115+
</dependency>
105116
<dependency>
106117
<groupId>com.google.guava</groupId>
107118
<artifactId>guava</artifactId>

tzatziki-common/src/main/java/com/decathlon/tzatziki/utils/TypeParser.java

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import lombok.SneakyThrows;
77
import lombok.extern.slf4j.Slf4j;
88
import org.jetbrains.annotations.NotNull;
9+
import org.reflections.Reflections;
10+
import org.reflections.scanners.Scanners;
11+
import org.reflections.util.ConfigurationBuilder;
912

1013
import java.lang.reflect.Type;
1114
import java.time.Instant;
@@ -23,13 +26,14 @@
2326
public class TypeParser {
2427

2528
private static final Pattern typePattern = Pattern.compile("((?:[a-z_$][a-z0-9_$]*\\.)*[A-Z_$][A-z0-9_$]*)(?:<(.*)>)?");
26-
private static List<ClassPath.ClassInfo> reflections;
29+
private static List<ClassPath.ClassInfo> allClasses;
30+
private static Reflections reflections;
2731
private static final Map<String, Type> KNOWN_TYPES = new LinkedHashMap<>();
28-
private static String defaultPackage = null;
32+
public static String defaultPackage = null;
2933

3034
public static void setDefaultPackage(String defaultPackage) {
3135
KNOWN_TYPES.clear();
32-
reflections = null;
36+
allClasses = null;
3337
TypeParser.defaultPackage = defaultPackage;
3438
}
3539

@@ -84,7 +88,7 @@ private static Type parseType(String name) {
8488
});
8589
}
8690

87-
public static boolean hasClass(String className){
91+
public static boolean hasClass(String className) {
8892
return classes().stream().anyMatch(clazz -> clazz.getName().equals(className) || clazz.getSimpleName().equals(className));
8993
}
9094

@@ -122,8 +126,8 @@ private static List<String> splitNames(String input) {
122126

123127
@SneakyThrows
124128
public static synchronized List<ClassPath.ClassInfo> classes() {
125-
if (reflections == null) {
126-
reflections = ClassPath.from(ClassLoader.getSystemClassLoader()).getAllClasses()
129+
if (allClasses == null) {
130+
allClasses = ClassPath.from(ClassLoader.getSystemClassLoader()).getAllClasses()
127131
.stream()
128132
.sorted((class1, class2) -> {
129133
if (defaultPackage != null) {
@@ -137,9 +141,15 @@ public static synchronized List<ClassPath.ClassInfo> classes() {
137141
}
138142
return class1.getPackageName().compareTo(class2.getPackageName());
139143
}
140-
)
141-
.collect(Collectors.toList());
144+
).collect(Collectors.toList());
145+
}
146+
return allClasses;
147+
}
148+
149+
public static synchronized <T> Set<Class<? extends T>> getSubtypesOf(Class<T> clazz) {
150+
if (reflections == null) {
151+
reflections = new Reflections(new ConfigurationBuilder().forPackage("").setScanners(Scanners.SubTypes));
142152
}
143-
return reflections;
153+
return reflections.getSubTypesOf(clazz);
144154
}
145-
}
155+
}

tzatziki-spring-jpa/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
<groupId>com.fasterxml.jackson.datatype</groupId>
4141
<artifactId>jackson-datatype-hibernate5</artifactId>
4242
</dependency>
43+
<dependency>
44+
<groupId>org.springframework.data</groupId>
45+
<artifactId>spring-data-relational</artifactId>
46+
</dependency>
4347

4448
<!-- test dependencies -->
4549
<dependency>

tzatziki-spring-jpa/src/main/java/com/decathlon/tzatziki/steps/SpringJPASteps.java

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@
1313
import org.springframework.jdbc.core.JdbcTemplate;
1414
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
1515

16-
import javax.persistence.Entity;
1716
import javax.persistence.Table;
18-
import javax.persistence.spi.PersistenceUnitInfo;
1917
import javax.sql.DataSource;
2018
import java.lang.reflect.Type;
2119
import java.lang.reflect.TypeVariable;
22-
import java.util.*;
23-
import java.util.function.Function;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Optional;
2423
import java.util.stream.Collectors;
2524
import java.util.stream.Stream;
2625
import java.util.stream.StreamSupport;
@@ -44,7 +43,8 @@ public class SpringJPASteps {
4443

4544
@Autowired(required = false)
4645
private List<LocalContainerEntityManagerFactoryBean> entityManagerFactories;
47-
private Map<String, Class<?>> entityClassByTableName;
46+
private Map<Type, CrudRepository<?, ?>> crudRepositoryByClass;
47+
private Map<String, Type> entityClassByTableName;
4848

4949
private boolean disableTriggers = true;
5050
private final ObjectSteps objects;
@@ -64,15 +64,41 @@ public void before() {
6464
});
6565
}
6666

67+
if (crudRepositoryByClass == null) {
68+
crudRepositoryByClass = spring.applicationContext().getBeansOfType(CrudRepository.class).values()
69+
.stream()
70+
.<Map.Entry<Class<?>, CrudRepository<?, ?>>>mapMulti((crudRepository, consumer) -> {
71+
Map<TypeVariable<?>, Type> typeArguments = TypeUtils.getTypeArguments(crudRepository.getClass(), CrudRepository.class);
72+
Type type = typeArguments.get(CrudRepository.class.getTypeParameters()[0]);
73+
74+
if (type instanceof TypeVariable<?> typeVariable) {
75+
type = typeVariable.getBounds()[0];
76+
TypeParser.getSubtypesOf((Class<?>) type)
77+
.forEach(clazz -> consumer.accept(Map.entry(clazz, crudRepository)));
78+
}
79+
80+
if (type instanceof Class<?> clazz) consumer.accept(Map.entry(clazz, crudRepository));
81+
})
82+
.collect(Collectors.toMap(
83+
Map.Entry::getKey,
84+
Map.Entry::getValue
85+
));
86+
}
6787
if (entityClassByTableName == null) {
68-
entityClassByTableName = Optional.ofNullable(entityManagerFactories).orElse(Collections.emptyList()).stream()
69-
.map(LocalContainerEntityManagerFactoryBean::getPersistenceUnitInfo)
70-
.map(PersistenceUnitInfo::getManagedClassNames)
71-
.flatMap(Collection::stream)
72-
.map(TypeParser::parse)
88+
entityClassByTableName = crudRepositoryByClass.keySet().stream()
7389
.map(type -> (Class<?>) type)
74-
.filter(entityClass -> entityClass.isAnnotationPresent(Table.class))
75-
.collect(Collectors.toMap(entityClass -> entityClass.getAnnotation(Table.class).name(), Function.identity()));
90+
.sorted((c1, c2) -> {
91+
if (TypeParser.defaultPackage == null) return 0;
92+
93+
if (c1.getPackageName().startsWith(TypeParser.defaultPackage)) return -1;
94+
95+
return c2.getPackageName().startsWith(TypeParser.defaultPackage) ? 1 : 0;
96+
})
97+
.<Map.Entry<String, Class<?>>>mapMulti((clazz, consumer) -> {
98+
String tableName = Optional.ofNullable(clazz.getAnnotation(Table.class)).map(Table::name)
99+
.orElse(Optional.ofNullable(clazz.getAnnotation(org.springframework.data.relational.core.mapping.Table.class)).map(org.springframework.data.relational.core.mapping.Table::value).orElse(null));
100+
if (tableName != null) consumer.accept(Map.entry(tableName, clazz));
101+
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (t1, t2) -> t1));
76102
}
77103
}
78104

@@ -166,7 +192,7 @@ public <E> void the_repository_will_contain_with_type(Guard guard, CrudRepositor
166192
.filter(entityManagerFactory -> entityManagerFactory.getPersistenceUnitInfo() != null)
167193
.filter(entityManagerFactory -> entityManagerFactory.getPersistenceUnitInfo().getManagedClassNames().contains(entityClass.getName()))
168194
.map(LocalContainerEntityManagerFactoryBean::getDataSource).findFirst()
169-
.orElseThrow();
195+
.orElse(entityManagerFactories.get(0).getDataSource());
170196
new JdbcTemplate(dataSource).update("TRUNCATE %s RESTART IDENTITY CASCADE".formatted(table));
171197
}
172198
repository.saveAll(Mapper.readAsAListOf(entities, entityClass));
@@ -190,43 +216,20 @@ public <E> void add_repository_content_to_variable(Guard guard, String name, Cru
190216
}
191217

192218
public <E> CrudRepository<E, ?> getRepositoryForTable(String table) {
193-
return getRepositoryForEntity(entityTypeByTableNameOrClassName(table));
219+
return getRepositoryForEntity(Optional.ofNullable(this.entityClassByTableName.get(table)).orElseGet(() -> TypeParser.parse(table)));
194220
}
195221

196222
@Nullable
197223
private Type entityTypeByTableNameOrClassName(String entityTableOrClass) {
198-
return entityClassByTableName.containsKey(entityTableOrClass) ? entityClassByTableName.get(entityTableOrClass) : TypeParser.parse(entityTableOrClass);
224+
return Optional.ofNullable(entityClassByTableName.get(entityTableOrClass)).orElseGet(() -> TypeParser.parse(entityTableOrClass));
199225
}
200226

201227
@SuppressWarnings({"unchecked"})
202228
public <E> CrudRepository<E, ?> getRepositoryForEntity(Type type) {
203-
if (Types.rawTypeOf(type).isAnnotationPresent(Entity.class)) {
204-
return spring.applicationContext().getBeansOfType(CrudRepository.class).values()
205-
.stream()
206-
.map(bean -> (CrudRepository<E, ?>) bean)
207-
.filter(r -> {
208-
Map<TypeVariable<?>, Type> typeArguments = TypeUtils.getTypeArguments(r.getClass(), CrudRepository.class);
209-
if (type.equals(TypeUtils.unrollVariables(typeArguments, CrudRepository.class.getTypeParameters()[0])))
210-
return true;
211-
212-
if (type instanceof Class<?> clazz
213-
&& typeArguments.get(CrudRepository.class.getTypeParameters()[0]) instanceof TypeVariable<?> typeVariable) {
214-
Type handledEntitySuperclass = typeVariable.getBounds()[0];
215-
216-
List<Class<?>> superClasses = new ArrayList<>();
217-
while (clazz != Object.class) {
218-
superClasses.add(clazz);
219-
clazz = clazz.getSuperclass();
220-
}
221-
222-
return superClasses.contains(handledEntitySuperclass);
223-
}
229+
CrudRepository<?, ?> crudRepository = crudRepositoryByClass.get(type);
230+
if (crudRepository == null) throw new AssertionError(type + " is not an Entity!");
224231

225-
return false;
226-
}).sorted((r1, r2) -> TypeUtils.getTypeArguments(r1.getClass(), CrudRepository.class).values().stream().findFirst().orElse(null) instanceof TypeVariable<?> typeVariable ? 1 : 0)
227-
.findFirst().orElseThrow(() -> new AssertionError("there was no CrudRepository found for entity %s! If you don't need one in your app, you must create one in your tests!".formatted(type.getTypeName())));
228-
}
229-
throw new AssertionError(type + " is not an Entity!");
232+
return (CrudRepository<E, ?>) crudRepository;
230233
}
231234

232235
public <E> CrudRepository<E, ?> getRepositoryByType(Type type) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.another_org;
2+
3+
import lombok.Getter;
4+
import lombok.NoArgsConstructor;
5+
6+
import javax.persistence.*;
7+
8+
@NoArgsConstructor
9+
@Getter
10+
@Entity
11+
@Table(name = "evilness")
12+
public class CorruptedEvilness {
13+
@Id
14+
@GeneratedValue(strategy = GenerationType.IDENTITY)
15+
Integer id;
16+
@Column(
17+
name = "bad_attribute"
18+
)
19+
boolean badAttribute;
20+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.another_org;
2+
3+
import org.springframework.data.repository.CrudRepository;
4+
import org.springframework.stereotype.Repository;
5+
6+
@Repository
7+
public interface CorruptedEvilnessDataSpringRepository extends CrudRepository<CorruptedEvilness, Integer> {}

tzatziki-spring-jpa/src/test/java/com/decathlon/tzatziki/app/TestApplication.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.boot.autoconfigure.domain.EntityScan;
6+
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
57

68
@SpringBootApplication
9+
@EnableJpaRepositories(basePackages = {"com.another_org", "com.decathlon.tzatziki.app"})
10+
@EntityScan(basePackages = {"com.another_org", "com.decathlon.tzatziki.app"})
711
public class TestApplication {
812

913
public static void main(String[] args) {

tzatziki-spring-jpa/src/test/java/com/decathlon/tzatziki/app/model/SuperUser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
@Getter
1111
@Entity
1212
@NoArgsConstructor
13-
@Table(name = "superusers")
13+
@Table(name = "super_users")
1414
public class SuperUser extends User {
1515
@Column(name = "role")
1616
String role;

tzatziki-spring-jpa/src/test/resources/com/decathlon/tzatziki/steps/spring-jpa.feature

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,28 @@ Feature: to interact with a spring boot service having a persistence layer
231231
| id | evil |
232232
| 1 | false |
233233

234-
Scenario: we can use extended entities and manage their tables (ex. superusers extends users)
235-
Given the superusers table will contain:
234+
Scenario: we can use extended entities and manage their tables (ex. super_users extends users)
235+
Given the super_users table will contain:
236236
| id | firstName | lastName | role |
237237
| 1 | Darth | Vader | admin |
238238
| 2 | Anakin | Skywalker | dummy |
239-
Then the superusers table contains:
239+
Then the super_users table contains:
240240
| id | firstName | lastName | role |
241241
| 1 | Darth | Vader | admin |
242-
| 2 | Anakin | Skywalker | dummy |
242+
| 2 | Anakin | Skywalker | dummy |
243+
244+
Scenario: if we have a table which is handled by multiple entities, we should prioritize entity types from default parser package
245+
# non-default package, should not be used and throw an exception
246+
Given that an UnrecognizedPropertyException is thrown when the evilness table will contain:
247+
| badAttribute |
248+
| true |
249+
And the evilness table will contain:
250+
| evil |
251+
| true |
252+
Then the evilness table contains only:
253+
| id | evil |
254+
| 1 | true |
255+
# the non-default package was not inserted
256+
And it is not true that the evilness table contains:
257+
| badAttribute |
258+
| true |

tzatziki-spring-jpa/src/test/resources/db/migration/V0__init.sql

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ create table users
88
group_id INT
99
);
1010

11-
create table superusers
11+
create table super_users
1212
(
13-
id SERIAL PRIMARY KEY REFERENCES users(id),
14-
role VARCHAR(255) NOT NULL
13+
id SERIAL PRIMARY KEY REFERENCES users (id),
14+
role VARCHAR(255) NOT NULL
1515
);
1616

1717
create table groups
@@ -22,8 +22,9 @@ create table groups
2222

2323
create table evilness
2424
(
25-
id SERIAL PRIMARY KEY,
26-
evil BOOL
25+
id SERIAL PRIMARY KEY,
26+
evil BOOL,
27+
bad_attribute BOOL
2728
);
2829

2930
CREATE OR REPLACE FUNCTION update_timestamp()

0 commit comments

Comments
 (0)