Skip to content

feat: add support for mapping transient fields with entity class hierarchy introspection #168

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -672,7 +673,7 @@ private List<GraphQLFieldDefinition> getEntityAttributesFields(EntityType<?> ent
private List<GraphQLFieldDefinition> 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Class<?>, CachedIntrospectionResult> map = new LinkedHashMap<>();

Expand All @@ -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)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The orElse(false) was added before to make sure that the application can start even if the field is not found. There may be a case that we have a getter that is not directly connected with any of the class' properties but is still mapped to the column in the database e.g.

public boolean isValuePresent(){
    return value != null;
}

But you are right that we should probably throw this error anyway and try to deal with such failure on the higher level. What do you think?

}

private static boolean isAnnotationPresent(Class<?> entity, String propertyName, Class<? extends Annotation> annotation){
private static Optional<Boolean> isAnnotationPresent(Class<?> entity, String propertyName, Class<? extends Annotation> annotation){
Copy link
Contributor

@anotender anotender Aug 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning Optional<Boolean> is a little bit misleading in this case. In my opinion calling isXXX methods should always return pure boolean value by convention. Using API with Optional<Boolean> leads us to the three-state logic we need to deal with. What do you think?

return introspect(entity).getPropertyDescriptor(propertyName)
.map(it -> it.isAnnotationPresent(annotation))
.orElse(false);
.map(it -> it.isAnnotationPresent(annotation));
}

private static Optional<Boolean> isModifierPresent(Class<?> entity, String propertyName, Function<Integer, Boolean> function){
return introspect(entity).getField(propertyName)
.map(it -> function.apply(it.getModifiers()));
}

public static class CachedIntrospectionResult {

private final Map<String, CachedPropertyDescriptor> map;
private final Class<?> entity;
private final BeanInfo beanInfo;

private final Map<String, Field> fields;

@SuppressWarnings("rawtypes")
public CachedIntrospectionResult(Class<?> entity) {
try {
this.beanInfo = Introspector.getBeanInfo(entity);
Expand All @@ -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<CachedPropertyDescriptor> getPropertyDescriptors() {
Expand All @@ -64,6 +91,14 @@ public Optional<CachedPropertyDescriptor> getPropertyDescriptor(String fieldName
return Optional.ofNullable(map.getOrDefault(fieldName, null));
}

public boolean hasPropertyDescriptor(String fieldName) {
return map.containsKey(fieldName);
}

public Optional<Field> getField(String fieldName) {
return Optional.ofNullable(fields.get(fieldName));
}

public Class<?> getEntity() {
return entity;
}
Expand All @@ -83,6 +118,10 @@ public PropertyDescriptor getDelegate() {
return delegate;
}

public Class<?> getPropertyType() {
return delegate.getPropertyType();
}

public String getName() {
return delegate.getName();
}
Expand All @@ -92,11 +131,9 @@ public boolean isAnnotationPresent(Class<? extends Annotation> annotation) {
}

private boolean isAnnotationPresentOnField(Class<? extends Annotation> 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<? extends Annotation> annotation) {
Expand All @@ -105,4 +142,38 @@ private boolean isAnnotationPresentOnReadMethod(Class<? extends Annotation> 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 <T> Stream<T> iterate( T seed, Function<T, Optional<T>> fetchNextFunction ) {
Objects.requireNonNull(fetchNextFunction);

Iterator<T> iterator = new Iterator<T>() {
private Optional<T> 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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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"})
Expand Down Expand Up @@ -80,6 +81,17 @@ public void testIgnoreFields() {
" hideFieldFunction" +
" propertyIgnoredOnGetter" +
" ignoredTransientValue" +
" transientModifier" +
" transientModifierGraphQLIgnore" +
" parentField" +
" parentTransientModifier" +
" parentTransient" +
" parentTransientGetter" +
" parentGraphQLIngore" +
" parentGraphQLIgnoreGetter" +
" parentTransientGraphQLIgnore" +
" parentTransientModifierGraphQLIgnore" +
" parentTransientGraphQLIgnoreGetter" +
" } " +
" } " +
"}";
Expand All @@ -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"))
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@

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 {

// given
private final Class<CalculatedEntity> 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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading