diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index e4f11ee0b..1c682b2d9 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -35,7 +35,6 @@ import javax.persistence.Convert; import javax.persistence.EntityManager; -import javax.persistence.Transient; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.EmbeddableType; import javax.persistence.metamodel.EntityType; @@ -44,6 +43,9 @@ import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnoreFilter; @@ -53,6 +55,7 @@ import com.introproventures.graphql.jpa.query.schema.NamingStrategy; import com.introproventures.graphql.jpa.query.schema.impl.IntrospectionUtils.CachedIntrospectionResult.CachedPropertyDescriptor; import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; + import graphql.Assert; import graphql.Scalars; import graphql.schema.Coercing; @@ -71,8 +74,6 @@ import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeReference; import graphql.schema.PropertyDataFetcher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * JPA specific schema builder implementation of {code #GraphQLSchemaBuilder} interface @@ -672,7 +673,7 @@ private List getEntityAttributesFields(EntityType ent private List getTransientFields(Class clazz) { return IntrospectionUtils.introspect(clazz) .getPropertyDescriptors().stream() - .filter(it -> it.isAnnotationPresent(Transient.class)) + .filter(it -> IntrospectionUtils.isTransient(clazz, it.getName())) .filter(it -> !it.isAnnotationPresent(GraphQLIgnore.class)) .map(CachedPropertyDescriptor::getDelegate) .map(this::getJavaFieldDefinition) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtils.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtils.java index fc42e9570..2921c5058 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtils.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtils.java @@ -1,21 +1,30 @@ package com.introproventures.graphql.jpa.query.schema.impl; -import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; - import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.Collection; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import javax.persistence.Transient; +import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; + public class IntrospectionUtils { private static final Map, CachedIntrospectionResult> map = new LinkedHashMap<>(); @@ -24,25 +33,38 @@ public static CachedIntrospectionResult introspect(Class entity) { } public static boolean isTransient(Class entity, String propertyName) { - return isAnnotationPresent(entity, propertyName, Transient.class); + if(!introspect(entity).hasPropertyDescriptor(propertyName)) { + throw new RuntimeException(new NoSuchFieldException(propertyName)); + } + + return Stream.of(isAnnotationPresent(entity, propertyName, Transient.class), + isModifierPresent(entity, propertyName, Modifier::isTransient)) + .anyMatch(it -> it.isPresent() && it.get() == true); } - + public static boolean isIgnored(Class entity, String propertyName) { - return isAnnotationPresent(entity, propertyName, GraphQLIgnore.class); + return isAnnotationPresent(entity, propertyName, GraphQLIgnore.class) + .orElseThrow(() -> new RuntimeException(new NoSuchFieldException(propertyName))); } - private static boolean isAnnotationPresent(Class entity, String propertyName, Class annotation){ + private static Optional isAnnotationPresent(Class entity, String propertyName, Class annotation){ return introspect(entity).getPropertyDescriptor(propertyName) - .map(it -> it.isAnnotationPresent(annotation)) - .orElse(false); + .map(it -> it.isAnnotationPresent(annotation)); } + private static Optional isModifierPresent(Class entity, String propertyName, Function function){ + return introspect(entity).getField(propertyName) + .map(it -> function.apply(it.getModifiers())); + } + public static class CachedIntrospectionResult { private final Map map; private final Class entity; private final BeanInfo beanInfo; - + private final Map fields; + + @SuppressWarnings("rawtypes") public CachedIntrospectionResult(Class entity) { try { this.beanInfo = Introspector.getBeanInfo(entity); @@ -54,6 +76,11 @@ public CachedIntrospectionResult(Class entity) { this.map = Stream.of(beanInfo.getPropertyDescriptors()) .map(CachedPropertyDescriptor::new) .collect(Collectors.toMap(CachedPropertyDescriptor::getName, it -> it)); + + this.fields = iterate((Class) entity, k -> Optional.ofNullable(k.getSuperclass())) + .flatMap(k -> Arrays.stream(k.getDeclaredFields())) + .filter(f -> map.containsKey(f.getName())) + .collect(Collectors.toMap(Field::getName, it -> it)); } public Collection getPropertyDescriptors() { @@ -64,6 +91,14 @@ public Optional getPropertyDescriptor(String fieldName return Optional.ofNullable(map.getOrDefault(fieldName, null)); } + public boolean hasPropertyDescriptor(String fieldName) { + return map.containsKey(fieldName); + } + + public Optional getField(String fieldName) { + return Optional.ofNullable(fields.get(fieldName)); + } + public Class getEntity() { return entity; } @@ -83,6 +118,10 @@ public PropertyDescriptor getDelegate() { return delegate; } + public Class getPropertyType() { + return delegate.getPropertyType(); + } + public String getName() { return delegate.getName(); } @@ -92,11 +131,9 @@ public boolean isAnnotationPresent(Class annotation) { } private boolean isAnnotationPresentOnField(Class annotation) { - try { - return entity.getDeclaredField(delegate.getName()).isAnnotationPresent(annotation); - } catch (NoSuchFieldException e) { - return false; - } + return Optional.ofNullable(fields.get(delegate.getName())) + .map(f -> f.isAnnotationPresent(annotation)) + .orElse(false); } private boolean isAnnotationPresentOnReadMethod(Class annotation) { @@ -105,4 +142,38 @@ private boolean isAnnotationPresentOnReadMethod(Class anno } } + + /** + * The following method is borrowed from Streams.iterate, + * however Streams.iterate is designed to create infinite streams. + * + * This version has been modified to end when Optional.empty() + * is returned from the fetchNextFunction. + */ + protected static Stream iterate( T seed, Function> fetchNextFunction ) { + Objects.requireNonNull(fetchNextFunction); + + Iterator iterator = new Iterator() { + private Optional t = Optional.ofNullable(seed); + + @Override + public boolean hasNext() { + return t.isPresent(); + } + + @Override + public T next() { + T v = t.get(); + + t = fetchNextFunction.apply(v); + + return v; + } + }; + + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( iterator, Spliterator.ORDERED | Spliterator.IMMUTABLE), + false + ); + } } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalculatedEntityTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalculatedEntityTests.java index c6be5ba04..e63463bfb 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalculatedEntityTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalculatedEntityTests.java @@ -6,8 +6,6 @@ import javax.persistence.EntityManager; -import graphql.ExecutionResult; -import graphql.validation.ValidationErrorType; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -21,6 +19,9 @@ import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; +import graphql.ExecutionResult; +import graphql.validation.ValidationErrorType; + @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.NONE) @TestPropertySource({"classpath:hibernate.properties"}) @@ -80,6 +81,17 @@ public void testIgnoreFields() { " hideFieldFunction" + " propertyIgnoredOnGetter" + " ignoredTransientValue" + + " transientModifier" + + " transientModifierGraphQLIgnore" + + " parentField" + + " parentTransientModifier" + + " parentTransient" + + " parentTransientGetter" + + " parentGraphQLIngore" + + " parentGraphQLIgnoreGetter" + + " parentTransientGraphQLIgnore" + + " parentTransientModifierGraphQLIgnore" + + " parentTransientGraphQLIgnoreGetter" + " } " + " } " + "}"; @@ -95,7 +107,13 @@ public void testIgnoreFields() { tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "hideField")), tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "hideFieldFunction")), tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "propertyIgnoredOnGetter")), - tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "ignoredTransientValue")) + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "ignoredTransientValue")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentGraphQLIngore")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentGraphQLIgnoreGetter")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentTransientGraphQLIgnore")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentTransientModifierGraphQLIgnore")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentTransientGraphQLIgnoreGetter")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "transientModifierGraphQLIgnore")) ); } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtilsTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtilsTest.java index 25554680c..6febd3d13 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtilsTest.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtilsTest.java @@ -4,7 +4,6 @@ import org.junit.Test; -import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; import com.introproventures.graphql.jpa.query.schema.model.calculated.CalculatedEntity; public class IntrospectionUtilsTest { @@ -12,12 +11,18 @@ public class IntrospectionUtilsTest { // given private final Class entity = CalculatedEntity.class; - @Test + @Test(expected = RuntimeException.class) public void testIsTransientNonExisting() throws Exception { // then assertThat(IntrospectionUtils.isTransient(entity, "notFound")).isFalse(); } + @Test(expected = RuntimeException.class) + public void testIsIgnoredNonExisting() throws Exception { + // then + assertThat(IntrospectionUtils.isIgnored(entity, "notFound")).isFalse(); + } + @Test public void testIsTransientClass() throws Exception { // then @@ -38,6 +43,10 @@ public void testIsTransientFields() throws Exception { assertThat(IntrospectionUtils.isTransient(entity, "fieldMem")).isTrue(); assertThat(IntrospectionUtils.isTransient(entity, "hideField")).isTrue(); assertThat(IntrospectionUtils.isTransient(entity, "logic")).isTrue(); + assertThat(IntrospectionUtils.isTransient(entity, "transientModifier")).isTrue(); + assertThat(IntrospectionUtils.isTransient(entity, "parentTransientModifier")).isTrue(); + assertThat(IntrospectionUtils.isTransient(entity, "parentTransient")).isTrue(); + assertThat(IntrospectionUtils.isTransient(entity, "parentTransientGetter")).isTrue(); } @Test @@ -46,6 +55,7 @@ public void testNotTransientFields() throws Exception { assertThat(IntrospectionUtils.isTransient(entity, "id")).isFalse(); assertThat(IntrospectionUtils.isTransient(entity, "info")).isFalse(); assertThat(IntrospectionUtils.isTransient(entity, "title")).isFalse(); + assertThat(IntrospectionUtils.isTransient(entity, "parentField")).isFalse(); } @Test @@ -56,12 +66,10 @@ public void testByPassSetMethod() throws Exception { @Test public void shouldIgnoreMethodsThatAreAnnotatedWithGraphQLIgnore() { - //when - boolean propertyIgnoredOnGetter = IntrospectionUtils.isIgnored(entity, "propertyIgnoredOnGetter"); - boolean ignoredTransientValue = IntrospectionUtils.isIgnored(entity, "ignoredTransientValue"); - //then - assertThat(propertyIgnoredOnGetter).isTrue(); - assertThat(ignoredTransientValue).isTrue(); + assertThat(IntrospectionUtils.isIgnored(entity, "propertyIgnoredOnGetter")).isTrue(); + assertThat(IntrospectionUtils.isIgnored(entity, "ignoredTransientValue")).isTrue(); + assertThat(IntrospectionUtils.isIgnored(entity, "hideField")).isTrue(); + assertThat(IntrospectionUtils.isIgnored(entity, "parentGraphQLIgnore")).isTrue(); } } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java index cfacedc58..da537cf51 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java @@ -8,19 +8,56 @@ import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * + 2.1.1 Persistent Fields and Properties + + The persistent state of an entity is accessed by the persistence provider + runtime either via JavaBeans style property accessors or via instance variables. + A single access type (field or property access) applies to an entity hierarchy. + + When annotations are used, the placement of the mapping annotations on either + the persistent fields or persistent properties of the entity class specifies the + access type as being either field - or property - based access respectively. + + If the entity has field-based access, the persistence provider runtime accesses + instance variables directly. All non-transient instance variables that are not + annotated with the Transient annotation are persistent. When field-based access + is used, the object/relational mapping annotations for the entity class annotate + the instance variables. + + If the entity has property-based access, the persistence provider runtime accesses + persistent state via the property accessor methods. All properties not annotated with + the Transient annotation are persistent. The property accessor methods must be public + or protected. When property-based access is used, the object/relational mapping + annotations for the entity class annotate the getter property accessors. + + Mapping annotations cannot be applied to fields or properties that are transient or Transient. + + The behavior is unspecified if mapping annotations are applied to both persistent fields and + properties or if the XML descriptor specifies use of different access types within a class hierarchy. + */ @Data +@EqualsAndHashCode(callSuper = true) @Entity -public class CalculatedEntity { +public class CalculatedEntity extends ParentCalculatedEntity { @Id Long id; String title; String info; + + transient Integer transientModifier; // transient property + + @GraphQLIgnore + transient Integer transientModifierGraphQLIgnore; // transient property @Transient - boolean logic = true; + boolean logic = true; // transient property @Transient @GraphQLDescription("i desc member") diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/ParentCalculatedEntity.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/ParentCalculatedEntity.java new file mode 100644 index 000000000..b7ff4f607 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/ParentCalculatedEntity.java @@ -0,0 +1,53 @@ +package com.introproventures.graphql.jpa.query.schema.model.calculated; + +import javax.persistence.MappedSuperclass; +import javax.persistence.Transient; + +import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; + +import lombok.Data; + +@MappedSuperclass +@Data +public class ParentCalculatedEntity { + + private Integer parentField; // persistent property + + private transient String parentTransientModifier; // transient property + + @GraphQLIgnore + private transient String parentTransientModifierGraphQLIgnore; // transient property + + @Transient + private String parentTransient; // transient property + + @GraphQLIgnore + @Transient + private String parentTransientGraphQLIgnore; // transient property + + @GraphQLIgnore + private String parentGraphQLIgnore; + + private String parentTransientGetter; + + private String parentGraphQLIgnoreGetter; + + private String parentTransientGraphQLIgnoreGetter; + + @Transient // transient getter property + public String getParentTransientGetter() { + return parentTransientGetter; + } + + @GraphQLIgnore + @Transient // transient getter property + public String getParentTransientGraphQLIgnoreGetter() { + return parentTransientGraphQLIgnoreGetter; + } + + @GraphQLIgnore + public String getParentGraphQLIgnoreGetter() { + return parentGraphQLIgnoreGetter; + } + +}