Skip to content

Commit 53f6015

Browse files
committed
GenericUtils: add handy getFieldTypeString method
This method gives you the field type declaration which you would need to use in Java source code as e.g. a method argument, which would be compile-time type compatible with the field in question in that context. Many thanks to rgettman on StackOverflow for taking the time to respond to my question about how to do this: http://stackoverflow.com/questions/28143029
1 parent 54200aa commit 53f6015

File tree

2 files changed

+214
-0
lines changed

2 files changed

+214
-0
lines changed

src/main/java/org/scijava/util/GenericUtils.java

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@
3535

3636
import java.lang.reflect.Field;
3737
import java.lang.reflect.Method;
38+
import java.lang.reflect.ParameterizedType;
3839
import java.lang.reflect.Type;
40+
import java.lang.reflect.TypeVariable;
3941
import java.util.List;
42+
import java.util.Stack;
4043

4144
/**
4245
* Useful methods for working with {@link Type} objects, particularly generic
@@ -225,4 +228,158 @@ public static Type getTypeParameter(final Type type, final Class<?> c,
225228
c.getTypeParameters()[paramNo]);
226229
}
227230

231+
/**
232+
* Obtains a string for the given field when viewed through the specified
233+
* subclass, which matches the syntax of Java source code.
234+
* <p>
235+
* Many thanks to <a
236+
* href="http://stackoverflow.com/questions/28143029">rgettman on
237+
* StackOverflow</a> for developing this logic.
238+
* </p>
239+
*
240+
* @param c The class from which to view the field in question.
241+
* @param field The field for which to generate a type string.
242+
* @return A type string describing the method, which matches the syntax of
243+
* Java source code.
244+
*/
245+
public static String getFieldTypeString(final Class<?> c, final Field field) {
246+
final Type genericType = field.getGenericType();
247+
// Declared as actual type name...
248+
if (genericType instanceof Class) {
249+
final Class<?> genericTypeClass = (Class<?>) genericType;
250+
return genericTypeClass.getName();
251+
}
252+
// .. or as a generic type?
253+
else if (genericType instanceof TypeVariable) {
254+
final TypeVariable<?> typeVariable = (TypeVariable<?>) genericType;
255+
final Class<?> declaringClass = field.getDeclaringClass();
256+
257+
// Create a Stack of classes going from c up to, but not including,
258+
// the declaring class.
259+
final Stack<Class<?>> stack = new Stack<Class<?>>();
260+
Class<?> currClass = c;
261+
while (!currClass.equals(declaringClass)) {
262+
stack.push(currClass);
263+
currClass = currClass.getSuperclass();
264+
}
265+
// Get the original type parameter from the declaring class.
266+
int typeVariableIndex = -1;
267+
String typeVariableName = typeVariable.getName();
268+
TypeVariable<?>[] currTypeParameters = currClass.getTypeParameters();
269+
for (int i = 0; i < currTypeParameters.length; i++) {
270+
final TypeVariable<?> currTypeVariable = currTypeParameters[i];
271+
if (currTypeVariable.getName().equals(typeVariableName)) {
272+
typeVariableIndex = i;
273+
break;
274+
}
275+
}
276+
277+
if (typeVariableIndex == -1) {
278+
throw new IllegalStateException("Expected Type variable \"" +
279+
typeVariable.getName() + "\" in class " + c +
280+
"; but it was not found.");
281+
}
282+
283+
// If the type parameter is from the same class, don't bother walking down
284+
// a non-existent hierarchy.
285+
if (declaringClass.equals(c)) {
286+
return getTypeVariableString(typeVariable);
287+
}
288+
289+
// Pop them in order, keeping track of which index is the type variable.
290+
while (!stack.isEmpty()) {
291+
currClass = stack.pop();
292+
// Must be ParameterizedType, not Class, because type arguments must be
293+
// supplied to the generic superclass.
294+
final ParameterizedType superclassParameterizedType =
295+
(ParameterizedType) currClass.getGenericSuperclass();
296+
final Type currType =
297+
superclassParameterizedType.getActualTypeArguments()[typeVariableIndex];
298+
if (currType instanceof Class) {
299+
// Type argument is an actual Class, e.g.
300+
// "extends ArrayList<Integer>".
301+
currClass = (Class<?>) currType;
302+
return currClass.getName();
303+
}
304+
else if (currType instanceof TypeVariable) {
305+
// e.g., "T"
306+
TypeVariable<?> currTypeVariable = (TypeVariable<?>) currType;
307+
typeVariableName = currTypeVariable.getName();
308+
// Reached passed-in class (bottom of hierarchy)? Report it.
309+
if (currClass.equals(c)) {
310+
return getTypeVariableString(currTypeVariable);
311+
}
312+
// Not at bottom? Find the type parameter to set up for next loop.
313+
typeVariableIndex = -1;
314+
currTypeParameters = currClass.getTypeParameters();
315+
for (int i = 0; i < currTypeParameters.length; i++) {
316+
currTypeVariable = currTypeParameters[i];
317+
if (currTypeVariable.getName().equals(typeVariableName)) {
318+
typeVariableIndex = i;
319+
break;
320+
}
321+
}
322+
323+
if (typeVariableIndex == -1) {
324+
// Shouldn't get here.
325+
throw new IllegalStateException("Expected Type variable \"" +
326+
typeVariable.getName() + "\" in class " + currClass.getName() +
327+
"; but it was not found.");
328+
}
329+
}
330+
else if (currType instanceof ParameterizedType) {
331+
final ParameterizedType pType = (ParameterizedType) currType;
332+
// e.g., "List<T>"
333+
return pType.toString();
334+
}
335+
}
336+
}
337+
// Shouldn't get here.
338+
final String genericTypeClassName = genericType == null ? "null" : genericType.getClass().getName();
339+
throw new IllegalStateException("Unsupported type of Type: " + genericTypeClassName);
340+
}
341+
342+
/** Gets a string describing a generic type parameter including its bounds. */
343+
public static String
344+
getTypeVariableString(final TypeVariable<?> typeVariable)
345+
{
346+
final StringBuilder buf = new StringBuilder();
347+
buf.append(typeVariable.getName());
348+
final Type[] bounds = typeVariable.getBounds();
349+
boolean first = true;
350+
// Don't report explicit "extends Object".
351+
if (bounds.length == 1 && bounds[0].equals(Object.class)) {
352+
return buf.toString();
353+
}
354+
for (final Type bound : bounds) {
355+
if (first) {
356+
buf.append(" extends ");
357+
first = false;
358+
}
359+
else {
360+
buf.append(" & ");
361+
}
362+
if (bound instanceof Class) {
363+
// e.g., "java.util.RandomAccess"
364+
final Class<?> boundClass = (Class<?>) bound;
365+
buf.append(boundClass.getName());
366+
}
367+
else if (bound instanceof TypeVariable) {
368+
// e.g., "T"
369+
final TypeVariable<?> typeVariableBound = (TypeVariable<?>) bound;
370+
buf.append(typeVariableBound.getName());
371+
}
372+
else if (bound instanceof ParameterizedType) {
373+
// e.g., "java.util.Collection<T>"
374+
final ParameterizedType pType = (ParameterizedType) bound;
375+
buf.append(pType.toString());
376+
}
377+
else {
378+
throw new IllegalStateException("Unknown bound type: " +
379+
bound.getClass().getName());
380+
}
381+
}
382+
return buf.toString();
383+
}
384+
228385
}

src/test/java/org/scijava/util/GenericUtilsTest.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.lang.reflect.Type;
4141
import java.util.HashMap;
4242
import java.util.List;
43+
import java.util.RandomAccess;
4344

4445
import org.junit.Test;
4546

@@ -135,6 +136,26 @@ public void testGetGenericType() {
135136
Serializable.class, Cloneable.class);
136137
}
137138

139+
@Test
140+
public void testGetFieldTypeString() {
141+
final Field f1 = ClassUtils.getField(Thing.class, "thing");
142+
assertFieldString("T", Thing.class, f1);
143+
assertFieldString("N extends java.lang.Number", NumberThing.class, f1);
144+
assertFieldString("java.lang.Integer", IntegerThing.class, f1);
145+
assertFieldString("java.lang.Integer", SpecificIntegerThing.class, f1);
146+
assertFieldString("java.util.List<T>", ListThing.class, f1);
147+
assertFieldString("java.util.List<Double>", ListDoubleThing.class, f1);
148+
assertFieldString(
149+
"RT extends org.scijava.util.GenericUtilsTest.RecursiveThing<RT>",
150+
RecursiveThing.class, f1);
151+
152+
final Field f2 = ClassUtils.getField(Superclass.class, "thing");
153+
assertFieldString("A extends java.io.Serializable", Superclass.class, f2);
154+
assertFieldString(
155+
"B extends java.util.RandomAccess & java.io.Serializable",
156+
Subclass.class, f2);
157+
}
158+
138159
// -- Helper classes --
139160

140161
private static class Thing<T> {
@@ -156,6 +177,36 @@ private static class ComplexThing<T extends Serializable & Cloneable> extends
156177
// NB: No implementation needed.
157178
}
158179

180+
private static class SpecificIntegerThing extends IntegerThing {
181+
// NB: No implementation needed.
182+
}
183+
184+
private static class ListThing<T> extends Thing<List<T>> {
185+
// NB: No implementation needed.
186+
}
187+
188+
private static class ListDoubleThing extends ListThing<Double> {
189+
// NB: No implementation needed.
190+
}
191+
192+
private static class RecursiveThing<RT extends RecursiveThing<RT>> extends
193+
Thing<RT>
194+
{
195+
// NB: No implementation needed.
196+
}
197+
198+
private static class Superclass<A extends Serializable, B> {
199+
@SuppressWarnings("unused")
200+
private A thing;
201+
}
202+
203+
/** NB: A and B are reversed in the extends clause! */
204+
private static class Subclass<A, B extends RandomAccess & Serializable>
205+
extends Superclass<B, A>
206+
{
207+
// NB: Marker class.
208+
}
209+
159210
// -- Helper methods --
160211

161212
/** Convenience method to get the {@link Type} of a field. */
@@ -185,4 +236,10 @@ private <T> void assertAllTheSame(final List<T> list, final T... values) {
185236
}
186237
}
187238

239+
private void assertFieldString(final String expected, final Class<?> c,
240+
final Field field)
241+
{
242+
assertEquals(expected, GenericUtils.getFieldTypeString(c, field));
243+
}
244+
188245
}

0 commit comments

Comments
 (0)