Skip to content

Commit 81a6a19

Browse files
authored
feat: support transient field modifier with class hierarchy introspection (#168)
1 parent 5385a41 commit 81a6a19

File tree

6 files changed

+219
-31
lines changed

6 files changed

+219
-31
lines changed

graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535

3636
import javax.persistence.Convert;
3737
import javax.persistence.EntityManager;
38-
import javax.persistence.Transient;
3938
import javax.persistence.metamodel.Attribute;
4039
import javax.persistence.metamodel.EmbeddableType;
4140
import javax.persistence.metamodel.EntityType;
@@ -44,6 +43,9 @@
4443
import javax.persistence.metamodel.SingularAttribute;
4544
import javax.persistence.metamodel.Type;
4645

46+
import org.slf4j.Logger;
47+
import org.slf4j.LoggerFactory;
48+
4749
import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription;
4850
import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore;
4951
import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnoreFilter;
@@ -53,6 +55,7 @@
5355
import com.introproventures.graphql.jpa.query.schema.NamingStrategy;
5456
import com.introproventures.graphql.jpa.query.schema.impl.IntrospectionUtils.CachedIntrospectionResult.CachedPropertyDescriptor;
5557
import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria;
58+
5659
import graphql.Assert;
5760
import graphql.Scalars;
5861
import graphql.schema.Coercing;
@@ -71,8 +74,6 @@
7174
import graphql.schema.GraphQLType;
7275
import graphql.schema.GraphQLTypeReference;
7376
import graphql.schema.PropertyDataFetcher;
74-
import org.slf4j.Logger;
75-
import org.slf4j.LoggerFactory;
7677

7778
/**
7879
* JPA specific schema builder implementation of {code #GraphQLSchemaBuilder} interface
@@ -672,7 +673,7 @@ private List<GraphQLFieldDefinition> getEntityAttributesFields(EntityType<?> ent
672673
private List<GraphQLFieldDefinition> getTransientFields(Class<?> clazz) {
673674
return IntrospectionUtils.introspect(clazz)
674675
.getPropertyDescriptors().stream()
675-
.filter(it -> it.isAnnotationPresent(Transient.class))
676+
.filter(it -> IntrospectionUtils.isTransient(clazz, it.getName()))
676677
.filter(it -> !it.isAnnotationPresent(GraphQLIgnore.class))
677678
.map(CachedPropertyDescriptor::getDelegate)
678679
.map(this::getJavaFieldDefinition)

graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtils.java

+85-14
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
package com.introproventures.graphql.jpa.query.schema.impl;
22

3-
import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore;
4-
53
import java.beans.BeanInfo;
64
import java.beans.IntrospectionException;
75
import java.beans.Introspector;
86
import java.beans.PropertyDescriptor;
97
import java.lang.annotation.Annotation;
8+
import java.lang.reflect.Field;
9+
import java.lang.reflect.Modifier;
10+
import java.util.Arrays;
1011
import java.util.Collection;
12+
import java.util.Iterator;
1113
import java.util.LinkedHashMap;
1214
import java.util.Map;
15+
import java.util.Objects;
1316
import java.util.Optional;
17+
import java.util.Spliterator;
18+
import java.util.Spliterators;
19+
import java.util.function.Function;
1420
import java.util.stream.Collectors;
1521
import java.util.stream.Stream;
22+
import java.util.stream.StreamSupport;
1623

1724
import javax.persistence.Transient;
1825

26+
import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore;
27+
1928
public class IntrospectionUtils {
2029
private static final Map<Class<?>, CachedIntrospectionResult> map = new LinkedHashMap<>();
2130

@@ -24,25 +33,38 @@ public static CachedIntrospectionResult introspect(Class<?> entity) {
2433
}
2534

2635
public static boolean isTransient(Class<?> entity, String propertyName) {
27-
return isAnnotationPresent(entity, propertyName, Transient.class);
36+
if(!introspect(entity).hasPropertyDescriptor(propertyName)) {
37+
throw new RuntimeException(new NoSuchFieldException(propertyName));
38+
}
39+
40+
return Stream.of(isAnnotationPresent(entity, propertyName, Transient.class),
41+
isModifierPresent(entity, propertyName, Modifier::isTransient))
42+
.anyMatch(it -> it.isPresent() && it.get() == true);
2843
}
29-
44+
3045
public static boolean isIgnored(Class<?> entity, String propertyName) {
31-
return isAnnotationPresent(entity, propertyName, GraphQLIgnore.class);
46+
return isAnnotationPresent(entity, propertyName, GraphQLIgnore.class)
47+
.orElseThrow(() -> new RuntimeException(new NoSuchFieldException(propertyName)));
3248
}
3349

34-
private static boolean isAnnotationPresent(Class<?> entity, String propertyName, Class<? extends Annotation> annotation){
50+
private static Optional<Boolean> isAnnotationPresent(Class<?> entity, String propertyName, Class<? extends Annotation> annotation){
3551
return introspect(entity).getPropertyDescriptor(propertyName)
36-
.map(it -> it.isAnnotationPresent(annotation))
37-
.orElse(false);
52+
.map(it -> it.isAnnotationPresent(annotation));
3853
}
3954

55+
private static Optional<Boolean> isModifierPresent(Class<?> entity, String propertyName, Function<Integer, Boolean> function){
56+
return introspect(entity).getField(propertyName)
57+
.map(it -> function.apply(it.getModifiers()));
58+
}
59+
4060
public static class CachedIntrospectionResult {
4161

4262
private final Map<String, CachedPropertyDescriptor> map;
4363
private final Class<?> entity;
4464
private final BeanInfo beanInfo;
45-
65+
private final Map<String, Field> fields;
66+
67+
@SuppressWarnings("rawtypes")
4668
public CachedIntrospectionResult(Class<?> entity) {
4769
try {
4870
this.beanInfo = Introspector.getBeanInfo(entity);
@@ -54,6 +76,11 @@ public CachedIntrospectionResult(Class<?> entity) {
5476
this.map = Stream.of(beanInfo.getPropertyDescriptors())
5577
.map(CachedPropertyDescriptor::new)
5678
.collect(Collectors.toMap(CachedPropertyDescriptor::getName, it -> it));
79+
80+
this.fields = iterate((Class) entity, k -> Optional.ofNullable(k.getSuperclass()))
81+
.flatMap(k -> Arrays.stream(k.getDeclaredFields()))
82+
.filter(f -> map.containsKey(f.getName()))
83+
.collect(Collectors.toMap(Field::getName, it -> it));
5784
}
5885

5986
public Collection<CachedPropertyDescriptor> getPropertyDescriptors() {
@@ -64,6 +91,14 @@ public Optional<CachedPropertyDescriptor> getPropertyDescriptor(String fieldName
6491
return Optional.ofNullable(map.getOrDefault(fieldName, null));
6592
}
6693

94+
public boolean hasPropertyDescriptor(String fieldName) {
95+
return map.containsKey(fieldName);
96+
}
97+
98+
public Optional<Field> getField(String fieldName) {
99+
return Optional.ofNullable(fields.get(fieldName));
100+
}
101+
67102
public Class<?> getEntity() {
68103
return entity;
69104
}
@@ -83,6 +118,10 @@ public PropertyDescriptor getDelegate() {
83118
return delegate;
84119
}
85120

121+
public Class<?> getPropertyType() {
122+
return delegate.getPropertyType();
123+
}
124+
86125
public String getName() {
87126
return delegate.getName();
88127
}
@@ -92,11 +131,9 @@ public boolean isAnnotationPresent(Class<? extends Annotation> annotation) {
92131
}
93132

94133
private boolean isAnnotationPresentOnField(Class<? extends Annotation> annotation) {
95-
try {
96-
return entity.getDeclaredField(delegate.getName()).isAnnotationPresent(annotation);
97-
} catch (NoSuchFieldException e) {
98-
return false;
99-
}
134+
return Optional.ofNullable(fields.get(delegate.getName()))
135+
.map(f -> f.isAnnotationPresent(annotation))
136+
.orElse(false);
100137
}
101138

102139
private boolean isAnnotationPresentOnReadMethod(Class<? extends Annotation> annotation) {
@@ -105,4 +142,38 @@ private boolean isAnnotationPresentOnReadMethod(Class<? extends Annotation> anno
105142

106143
}
107144
}
145+
146+
/**
147+
* The following method is borrowed from Streams.iterate,
148+
* however Streams.iterate is designed to create infinite streams.
149+
*
150+
* This version has been modified to end when Optional.empty()
151+
* is returned from the fetchNextFunction.
152+
*/
153+
protected static <T> Stream<T> iterate( T seed, Function<T, Optional<T>> fetchNextFunction ) {
154+
Objects.requireNonNull(fetchNextFunction);
155+
156+
Iterator<T> iterator = new Iterator<T>() {
157+
private Optional<T> t = Optional.ofNullable(seed);
158+
159+
@Override
160+
public boolean hasNext() {
161+
return t.isPresent();
162+
}
163+
164+
@Override
165+
public T next() {
166+
T v = t.get();
167+
168+
t = fetchNextFunction.apply(v);
169+
170+
return v;
171+
}
172+
};
173+
174+
return StreamSupport.stream(
175+
Spliterators.spliteratorUnknownSize( iterator, Spliterator.ORDERED | Spliterator.IMMUTABLE),
176+
false
177+
);
178+
}
108179
}

graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalculatedEntityTests.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
import javax.persistence.EntityManager;
88

9-
import graphql.ExecutionResult;
10-
import graphql.validation.ValidationErrorType;
119
import org.junit.Test;
1210
import org.junit.runner.RunWith;
1311
import org.springframework.beans.factory.annotation.Autowired;
@@ -21,6 +19,9 @@
2119
import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor;
2220
import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder;
2321

22+
import graphql.ExecutionResult;
23+
import graphql.validation.ValidationErrorType;
24+
2425
@RunWith(SpringRunner.class)
2526
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.NONE)
2627
@TestPropertySource({"classpath:hibernate.properties"})
@@ -80,6 +81,17 @@ public void testIgnoreFields() {
8081
" hideFieldFunction" +
8182
" propertyIgnoredOnGetter" +
8283
" ignoredTransientValue" +
84+
" transientModifier" +
85+
" transientModifierGraphQLIgnore" +
86+
" parentField" +
87+
" parentTransientModifier" +
88+
" parentTransient" +
89+
" parentTransientGetter" +
90+
" parentGraphQLIngore" +
91+
" parentGraphQLIgnoreGetter" +
92+
" parentTransientGraphQLIgnore" +
93+
" parentTransientModifierGraphQLIgnore" +
94+
" parentTransientGraphQLIgnoreGetter" +
8395
" } " +
8496
" } " +
8597
"}";
@@ -95,7 +107,13 @@ public void testIgnoreFields() {
95107
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "hideField")),
96108
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "hideFieldFunction")),
97109
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "propertyIgnoredOnGetter")),
98-
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "ignoredTransientValue"))
110+
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "ignoredTransientValue")),
111+
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentGraphQLIngore")),
112+
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentGraphQLIgnoreGetter")),
113+
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentTransientGraphQLIgnore")),
114+
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentTransientModifierGraphQLIgnore")),
115+
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentTransientGraphQLIgnoreGetter")),
116+
tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "transientModifierGraphQLIgnore"))
99117
);
100118
}
101119

graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtilsTest.java

+16-8
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,25 @@
44

55
import org.junit.Test;
66

7-
import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore;
87
import com.introproventures.graphql.jpa.query.schema.model.calculated.CalculatedEntity;
98

109
public class IntrospectionUtilsTest {
1110

1211
// given
1312
private final Class<CalculatedEntity> entity = CalculatedEntity.class;
1413

15-
@Test
14+
@Test(expected = RuntimeException.class)
1615
public void testIsTransientNonExisting() throws Exception {
1716
// then
1817
assertThat(IntrospectionUtils.isTransient(entity, "notFound")).isFalse();
1918
}
2019

20+
@Test(expected = RuntimeException.class)
21+
public void testIsIgnoredNonExisting() throws Exception {
22+
// then
23+
assertThat(IntrospectionUtils.isIgnored(entity, "notFound")).isFalse();
24+
}
25+
2126
@Test
2227
public void testIsTransientClass() throws Exception {
2328
// then
@@ -38,6 +43,10 @@ public void testIsTransientFields() throws Exception {
3843
assertThat(IntrospectionUtils.isTransient(entity, "fieldMem")).isTrue();
3944
assertThat(IntrospectionUtils.isTransient(entity, "hideField")).isTrue();
4045
assertThat(IntrospectionUtils.isTransient(entity, "logic")).isTrue();
46+
assertThat(IntrospectionUtils.isTransient(entity, "transientModifier")).isTrue();
47+
assertThat(IntrospectionUtils.isTransient(entity, "parentTransientModifier")).isTrue();
48+
assertThat(IntrospectionUtils.isTransient(entity, "parentTransient")).isTrue();
49+
assertThat(IntrospectionUtils.isTransient(entity, "parentTransientGetter")).isTrue();
4150
}
4251

4352
@Test
@@ -46,6 +55,7 @@ public void testNotTransientFields() throws Exception {
4655
assertThat(IntrospectionUtils.isTransient(entity, "id")).isFalse();
4756
assertThat(IntrospectionUtils.isTransient(entity, "info")).isFalse();
4857
assertThat(IntrospectionUtils.isTransient(entity, "title")).isFalse();
58+
assertThat(IntrospectionUtils.isTransient(entity, "parentField")).isFalse();
4959
}
5060

5161
@Test
@@ -56,12 +66,10 @@ public void testByPassSetMethod() throws Exception {
5666

5767
@Test
5868
public void shouldIgnoreMethodsThatAreAnnotatedWithGraphQLIgnore() {
59-
//when
60-
boolean propertyIgnoredOnGetter = IntrospectionUtils.isIgnored(entity, "propertyIgnoredOnGetter");
61-
boolean ignoredTransientValue = IntrospectionUtils.isIgnored(entity, "ignoredTransientValue");
62-
6369
//then
64-
assertThat(propertyIgnoredOnGetter).isTrue();
65-
assertThat(ignoredTransientValue).isTrue();
70+
assertThat(IntrospectionUtils.isIgnored(entity, "propertyIgnoredOnGetter")).isTrue();
71+
assertThat(IntrospectionUtils.isIgnored(entity, "ignoredTransientValue")).isTrue();
72+
assertThat(IntrospectionUtils.isIgnored(entity, "hideField")).isTrue();
73+
assertThat(IntrospectionUtils.isIgnored(entity, "parentGraphQLIgnore")).isTrue();
6674
}
6775
}

graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java

+39-2
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,56 @@
88
import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore;
99

1010
import lombok.Data;
11+
import lombok.EqualsAndHashCode;
12+
13+
/**
14+
*
15+
2.1.1 Persistent Fields and Properties
16+
17+
The persistent state of an entity is accessed by the persistence provider
18+
runtime either via JavaBeans style property accessors or via instance variables.
19+
A single access type (field or property access) applies to an entity hierarchy.
20+
21+
When annotations are used, the placement of the mapping annotations on either
22+
the persistent fields or persistent properties of the entity class specifies the
23+
access type as being either field - or property - based access respectively.
24+
25+
If the entity has field-based access, the persistence provider runtime accesses
26+
instance variables directly. All non-transient instance variables that are not
27+
annotated with the Transient annotation are persistent. When field-based access
28+
is used, the object/relational mapping annotations for the entity class annotate
29+
the instance variables.
30+
31+
If the entity has property-based access, the persistence provider runtime accesses
32+
persistent state via the property accessor methods. All properties not annotated with
33+
the Transient annotation are persistent. The property accessor methods must be public
34+
or protected. When property-based access is used, the object/relational mapping
35+
annotations for the entity class annotate the getter property accessors.
36+
37+
Mapping annotations cannot be applied to fields or properties that are transient or Transient.
38+
39+
The behavior is unspecified if mapping annotations are applied to both persistent fields and
40+
properties or if the XML descriptor specifies use of different access types within a class hierarchy.
41+
*/
1142

1243
@Data
44+
@EqualsAndHashCode(callSuper = true)
1345
@Entity
14-
public class CalculatedEntity {
46+
public class CalculatedEntity extends ParentCalculatedEntity {
1547
@Id
1648
Long id;
1749

1850
String title;
1951

2052
String info;
53+
54+
transient Integer transientModifier; // transient property
55+
56+
@GraphQLIgnore
57+
transient Integer transientModifierGraphQLIgnore; // transient property
2158

2259
@Transient
23-
boolean logic = true;
60+
boolean logic = true; // transient property
2461

2562
@Transient
2663
@GraphQLDescription("i desc member")

0 commit comments

Comments
 (0)