ValueExtractor.java

/*
 * Copyright 2019-2022 the original author or authors.
 *
 * 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
 *
 *      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.bremersee.comparator;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;

/**
 * The value extractor finds the value of a given field name or path by reflection.
 *
 * @author Christian Bremer
 */
public interface ValueExtractor {

  /**
   * Find the value of the given add name or path of the given object.
   *
   * @param obj the object
   * @param field the field name or path
   * @return the object
   * @throws ValueExtractorException if no add nor method is found
   */
  Object findValue(Object obj, String field);

  /**
   * Find field with the given name of the specified class.
   *
   * @param clazz the class
   * @param name the field name
   * @return the field
   */
  default Optional<Field> findField(final Class<?> clazz, final String name) {
    return findField(clazz, name, null);
  }

  /**
   * Find the field with the given name of the specified class.
   *
   * @param clazz the class
   * @param name the field name
   * @param type the type of the field
   * @return the field
   */
  default Optional<Field> findField(final Class<?> clazz, final String name,
      @SuppressWarnings("SameParameterValue") final Class<?> type) {
    Class<?> searchType = clazz;
    while (!Object.class.equals(searchType) && searchType != null) {
      Field[] fields = searchType.getDeclaredFields();
      for (Field field : fields) {
        if ((name == null || name.equals(field.getName())) && (type == null || type
            .equals(field.getType()))) {
          return Optional.of(field);
        }
      }
      searchType = searchType.getSuperclass();
    }
    return Optional.empty();
  }

  /**
   * Get possible method names of the given field name. The default implementation returns the field
   * name, it's getter for an object and a primitive boolean.
   *
   * <p>If '{@code firstName}' is given for example, '{@code firstName}', '{@code getFirstName}'
   * and  '{@code isFirstName}' will be returned.
   *
   * @param name the field name
   * @return the possible method names
   */
  default String[] getPossibleMethodNames(final String name) {
    if (name == null || name.isEmpty()) {
      return new String[0];
    }
    final String baseName;
    if (name.length() == 1) {
      baseName = name.toUpperCase();
    } else {
      baseName = name.substring(0, 1).toUpperCase() + name.substring(1);
    }
    String[] names = new String[3];
    names[0] = name;
    names[1] = "get" + baseName;
    names[2] = "is" + baseName;
    return names;
  }

  /**
   * Find the method with the given name and no parameters of the specified class.
   *
   * @param clazz the class
   * @param name the method name
   * @return the method
   */
  default Optional<Method> findMethod(final Class<?> clazz, final String name) {

    return Arrays.stream(getPossibleMethodNames(name))
        .map(methodName -> findMethod(clazz, methodName, new Class[0]).orElse(null))
        .filter(Objects::nonNull)
        .findFirst();
  }

  /**
   * Find the method with the given name and parameters of the specified class.
   *
   * @param clazz the class
   * @param name the method name
   * @param paramTypes the parameter types
   * @return the method
   */
  default Optional<Method> findMethod(
      final Class<?> clazz,
      final String name,
      final Class<?>... paramTypes) {

    Class<?> searchType = clazz;
    while (searchType != null) {
      Method[] methods =
          searchType.isInterface() ? searchType.getMethods() : searchType.getDeclaredMethods();
      for (Method method : methods) {
        if (name.equals(method.getName())
            && (paramTypes == null || Arrays.equals(paramTypes, method.getParameterTypes()))) {
          return Optional.of(method);
        }
      }
      searchType = searchType.getSuperclass();
    }
    return Optional.empty();
  }

  /**
   * Invoke the given method on the given object. If the method is not accessible, {@code
   * setAccessible(true)} will be called.
   *
   * @param method the method
   * @param obj the object
   * @return the return value of the method
   * @throws ValueExtractorException if a {@link IllegalAccessException} or a {@link
   *     InvocationTargetException} occurs
   */
  default Object invoke(final Method method, final Object obj) {
    try {
      if (!method.canAccess(obj)) {
        method.setAccessible(true);
      }
      return method.invoke(obj);
    } catch (IllegalAccessException | InvocationTargetException e) {
      throw new ValueExtractorException("Invoking method '" + method.getName() + "' failed.", e);
    }
  }

  /**
   * Invoke the given field on the given object. If the field is not accessible, {@code
   * setAccessible(true)} will be called.
   *
   * @param field the field
   * @param obj the object
   * @return the value of the field
   * @throws ValueExtractorException if a {@link IllegalAccessException} occurs
   */
  default Object invoke(final Field field, final Object obj) {
    if (!field.canAccess(obj)) {
      field.setAccessible(true);
    }
    try {
      return field.get(obj);
    } catch (IllegalAccessException e) {
      throw new ValueExtractorException("Getting value from add '" + field.getName()
          + "' failed", e);
    }
  }

}