001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package org.wpilib.util;
006
007import java.lang.reflect.Constructor;
008import java.util.List;
009import java.util.Optional;
010
011/**
012 * Utility class to find the best matching constructor for a given set of parameter types. The
013 * constructor must have parameter types that are assignable from the given parameter types, and the
014 * parameter types must not be assignable to each other. If multiple constructors match, the one
015 * with the most specific parameter types is chosen. If there is still a tie, the one with the most
016 * specific first parameter type is chosen, then the second parameter type, and so on.
017 *
018 * @param <T> the type of the class to find the constructor for
019 */
020public class ConstructorMatch<T> {
021  private final Constructor<T> m_constructor;
022  private final List<Class<?>> m_parameterTypes;
023
024  /**
025   * Constructs a ConstructorMatch with the given constructor and parameter types. The parameter
026   * types must not be assignable to each other.
027   *
028   * @param constructor the constructor to match
029   * @param parameterTypes the parameter types for the constructor
030   */
031  public ConstructorMatch(Constructor<T> constructor, Class<?>... parameterTypes) {
032    m_constructor = constructor;
033    m_parameterTypes = List.of(parameterTypes);
034    if (!isValidParameterPack(parameterTypes)) {
035      throw new IllegalArgumentException("Parameter types must not be assignable to each other");
036    }
037  }
038
039  private static boolean isValidParameterPack(Class<?>... types) {
040    // Verify that all of the parameter types are not assignable to each other
041    for (int i = 0; i < types.length; i++) {
042      // Don't allow object parameters, as they would match any parameter type
043      // and prevent more specific matches from being found
044      if (types[i].equals(Object.class)) {
045        return false;
046      }
047
048      for (int j = i + 1; j < types.length; j++) {
049        if (types[i].isAssignableFrom(types[j]) || types[j].isAssignableFrom(types[i])) {
050          return false;
051        }
052      }
053    }
054    return true;
055  }
056
057  /**
058   * Creates a new instance of the constructor's class using the given arguments. The arguments must
059   * match the parameter types of the constructor, and must not be assignable to each other. The
060   * order of the arguments does matter, as they will be matched to the constructor parameter types
061   * in order.
062   *
063   * @param args the arguments to pass to the constructor
064   * @return a new instance of the constructor's class
065   * @throws ReflectiveOperationException if the constructor cannot be invoked
066   */
067  public T newInstance(Object... args) throws ReflectiveOperationException {
068    Object[] parameterArgs = new Object[m_parameterTypes.size()];
069    // Find the incoming argument that matches each parameter type
070    for (int i = 0; i < m_parameterTypes.size(); i++) {
071      boolean found = false;
072      for (Object arg : args) {
073        if (m_parameterTypes.get(i).isAssignableFrom(arg.getClass())) {
074          parameterArgs[i] = arg;
075          found = true;
076          break;
077        }
078      }
079      if (!found) {
080        throw new IllegalArgumentException(
081            "No argument found for parameter type " + m_parameterTypes.get(i));
082      }
083    }
084    return m_constructor.newInstance(parameterArgs);
085  }
086
087  /**
088   * Finds the best matching constructor for the given class and parameter types. The constructor
089   * must have parameter types that are assignable from the given parameter types, and the parameter
090   * types must not be assignable to each other. If multiple constructors match, the one with the
091   * most specific parameter types is chosen. If there is still a tie, the one with the most
092   * specific first parameter type is chosen, then the second parameter type, and so on. The order
093   * of the parameter types does matter, as they will be matched to the constructor parameter types
094   * in order.
095   *
096   * @param <T> the type of the class to find the constructor for
097   * @param clazz the class to find the constructor for
098   * @param parameterTypes the parameter types to match
099   * @return an Optional containing the best matching ConstructorMatch, or empty if no match is
100   *     found
101   */
102  public static <T> Optional<ConstructorMatch<T>> findBestConstructor(
103      Class<T> clazz, Class<?>... parameterTypes) {
104    if (!isValidParameterPack(parameterTypes)) {
105      return Optional.empty();
106    }
107    Constructor<T> bestCtor = null;
108    Class<?>[] bestParameterTypes = new Class<?>[parameterTypes.length];
109    @SuppressWarnings("unchecked")
110    Constructor<T>[] constructors = (Constructor<T>[]) clazz.getConstructors();
111    for (Constructor<T> constructor : constructors) {
112      Class<?>[] ctorParameterTypes = constructor.getParameterTypes();
113      if (ctorParameterTypes.length != parameterTypes.length) {
114        continue;
115      }
116      boolean matches = true;
117      for (int i = 0; i < parameterTypes.length; i++) {
118        // Don't allow object parameters, as they would match any parameter type and
119        // prevent more specific matches from being found
120        if (ctorParameterTypes[i].equals(Object.class)) {
121          matches = false;
122          break;
123        }
124        if (!ctorParameterTypes[i].isAssignableFrom(parameterTypes[i])) {
125          matches = false;
126          break;
127        }
128      }
129      if (!matches) {
130        continue;
131      }
132      boolean better = false;
133      if (bestCtor == null) {
134        better = true;
135      } else {
136        // Check if this constructor is more specific than the best one found so far
137        // Order by parameter order so that if one constructor has a more specific
138        // parameter type for the first parameter, it is preferred over a constructor
139        // that has a more specific parameter type for the second parameter
140        for (int i = 0; i < parameterTypes.length; i++) {
141          if (ctorParameterTypes[i] != bestParameterTypes[i]) {
142            if (bestParameterTypes[i].isAssignableFrom(ctorParameterTypes[i])) {
143              better = true;
144            }
145            break;
146          }
147        }
148      }
149      if (better) {
150        bestCtor = constructor;
151        System.arraycopy(ctorParameterTypes, 0, bestParameterTypes, 0, parameterTypes.length);
152      }
153    }
154    return bestCtor == null
155        ? Optional.empty()
156        : Optional.of(new ConstructorMatch<>(bestCtor, bestParameterTypes));
157  }
158
159  /**
160   * Returns the constructor that was matched.
161   *
162   * @return the constructor that was matched
163   */
164  public Constructor<T> getConstructor() {
165    return m_constructor;
166  }
167
168  /**
169   * Returns the parameter types for the constructor.
170   *
171   * @return the parameter types for the constructor
172   */
173  public List<Class<?>> getParameterTypes() {
174    return m_parameterTypes;
175  }
176}