DefaultValueExtractor.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.Method;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
 * The default value extractor supports field names and paths as described in {@link
 * org.bremersee.comparator.model.SortOrder}. A field name can be just the field name (for example
 * {@code firstName}), a method name, that is not generated by {@link
 * ValueExtractor#getPossibleMethodNames(String)} (for example {@code toString}) or a path of field
 * names separated by dots (.), for example {@code person.firstName}.
 *
 * @author Christian Bremer
 */
@ToString
@EqualsAndHashCode
public class DefaultValueExtractor implements ValueExtractor {

  private final boolean throwingException;

  /**
   * Instantiates a new default value extractor that will throw {@link ValueExtractorException}, if
   * the given field cannot be found.
   */
  public DefaultValueExtractor() {
    this(true);
  }

  /**
   * Instantiates a new default value extractor.
   *
   * @param throwingException if {@code true} and the given field cannot be found, {@link
   *     ValueExtractorException} will be thrown; otherwise {@code null} will be returned
   */
  public DefaultValueExtractor(boolean throwingException) {
    this.throwingException = throwingException;
  }

  @Override
  public Object findValue(Object obj, String fieldPath) {
    final String fieldIdentifier = trimFieldPath(fieldPath);
    if (obj == null || fieldIdentifier == null || fieldIdentifier.isEmpty()) {
      return obj;
    }

    final int index = fieldIdentifier.indexOf('.');
    final String fieldName = index < 0
        ? fieldIdentifier
        : fieldIdentifier.substring(0, index).trim();
    Object value;
    Optional<Field> field = findField(obj.getClass(), fieldName);
    if (field.isPresent()) {
      value = invoke(field.get(), obj);
    } else {
      Optional<Method> method = findMethod(obj.getClass(), fieldName);
      if (method.isPresent()) {
        value = invoke(method.get(), obj);
      } else if (throwingException) {
        throw new ValueExtractorException(
            "Field [" + fieldName + "] was not found on object [" + obj + "].");
      } else {
        value = null;
      }
    }

    return index < 0 ? value : findValue(value, fieldIdentifier.substring(index + 1));
  }

  private static String trimFieldPath(String field) {
    if (field == null) {
      return null;
    }
    String tmp = field
        .replace("..", ".")
        .trim();
    while (tmp.startsWith(".")) {
      tmp = tmp.substring(1).trim();
    }
    while (tmp.endsWith(".")) {
      tmp = tmp.substring(0, tmp.length() - 1).trim();
    }
    return tmp;
  }

}