Skip to content

Commit

Permalink
GH-492 - Backport of fixes regarding the detection of generic field t…
Browse files Browse the repository at this point in the history
…ypes.

This is a backport of

* 687ae80
* e70981b
* 181f3b6

and fixes the type detection of generic fields in concrete subclasses.
It includes shaded parts of TypeResolver/Typetools as well.
  • Loading branch information
michael-simons committed Dec 11, 2018
1 parent d10a44d commit 25f55e7
Show file tree
Hide file tree
Showing 7 changed files with 475 additions and 42 deletions.
6 changes: 4 additions & 2 deletions core/src/main/java/org/neo4j/ogm/metadata/FieldInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

package org.neo4j.ogm.metadata;

import static org.neo4j.ogm.metadata.reflect.GenericUtils.*;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
Expand Down Expand Up @@ -95,8 +97,8 @@ public class FieldInfo {
public FieldInfo(ClassInfo classInfo, Field field, String typeParameterDescriptor, ObjectAnnotations annotations) {
this.containingClassInfo = classInfo;
this.field = field;
this.fieldType = field.getType();
this.isArray = field.getType().isArray();
this.fieldType = isGenericField(field) ? findFieldType(field, classInfo.getUnderlyingClass()) : field.getType();
this.isArray = fieldType.isArray();
this.name = field.getName();
this.descriptor = field.getType().getTypeName();
this.typeParameterDescriptor = typeParameterDescriptor;
Expand Down
28 changes: 26 additions & 2 deletions core/src/main/java/org/neo4j/ogm/metadata/FieldsInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import org.neo4j.ogm.annotation.Relationship;
import org.neo4j.ogm.annotation.Transient;
import org.neo4j.ogm.metadata.reflect.GenericUtils;

/**
* @author Vince Bickers
Expand All @@ -42,10 +45,17 @@ public class FieldsInfo {
this.fields = new HashMap<>();
}

public FieldsInfo(ClassInfo classInfo, Class<?> cls) {
FieldsInfo(ClassInfo classInfo, Class<?> clazz) {
this.fields = new HashMap<>();

for (Field field : cls.getDeclaredFields()) {
// Fields influenced by this class are all all declared fields plus
// all generics fields of possible superclasses that resolve to concrete
// types through this class.
List<Field> allFieldsInfluencedByThisClass = new ArrayList<>();
allFieldsInfluencedByThisClass.addAll(getGenericFieldsInHierarchyOf(clazz));
allFieldsInfluencedByThisClass.addAll(Arrays.asList(clazz.getDeclaredFields()));

for (Field field : allFieldsInfluencedByThisClass) {
final int modifiers = field.getModifiers();
if (!Modifier.isTransient(modifiers) && !Modifier.isFinal(modifiers) && !Modifier.isStatic(modifiers)) {
ObjectAnnotations objectAnnotations = ObjectAnnotations.of(field.getDeclaredAnnotations());
Expand Down Expand Up @@ -86,6 +96,20 @@ public FieldsInfo(ClassInfo classInfo, Class<?> cls) {
}
}

private static List<Field> getGenericFieldsInHierarchyOf(Class<?> clazz) {

List<Field> genericFieldsInHierarchy = new ArrayList<>();
Class<?> currentClass = clazz.getSuperclass();
while(currentClass != null) {
Stream.of(currentClass.getDeclaredFields())
.filter(GenericUtils::isGenericField)
.forEach(genericFieldsInHierarchy::add);
currentClass = currentClass.getSuperclass();
}

return genericFieldsInHierarchy;
}

public Collection<FieldInfo> fields() {
return fields.values();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public static Object merge(Class<?> parameterType, Object newValues, Collection

Object array = Array.newInstance(type, objects.size());
for (int i = 0; i < objects.size(); i++) {
Array.set(array, i, objects.get(i));
Array.set(array, i, Utils.coerceTypes(type, objects.get(i)));
}
return array;
}
Expand Down
240 changes: 204 additions & 36 deletions core/src/main/java/org/neo4j/ogm/metadata/reflect/GenericUtils.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,49 @@
/*
* Copyright (c) 2002-2018 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
* Copyright 2002-2019 the original author or authors.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This product may include a number of subcomponents with
* separate copyright notices and license terms. Your use of the source
* code for these subcomponents is subject to the terms and
* conditions of the subcomponent's license, as noted in the LICENSE file.
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.neo4j.ogm.metadata.reflect;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import java.util.Map;

/**
* Contains a stripped down version of <a href="https://github.com/jhalterman">Jonathan Halterman's</a>
* <a href="https://github.com/jhalterman/typetools/blob/master/src/main/java/net/jodah/typetools/TypeResolver.java">TypeResolver</a>
* from <a href="https://github.com/jhalterman/typetools">Typetools</a>.
*
* @author Frantisek Hartman
* @author Michael J. Simons
*/
public class GenericUtils {
public final class GenericUtils {

/**
* Helper to check whether a given field is a generic field (A field described by a type variable).
*
* @param field The field to check for a type variable
* @return True, if {@code field} is a generic field.
*/
public static boolean isGenericField(Field field) {
return field.getGenericType() instanceof TypeVariable;
}

/**
* Tries to discover type of given field
Expand All @@ -35,47 +57,193 @@ public class GenericUtils {
*/
public static Class findFieldType(Field field, Class concreteClass) {

if (field.getGenericType() instanceof Class) {
Class<?>[] arguments = resolveRawArguments(field.getGenericType(), concreteClass);
if (arguments == null || arguments.length == 0 || arguments[0] == Unknown.class) {
return field.getType();
}

TypeVariable[] typeParameters = field.getDeclaringClass().getTypeParameters();
for (int i = 0; i < typeParameters.length; i++) {
if (typeParameters[i].getName().equals(field.getGenericType().getTypeName())) {
return arguments[0];
}

ParameterizedType genericSuperclass = findMatchingSuperclass(concreteClass, field);
if (genericSuperclass != null) {
return (Class) genericSuperclass.getActualTypeArguments()[i];
}
}
/**
* Returns an array of raw classes representing arguments for the {@code genericType} using type variable information
* from the {@code subType}. Arguments for {@code genericType} that cannot be resolved are returned as
* {@code Unknown.class}. If no arguments can be resolved then {@code null} is returned.
*
* @param genericType to resolve arguments for
* @param subType to extract type variable information from
* @return array of raw classes representing arguments for the {@code genericType} else {@code null} if no type
* arguments are declared
*/
private static Class<?>[] resolveRawArguments(Type genericType, Class<?> subType) {
Class<?>[] result = null;
Class<?> functionalInterface = null;

// Handle lambdas
if (subType.isSynthetic()) {
Class<?> fi = genericType instanceof ParameterizedType
&& ((ParameterizedType) genericType).getRawType() instanceof Class
? (Class<?>) ((ParameterizedType) genericType).getRawType()
: genericType instanceof Class ? (Class<?>) genericType : null;
if (fi != null && fi.isInterface())
functionalInterface = fi;
}

if (genericType instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) genericType;
Type[] arguments = paramType.getActualTypeArguments();
result = new Class[arguments.length];
for (int i = 0; i < arguments.length; i++)
result[i] = resolveRawClass(arguments[i], subType, functionalInterface);
} else if (genericType instanceof TypeVariable) {
result = new Class[1];
result[0] = resolveRawClass(genericType, subType, functionalInterface);
} else if (genericType instanceof Class) {
TypeVariable<?>[] typeParams = ((Class<?>) genericType).getTypeParameters();
result = new Class[typeParams.length];
for (int i = 0; i < typeParams.length; i++)
result[i] = resolveRawClass(typeParams[i], subType, functionalInterface);
}

return result;
}

private static Class<?> resolveRawClass(Type genericType, Class<?> subType, Class<?> functionalInterface) {
if (genericType instanceof Class) {
return (Class<?>) genericType;
} else if (genericType instanceof ParameterizedType) {
return resolveRawClass(((ParameterizedType) genericType).getRawType(), subType, functionalInterface);
} else if (genericType instanceof GenericArrayType) {
GenericArrayType arrayType = (GenericArrayType) genericType;
Class<?> component = resolveRawClass(arrayType.getGenericComponentType(), subType, functionalInterface);
return Array.newInstance(component, 0).getClass();
} else if (genericType instanceof TypeVariable) {
TypeVariable<?> variable = (TypeVariable<?>) genericType;
genericType = getTypeVariableMap(subType, functionalInterface).get(variable);
genericType = genericType == null ? resolveBound(variable)
: resolveRawClass(genericType, subType, functionalInterface);
}

return genericType instanceof Class ? (Class<?>) genericType : Unknown.class;
}

private static Map<TypeVariable<?>, Type> getTypeVariableMap(final Class<?> targetType,
Class<?> functionalInterface) {
Map<TypeVariable<?>, Type> map = new HashMap<TypeVariable<?>, Type>();

// Populate interfaces
populateSuperTypeArgs(targetType.getGenericInterfaces(), map, functionalInterface != null);

// Populate super classes and interfaces
Type genericType = targetType.getGenericSuperclass();
Class<?> type = targetType.getSuperclass();
while (type != null && !Object.class.equals(type)) {
if (genericType instanceof ParameterizedType)
populateTypeArgs((ParameterizedType) genericType, map, false);
populateSuperTypeArgs(type.getGenericInterfaces(), map, false);

genericType = type.getGenericSuperclass();
type = type.getSuperclass();
}

// Populate enclosing classes
type = targetType;
while (type.isMemberClass()) {
genericType = type.getGenericSuperclass();
if (genericType instanceof ParameterizedType)
populateTypeArgs((ParameterizedType) genericType, map, functionalInterface != null);

type = type.getEnclosingClass();
}

return field.getType();
return map;
}

/**
* Find a generic superclass of given class that matches declaring class of given field
*
* @param clazz concrete class
* @param field field
* @return superclass as ParameterizedType
* Populates the {@code map} with with variable/argument pairs for the given {@code types}.
*/
private static ParameterizedType findMatchingSuperclass(Class clazz, Field field) {
Type superclass = clazz.getGenericSuperclass();
if (superclass == null) {
return null;
private static void populateSuperTypeArgs(final Type[] types, final Map<TypeVariable<?>, Type> map,
boolean depthFirst) {
for (Type type : types) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
if (!depthFirst)
populateTypeArgs(parameterizedType, map, depthFirst);
Type rawType = parameterizedType.getRawType();
if (rawType instanceof Class)
populateSuperTypeArgs(((Class<?>) rawType).getGenericInterfaces(), map, depthFirst);
if (depthFirst)
populateTypeArgs(parameterizedType, map, depthFirst);
} else if (type instanceof Class) {
populateSuperTypeArgs(((Class<?>) type).getGenericInterfaces(), map, depthFirst);
}
}
}

/**
* Populates the {@code map} with variable/argument pairs for the given {@code type}.
*/
private static void populateTypeArgs(ParameterizedType type, Map<TypeVariable<?>, Type> map, boolean depthFirst) {
if (type.getRawType() instanceof Class) {
TypeVariable<?>[] typeVariables = ((Class<?>) type.getRawType()).getTypeParameters();
Type[] typeArguments = type.getActualTypeArguments();

if (type.getOwnerType() != null) {
Type owner = type.getOwnerType();
if (owner instanceof ParameterizedType)
populateTypeArgs((ParameterizedType) owner, map, depthFirst);
}

for (int i = 0; i < typeArguments.length; i++) {
TypeVariable<?> variable = typeVariables[i];
Type typeArgument = typeArguments[i];

if (superclass instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) superclass;
if (paramType.getRawType().equals(field.getDeclaringClass())) {
return paramType;
} else {
return findMatchingSuperclass(clazz.getSuperclass(), field);
if (typeArgument instanceof Class) {
map.put(variable, typeArgument);
} else if (typeArgument instanceof GenericArrayType) {
map.put(variable, typeArgument);
} else if (typeArgument instanceof ParameterizedType) {
map.put(variable, typeArgument);
} else if (typeArgument instanceof TypeVariable) {
TypeVariable<?> typeVariableArgument = (TypeVariable<?>) typeArgument;
if (depthFirst) {
Type existingType = map.get(variable);
if (existingType != null) {
map.put(typeVariableArgument, existingType);
continue;
}
}

Type resolvedType = map.get(typeVariableArgument);
if (resolvedType == null)
resolvedType = resolveBound(typeVariableArgument);
map.put(variable, resolvedType);
}
}
} else {
return findMatchingSuperclass(clazz.getSuperclass(), field);
}
}

/**
* Resolves the first bound for the {@code typeVariable}, returning {@code Unknown.class} if none can be resolved.
*/
private static Type resolveBound(TypeVariable<?> typeVariable) {
Type[] bounds = typeVariable.getBounds();
if (bounds.length == 0)
return Unknown.class;

Type bound = bounds[0];
if (bound instanceof TypeVariable)
bound = resolveBound((TypeVariable<?>) bound);

return bound == Object.class ? Unknown.class : bound;
}

/**
* An unknown type.
*/
private static final class Unknown {
}

private GenericUtils() {
}
}
Loading

0 comments on commit 25f55e7

Please sign in to comment.