IuTypeKey.java
/*
* Copyright © 2025 Indiana University
* All rights reserved.
*
* BSD 3-Clause License
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* - Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package edu.iu.type;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.function.Supplier;
import edu.iu.IuIterable;
import edu.iu.IuObject;
/**
* Hash key for use with generic types.
*
* <p>
* Java controls instances of {@link Class}, but instance control for other
* implementations of the {@link Type} interface is not clearly defined. The JDK
* typically, but is not required to, produces a new instance each time a
* generic type is referred to. This class inspects the generic type to
* determine strict equality as:
* </p>
*
* <ul>
* <li>Same instances are equal. Recursive base case: all leaf nodes are
* {@link Class}; and {@link Class} is strictly a leaf node.</li>
* <li>Both {@code instanceof} matching exactly one of:
* <ul>
* <li>{@link ParameterizedType}
* <ul>
* <li>Recursively checks raw type and all type arguments</li>
* </ul>
* </li>
* <li>{@link TypeVariable}
* <ul>
* <li>Checks variable name</li>
* <li>Recursively checks bounds</li>
* </ul>
* </li>
* <li>{@link WildcardType}
* <ul>
* <li>Recursively check upper and lower bounds</li>
* </ul>
* </li>
* <li>{@link GenericArrayType}
* <ul>
* <li>Recursively check component type</li>
* </ul>
* </li>
* </ul>
* </li>
* </ul>
*/
public class IuTypeKey {
private static final Map<Type, IuTypeKey> KEY_CACHE = new WeakHashMap<>();
private static final ThreadLocal<IdentityHashMap<IuTypeKey, IuTypeKey>> LOOP = new ThreadLocal<>();
/**
* Distinguishes between kinds of generic types.
*
* <p>
* {@link Kind} loosely dictates internal structure for {@link IuTypeKey}.
* </p>
*/
public static enum Kind {
/**
* {@link Class}
*/
CLASS,
/**
* {@link ParameterizedType}
*/
PARAMETERIZED,
/**
* {@link TypeVariable}
*/
VARIABLE,
/**
* {@link WildcardType}
*/
WILDCARD,
/**
* {@link GenericArrayType}
*/
ARRAY
}
/**
* Refers to a type in the generic hierarchy of a <strong>reference
* class</strong> by {@link IuType#erase() erasure}.
*
* @param referenceClass <strong>reference class</strong>
* @param inheritedTypeErasure {@link IuType#erase() erasure}
* @return Generic type extended or implemented by the <strong>reference
* class</strong> with {@link IuType#erase() erasure}
* {@code == inheritedTypeErasure}
*/
public static Type referTo(final Class<?> referenceClass, final Class<?> inheritedTypeErasure) {
if (referenceClass == inheritedTypeErasure)
return referenceClass;
var classToCheck = referenceClass;
while (classToCheck != null) {
final var rawInterfaces = classToCheck.getInterfaces();
final var length = rawInterfaces.length;
if (length > 0)
for (var i = 0; i < length; i++)
if (rawInterfaces[i] == inheritedTypeErasure)
return classToCheck.getGenericInterfaces()[i];
final var rawSuperclass = classToCheck.getSuperclass();
if (inheritedTypeErasure == rawSuperclass)
return classToCheck.getGenericSuperclass();
else
classToCheck = rawSuperclass;
}
throw new IllegalArgumentException(
inheritedTypeErasure.getSimpleName() + " is not assignable from " + referenceClass.getSimpleName());
}
/**
* Gets a hash key for a generic type.
*
* @param type generic type
* @return unique hash key
*/
public static IuTypeKey of(final Type type) {
IuTypeKey key = KEY_CACHE.get(type);
if (key != null)
return key;
if (type instanceof Class rawClass)
return new IuTypeKey(Kind.CLASS, type, rawClass, null, null);
if (type instanceof ParameterizedType parameterizedType)
return new IuTypeKey(Kind.PARAMETERIZED, type, null, null, () -> {
final var actualTypeArguments = parameterizedType.getActualTypeArguments();
final var length = actualTypeArguments.length;
final var children = new IuTypeKey[length + 1];
children[0] = of(parameterizedType.getRawType());
for (var i = 0; i < length; i++)
children[i + 1] = of(actualTypeArguments[i]);
return children;
});
if (type instanceof WildcardType wildcardType)
return new IuTypeKey(Kind.WILDCARD, type, null, null, () -> {
final var upperBounds = wildcardType.getUpperBounds();
final var upperBoundLimit = upperBounds.length;
final var lowerBounds = wildcardType.getLowerBounds();
final var lowerBoundLimit = lowerBounds.length;
final var length = upperBoundLimit + lowerBoundLimit;
final var children = new IuTypeKey[length];
for (var i = 0; i < upperBoundLimit; i++)
children[i] = of(upperBounds[i]);
for (var i = 0; i < lowerBoundLimit; i++)
children[upperBoundLimit + i] = of(lowerBounds[i]);
return children;
});
if (type instanceof TypeVariable<?> typeVariable)
return new IuTypeKey(Kind.VARIABLE, type, null, typeVariable.getName(), () -> {
final var bounds = typeVariable.getBounds();
final var length = bounds.length;
final var children = new IuTypeKey[length];
for (var i = 0; i < length; i++)
children[i] = of(bounds[i]);
return children;
});
if (type instanceof GenericArrayType genericArrayType)
return new IuTypeKey(Kind.ARRAY, type, null, null,
() -> new IuTypeKey[] { of(genericArrayType.getGenericComponentType()) });
throw new UnsupportedOperationException("Not supported in this version: " + type.getClass().getSimpleName());
}
private final Kind kind;
private final Class<?> raw;
private final String name;
private final IuTypeKey[] children;
private IuTypeKey(Kind kind, Type type, Class<?> raw, String name, Supplier<IuTypeKey[]> children) {
synchronized (KEY_CACHE) {
KEY_CACHE.put(type, this);
}
this.kind = kind;
this.raw = raw;
this.name = name;
if (children == null)
this.children = null;
else
this.children = children.get();
}
private <T> T preventLoop(Supplier<T> supplier, T baseCase) {
final var loop = LOOP.get();
try {
if (loop == null)
LOOP.set(new IdentityHashMap<>(Map.of(this, this)));
else if (loop.containsKey(this))
return baseCase;
else
loop.put(this, this);
return supplier.get();
} finally {
if (loop == null)
LOOP.remove();
else
loop.remove(this);
}
}
@Override
public int hashCode() {
return preventLoop(() -> IuObject.hashCode(kind, name, raw, children), 0);
}
@Override
public boolean equals(Object obj) {
if (!IuObject.typeCheck(this, obj))
return false;
IuTypeKey other = (IuTypeKey) obj;
return kind == other.kind //
&& raw == other.raw //
&& IuObject.equals(name, other.name) //
&& IuObject.equals(children, other.children);
}
@Override
public String toString() {
if (kind == Kind.CLASS)
return "CLASS " + raw.getSimpleName();
final var sb = new StringBuilder(kind.name());
if (name != null)
sb.append(' ').append(name);
final var childrenToString = preventLoop(() -> IuIterable.print(IuIterable.iter(children)), "");
if (!childrenToString.isEmpty())
sb.append(' ').append(childrenToString);
return sb.toString();
}
}