All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cedarsoftware.io.MetaUtils Maven / Gradle / Ivy

There is a newer version: 4.30.0
Show newest version
package com.cedarsoftware.io;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.Scanner;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.cedarsoftware.util.ClassUtilities;
import com.cedarsoftware.util.convert.Converter;

import static java.lang.reflect.Modifier.isProtected;
import static java.lang.reflect.Modifier.isPublic;

/**
 * This utility class has the methods mostly related to reflection related code.
 *
 * @author John DeRegnaucourt ([email protected])
 *         
* Copyright (c) Cedar Software LLC *

* 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 *

* License *

* 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. */ public class MetaUtils { private MetaUtils () {} enum Dumpty {} private static final ConcurrentMap constructors = new ConcurrentHashMap<>(); static final ThreadLocal dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")); private static boolean useUnsafe = false; private static Unsafe unsafe; private static final Map, Supplier> DIRECT_CLASS_MAPPING = new HashMap<>(); private static final Map, Supplier> ASSIGNABLE_CLASS_MAPPING = new LinkedHashMap<>(); private static final Pattern extraQuotes = Pattern.compile("^\"*(.*?)\"*$"); static { DIRECT_CLASS_MAPPING.put(Date.class, Date::new); DIRECT_CLASS_MAPPING.put(StringBuilder.class, StringBuilder::new); DIRECT_CLASS_MAPPING.put(StringBuffer.class, StringBuffer::new); DIRECT_CLASS_MAPPING.put(Locale.class, Locale::getDefault); DIRECT_CLASS_MAPPING.put(TimeZone.class, TimeZone::getDefault); DIRECT_CLASS_MAPPING.put(Timestamp.class, () -> new Timestamp(System.currentTimeMillis())); DIRECT_CLASS_MAPPING.put(java.sql.Date.class, () -> new java.sql.Date(System.currentTimeMillis())); DIRECT_CLASS_MAPPING.put(LocalDate.class, LocalDate::now); DIRECT_CLASS_MAPPING.put(LocalDateTime.class, LocalDateTime::now); DIRECT_CLASS_MAPPING.put(OffsetDateTime.class, OffsetDateTime::now); DIRECT_CLASS_MAPPING.put(ZonedDateTime.class, ZonedDateTime::now); DIRECT_CLASS_MAPPING.put(ZoneId.class, ZoneId::systemDefault); DIRECT_CLASS_MAPPING.put(AtomicBoolean.class, AtomicBoolean::new); DIRECT_CLASS_MAPPING.put(AtomicInteger.class, AtomicInteger::new); DIRECT_CLASS_MAPPING.put(AtomicLong.class, AtomicLong::new); DIRECT_CLASS_MAPPING.put(URL.class, () -> safelyIgnoreException(() -> new URL("http://localhost"), null)); DIRECT_CLASS_MAPPING.put(Object.class, Object::new); DIRECT_CLASS_MAPPING.put(String.class, () -> ""); DIRECT_CLASS_MAPPING.put(BigInteger.class, () -> BigInteger.ZERO); DIRECT_CLASS_MAPPING.put(BigDecimal.class, () -> BigDecimal.ZERO); DIRECT_CLASS_MAPPING.put(Class.class, () -> String.class); DIRECT_CLASS_MAPPING.put(Calendar.class, Calendar::getInstance); DIRECT_CLASS_MAPPING.put(Instant.class, Instant::now); // order is important ASSIGNABLE_CLASS_MAPPING.put(EnumSet.class, () -> null); ASSIGNABLE_CLASS_MAPPING.put(List.class, ArrayList::new); ASSIGNABLE_CLASS_MAPPING.put(NavigableSet.class, TreeSet::new); ASSIGNABLE_CLASS_MAPPING.put(SortedSet.class, TreeSet::new); ASSIGNABLE_CLASS_MAPPING.put(Set.class, LinkedHashSet::new); ASSIGNABLE_CLASS_MAPPING.put(NavigableMap.class, TreeMap::new); ASSIGNABLE_CLASS_MAPPING.put(SortedMap.class, TreeMap::new); ASSIGNABLE_CLASS_MAPPING.put(Map.class, LinkedHashMap::new); ASSIGNABLE_CLASS_MAPPING.put(Collection.class, ArrayList::new); ASSIGNABLE_CLASS_MAPPING.put(Calendar.class, Calendar::getInstance); ASSIGNABLE_CLASS_MAPPING.put(LinkedHashSet.class, LinkedHashSet::new); } /** * Globally turn on (or off) the 'unsafe' option of Class construction. The unsafe option * is used when all constructors have been tried and the Java class could not be instantiated. * @param state boolean true = on, false = off. */ public static void setUseUnsafe(boolean state) { useUnsafe = state; if (state) { try { unsafe = new Unsafe(); } catch (InvocationTargetException e) { useUnsafe = false; } } } public static Optional> getClassIfEnum(Class c) { if (c.isEnum()) { return Optional.of(c); } if (!Enum.class.isAssignableFrom(c)) { return Optional.empty(); } Class enclosingClass = c.getEnclosingClass(); return enclosingClass != null && enclosingClass.isEnum() ? Optional.of(enclosingClass) : Optional.empty(); } static void throwIfSecurityConcern(Class securityConcern, Class c) { if (securityConcern.isAssignableFrom(c)) { throw new JsonIoException("For security reasons, json-io does not allow instantiation of: " + securityConcern.getName()); } } static Object getArgForType(Converter converter, Class argType) { if (Primitives.isPrimitive(argType)) { return converter.convert(null, argType); // Get the defaults (false, 0, 0.0d, etc.) } Supplier directClassMapping = DIRECT_CLASS_MAPPING.get(argType); if (directClassMapping != null) { return directClassMapping.get(); } for (Map.Entry, Supplier> entry : ASSIGNABLE_CLASS_MAPPING.entrySet()) { if (entry.getKey().isAssignableFrom(argType)) { return entry.getValue().get(); } } if (argType.isArray()) { return Array.newInstance(argType.getComponentType(), 0); } return null; } /** * Build a List the same size of parameterTypes, where the objects in the list are ordered * to best match the parameters. Values from the passed in list are used only once or never. * @param values A list of potential arguments. This list can be smaller than parameterTypes * or larger. * @param parameterTypes A list of classes that the values will be matched against. * @return List of values that are best ordered to match the passed in parameter types. This * list will be the same length as the passed in parameterTypes list. */ public static List matchArgumentsToParameters(Converter converter, Collection values, Parameter[] parameterTypes, boolean useNull) { List answer = new ArrayList<>(); if (parameterTypes == null || parameterTypes.length == 0) { return answer; } List copyValues = new ArrayList<>(values); for (Parameter parameter : parameterTypes) { final Class paramType = parameter.getType(); Object value = pickBestValue(paramType, copyValues); if (value == null) { if (useNull) { value = paramType.isPrimitive() ? converter.convert(null, paramType) : null; // don't send null to a primitive parameter } else { value = getArgForType(converter, paramType); } } answer.add(value); } return answer; } /** * Pick the best value from the list that has the least 'distance' from the passed in Class 'param.' * Note: this method has a side effect - it will remove the value that was chosen from the list. * Note: If none of the instances in the 'values' list are instances of the 'param' class, * then the values list is not modified. * @param param Class driving the choice. * @param values List of potential argument values to pick from, that would best match the param (class). * @return a value from the 'values' list that best matched the 'param,' or null if none of the values * were assignable to the 'param'. */ private static Object pickBestValue(Class param, List values) { int[] distances = new int[values.size()]; int i=0; for (Object value : values) { distances[i++] = value == null ? -1 : ClassUtilities.computeInheritanceDistance(value.getClass(), param); } int index = indexOfSmallestValue(distances); if (index >= 0) { Object valueBestMatching = values.get(index); values.remove(index); return valueBestMatching; } else { return null; } } /** * Returns the index of the smallest value in an array. * @param array The array to search. * @return The index of the smallest value, or -1 if the array is empty. */ public static int indexOfSmallestValue(int[] array) { if (array == null || array.length == 0) { return -1; // Return -1 for null or empty array. } int minValue = Integer.MAX_VALUE; int minIndex = -1; for (int i = 0; i < array.length; i++) { if (array[i] < minValue && array[i] > -1) { minValue = array[i]; minIndex = i; } } return minIndex; } /** * Ideal class to hold all constructors for a Class, so that they are sorted in the most * appeasing construction order, in terms of public vs protected vs private. That could be * the same, so then it looks at values passed into the arguments, non-null being more * valuable than null, as well as number of argument types - more is better than fewer. */ private static class ConstructorWithValues implements Comparable { final Constructor constructor; final Object[] argsNull; final Object[] argsNonNull; ConstructorWithValues(Constructor constructor, Object[] argsNull, Object[] argsNonNull) { this.constructor = constructor; this.argsNull = argsNull; this.argsNonNull = argsNonNull; } public int compareTo(ConstructorWithValues other) { final int mods = constructor.getModifiers(); final int otherMods = other.constructor.getModifiers(); // Rule 1: Visibility: favor public over non-public if (!isPublic(mods) && isPublic(otherMods)) { return 1; } else if (isPublic(mods) && !isPublic(otherMods)) { return -1; } // Rule 2: Visibility: favor protected over private if (!isProtected(mods) && isProtected(otherMods)) { return 1; } else if (isProtected(mods) && !isProtected(otherMods)) { return -1; } // Rule 3: Sort by score of the argsNull list long score1 = scoreArgumentValues(argsNull); long score2 = scoreArgumentValues(other.argsNull); if (score1 < score2) { return 1; } else if (score1 > score2) { return -1; } // Rule 4: Sort by score of the argsNonNull list score1 = scoreArgumentValues(argsNonNull); score2 = scoreArgumentValues(other.argsNonNull); if (score1 < score2) { return 1; } else if (score1 > score2) { return -1; } // Rule 5: Favor by Class of parameter type alphabetically. Mainly, distinguish so that no constructors // are dropped from the Set. Although an "arbitrary" rule, it is consistent. String params1 = buildParameterTypeString(constructor); String params2 = buildParameterTypeString(other.constructor); return params1.compareTo(params2); } /** * The more non-null arguments you have, the higher your score. 100 points for each non-null argument. * 50 points for each parameter. So non-null values are twice as high (100 points versus 50 points) as * parameter "slots." */ private long scoreArgumentValues(Object[] args) { if (args.length == 0) { return 0L; } int nonNull = 0; for (Object arg : args) { if (arg != null) { nonNull++; } } return nonNull * 100L + args.length * 50L; } private String buildParameterTypeString(Constructor constructor) { Class[] paramTypes = constructor.getParameterTypes(); StringBuilder s = new StringBuilder(); for (Class paramType : paramTypes) { s.append(paramType.getName()).append("."); } return s.toString(); } } public static String createCacheKey(Class c, Collection args) { StringBuilder s = new StringBuilder(c.getName()); for (Object o : args) { if (o == null) { s.append(":null"); } else { s.append(':'); s.append(o.getClass().getSimpleName()); } } return s.toString(); } private static class CachedConstructor { private final Constructor constructor; private final boolean useNullSetting; CachedConstructor(Constructor constructor, boolean useNullSetting) { this.constructor = constructor; this.useNullSetting = useNullSetting; } } /** * Create a new instance of the passed in class c. You can optionally pass in argument values that will * be best-matched to a constructor on c. You can pass in null or an empty list, in which case, other * techniques will be used to attempt to instantiate the class. For security reasons, Process, ClassLoader, * ProcessBuilder, Constructor, Method, and Field cannot be instantiated. * @param c Class to instantiate. * @param argumentValues List of values to supply to a constructor on 'c'. The constructor chosen on 'c' * will be the one with a combination of the most fields that are satisfied with * non-null values from the 'argumentsValues.' The method will attempt to use values * from the list as constructor arguments for the passed in class c, ordering them * to best-fit the constructor, by matching the class type of the argument values * to the class types of the parameters on 'c' constructors. It will use all * constructors exhaustively, until it is successful. If not, then it will look at * the 'unsafe' setting and attempt to use that. * @return an instance of the passed in class. * @throws JsonIoException if it could not instantiate the passed in class. In that case, it is best to * create a ClassFactory for this specific class, and add that to the ReadOptions as an instantiator * that is associated to the class 'c'. In the ClassFactory, the JsonObject containing the data from the * associated JsonObject { } is passed in, allowing you to instantiate and load the values in one operation. * If you do that, and no further sub-objects exist, or you load the sub-objects in your ClassFactory, * make sure to return 'true' for isObjectFinal(). */ public static Object newInstance(Converter converter, Class c, Collection argumentValues) { throwIfSecurityConcern(ProcessBuilder.class, c); throwIfSecurityConcern(Process.class, c); throwIfSecurityConcern(ClassLoader.class, c); throwIfSecurityConcern(Constructor.class, c); throwIfSecurityConcern(Method.class, c); throwIfSecurityConcern(Field.class, c); // JDK11+ remove the line below if (c.getName().equals("java.lang.ProcessImpl")) { throw new IllegalArgumentException("For security reasons, json-io does not allow instantiation of: java.lang.ProcessImpl"); } if (argumentValues == null) { argumentValues = new ArrayList<>(); } final String cacheKey = createCacheKey(c, argumentValues); CachedConstructor cachedConstructor = constructors.get(cacheKey); if (cachedConstructor == null) { if (c.isInterface()) { throw new JsonIoException("Cannot instantiate unknown interface: " + c.getName()); } final Constructor[] declaredConstructors = c.getDeclaredConstructors(); Set constructorOrder = new TreeSet<>(); List argValues = new ArrayList<>(argumentValues); // Copy to allow destruction // Spin through all constructors, adding the constructor and the best match of arguments for it, as an // Object to a Set. The Set is ordered by ConstructorWithValues.compareTo(). for (Constructor constructor : declaredConstructors) { Parameter[] parameters = constructor.getParameters(); List argumentsNull = matchArgumentsToParameters(converter, argValues, parameters, true); List argumentsNonNull = matchArgumentsToParameters(converter, argValues, parameters, false); constructorOrder.add(new ConstructorWithValues(constructor, argumentsNull.toArray(), argumentsNonNull.toArray())); } for (ConstructorWithValues constructorWithValues : constructorOrder) { Constructor constructor = constructorWithValues.constructor; try { MetaUtils.trySetAccessible(constructor); Object o = constructor.newInstance(constructorWithValues.argsNull); // cache constructor search effort (null used for parameters of common types not matched to arguments) constructors.put(cacheKey, new CachedConstructor(constructor, true)); return o; } catch (Exception ignore) { try { if (constructor.getParameterCount() > 0) { // The no-arg constructor should only be tried one time. Object o = constructor.newInstance(constructorWithValues.argsNonNull); // cache constructor search effort (non-null used for parameters of common types not matched to arguments) constructors.put(cacheKey, new CachedConstructor(constructor, false)); return o; } } catch (Exception ignored) { } } } Object o = tryUnsafeInstantiation(c); if (o != null) { return o; } } else { List argValues = new ArrayList<>(argumentValues); // Copy to allow destruction Parameter[] parameters = cachedConstructor.constructor.getParameters(); List arguments = matchArgumentsToParameters(converter, argValues, parameters, cachedConstructor.useNullSetting); try { // Be nice to person debugging Object o = cachedConstructor.constructor.newInstance(arguments.toArray()); return o; } catch (Exception ignored) { } Object o = tryUnsafeInstantiation(c); if (o != null) { return o; } } throw new JsonIoException("Unable to instantiate: " + c.getName()); } // Try instantiation via unsafe (if turned on). It is off by default. Use // MetaUtils.setUseUnsafe(true) to enable it. This may result in heap-dumps // for e.g. ConcurrentHashMap or can cause problems when the class is not initialized, // that's why we try ordinary constructors first. private static Object tryUnsafeInstantiation(Class c) { if (useUnsafe) { try { Object o = unsafe.allocateInstance(c); return o; } catch (Exception ignored) {} } return null; } /** * Format a nice looking method signature for logging output */ public static String getLogMessage(String methodName, Object[] args) { return getLogMessage(methodName, args, 64); } public static String getLogMessage(String methodName, Object[] args, int argCharLen) { StringBuilder sb = new StringBuilder(); sb.append(methodName); sb.append('('); for (Object arg : args) { sb.append(getJsonStringToMaxLength(arg, argCharLen)); sb.append(" "); } String result = sb.toString().trim(); return result + ')'; } private static String getJsonStringToMaxLength(Object obj, int argCharLen) { WriteOptions options = new WriteOptionsBuilder().shortMetaKeys(true).showTypeInfoNever().build(); String arg = JsonIo.toJson(obj, options); if (arg.length() > argCharLen) { arg = arg.substring(0, argCharLen) + "..."; } return arg; } public static V getValueWithDefaultForNull(Map map, K key, V defaultValue) { V value = (V) map.get(key); return (value == null) ? defaultValue : value; } public static V getValueWithDefaultForMissing(Map map, K key, V defaultValue) { if (!map.containsKey(key)) { return defaultValue; } return (V) map.get(key); } public static void setFieldValue(Field field, Object instance, Object value) { try { if (instance == null) { throw new JsonIoException("Attempting to set field: " + field.getName() + " on null object."); } field.set(instance, value); } catch (IllegalAccessException e) { throw new JsonIoException("Cannot set field: " + field.getName() + " on class: " + instance.getClass().getName() + " as field is not accessible. Add a ClassFactory implementation to create the needed class, and use JsonReader.assignInstantiator() to associate your ClassFactory to the class: " + instance.getClass().getName(), e); } } public static void trySetAccessible(AccessibleObject object) { safelyIgnoreException(() -> object.setAccessible(true)); } public static T safelyIgnoreException(Callable callable, T defaultValue) { try { return callable.call(); } catch (Throwable e) { return defaultValue; } } public static void safelyIgnoreException(Runnable runnable) { try { runnable.run(); } catch (Throwable ignored) { } } /** * Use this method when you don't want a length check to * throw a NullPointerException when * * @param s string to return length of * @return 0 if string is null, otherwise the length of string. */ public static int length(final String s) { return s == null ? 0 : s.length(); } /** * Returns the length of the trimmed string. If the length is * null then it returns 0. */ public static int trimLength(final String s) { return (s == null) ? 0 : s.trim().length(); } /** * Legacy API that many applications consumed. */ public static boolean isPrimitive(Class c) { return Primitives.isPrimitive(c); } /** * Legacy API that many applications consumed. */ public static boolean isLogicalPrimitive(Class c) { return isPrimitive(c) || c.equals(String.class) || Number.class.isAssignableFrom(c) || Date.class.isAssignableFrom(c) || c.isEnum() || c.equals(Class.class); } static Set commaSeparatedStringToSet(String commaSeparatedString) { return Arrays.stream(commaSeparatedString.split(",")) .map(String::trim) .collect(Collectors.toSet()); } /** * Load in a Map-style properties file. Expects key and value to be separated by a = (whitespace ignored). * Ignores lines beginning with a # and it also ignores blank lines. * @param resName String name of the resource file. */ public static Map loadMapDefinition(String resName) { Map map = new LinkedHashMap<>(); try { String contents = MetaUtils.loadResourceAsString(resName); Scanner scanner = new Scanner(contents); while (scanner.hasNextLine()) { String line = scanner.nextLine(); if (!line.trim().startsWith("#") && !line.isEmpty()) { String[] parts = line.split("="); map.put(parts[0].trim(), parts[1].trim()); } } scanner.close(); } catch (Exception e) { throw new JsonIoException("Error reading in " + resName + ". The file should be in the resources folder. The contents are expected to have two strings separated by '='. You can use # or blank lines in the file, they will be skipped."); } return map; } /** * Load in a Set-style simple file of values. Expects values to be one per line. Ignores lines beginning with a # * and it also ignores blank lines. * @param resName String name of the resource file. * @return the set of strings */ public static Set loadSetDefinition(String resName) { Set set = new LinkedHashSet<>(); try { String contents = MetaUtils.loadResourceAsString(resName); Scanner scanner = new Scanner(contents); while (scanner.hasNextLine()) { String line = scanner.nextLine().trim(); if (!line.startsWith("#") && !line.isEmpty()) { set.add(line); } } scanner.close(); } catch (Exception e) { throw new JsonIoException("Error reading in " + resName + ". The file should be in the resources folder. The contents have a single String per line. You can use # (comment) or blank lines in the file, they will be skipped."); } return set; } /** * Loads resource content as a String. * @param resourceName Name of the resource file. * @return Content of the resource file as a String. */ public static String loadResourceAsString(String resourceName) { byte[] resourceBytes = loadResourceAsBytes(resourceName); return new String(resourceBytes, StandardCharsets.UTF_8); } /** * Loads resource content as a byte[]. * @param resourceName Name of the resource file. * @return Content of the resource file as a byte[]. */ public static byte[] loadResourceAsBytes(String resourceName) { try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName)) { if (inputStream == null) { throw new JsonIoException("Resource not found: " + resourceName); } return readInputStreamFully(inputStream); } catch (Exception e) { throw new JsonIoException("Error reading resource: " + resourceName, e); } } /** * Reads an InputStream fully and returns its content as a byte array. * * @param inputStream InputStream to read. * @return Content of the InputStream as byte array. * @throws IOException if an I/O error occurs. */ private static byte[] readInputStreamFully(InputStream inputStream) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] data = new byte[1024]; int nRead; while ((nRead = inputStream.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } buffer.flush(); return buffer.toByteArray(); } /** * Strip leading and trailing double quotes from the passed in String. If there are more than one * set of quotes, ""this is weird"" then all leading and trailing quotes will be removed, yielding * this is weird. Note that: ["""this is "really" weird""] will be: [this is "really" weird]. */ public static String removeLeadingAndTrailingQuotes(String input) { Matcher m = extraQuotes.matcher(input); if (m.find()) { input = m.group(1); } return input; } /** * Fetch the closest inherited class for the passed in Class. This method always fetches the closest class * or returns the default class, doing the complicated inheritance distance checking. This method is only * called when a cache miss has happened. A sentinel 'defaultClass' is returned when no class is found. * This prevents future cache misses from re-attempting to find classes that do not have a custom writer. * * @param clazz Class of object for which fetch the closest inherited class * @param workerClasses The classes to search through * @param defaultClass the class to return when no viable class was found. * @return Class the closest class found or defaultClass if none found. */ public static T findClosest(Class clazz, Map, T> workerClasses, T defaultClass) { T closest = defaultClass; int minDistance = Integer.MAX_VALUE; for (Map.Entry, T> entry : workerClasses.entrySet()) { Class clz = entry.getKey(); if (clz == clazz) { return entry.getValue(); } int distance = ClassUtilities.computeInheritanceDistance(clazz, clz); if (distance != -1 && distance < minDistance) { minDistance = distance; closest = entry.getValue(); } } return closest; } }