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

org.klojang.invoke.BeanReaderBuilder Maven / Gradle / Ivy

Go to download

Klojang Invoke is a Java module focused on path-based object access and dynamic invocation. Its central classes are the Path class and the PathWalker class. The Path class captures a path through an object graph. For example "employee.address.city". The PathWalker class lets you read from and write to a wide variety of types using Path objects.

There is a newer version: 21.1.0
Show newest version
package org.klojang.invoke;

import org.klojang.check.Check;
import org.klojang.util.InvokeException;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.HashMap;
import java.util.Map;

import static java.lang.Character.*;
import static java.lang.invoke.MethodType.methodType;
import static org.klojang.check.CommonChecks.empty;
import static org.klojang.check.CommonChecks.keyIn;

/**
 * A {@code Builder} class for {@link BeanReader} instances. Use this class if the bean
 * class resides in a Java 9+ module that does not allow reflective access to its classes,
 * or if you need or prefer 100% reflection-free bean readers for other reasons. You
 * obtain a {@code BeanReaderBuilder} instance through
 * {@link BeanReader#forClass(Class) BeanReader.forClass()}.
 *
 * @param  the type of the objects to be read by the {@code BeanReader}.
 */
public final class BeanReaderBuilder {

  private final Class clazz;

  private final Map getters = new HashMap<>();

  private BeanValueTransformer transformer = BeanValueTransformer.identity();

  BeanReaderBuilder(Class beanClass) {
    this.clazz = beanClass;
  }

  /**
   * Registers the specified names as {@code int} properties on the bean class.
   *
   * @param properties the property names
   * @return this instance
   */
  public BeanReaderBuilder withInt(String... properties) {
    return with(int.class, properties);
  }

  /**
   * Registers the specified names as {@code String} properties on the bean class.
   *
   * @param properties the property names
   * @return this instance
   */
  public BeanReaderBuilder withString(String... properties) {
    return with(String.class, properties);
  }

  /**
   * Registers the specified names as properties of the specified type. If the bean class
   * is a {@code record} type, there is no difference between calling this method and
   * calling {@link #withGetter(Class, String...) withGetter()}.
   *
   * @param type the type of the properties
   * @param properties the property names
   * @return this instance
   */
  public BeanReaderBuilder with(Class type, String... properties) {
    Check.notNull(type, "type");
    Check.notNull(properties, "properties");
    for (String prop : properties) {
      Check.on(InvokeException::new, prop)
            .isNot(keyIn(), getters, "duplicate property: \"${arg}\"");
      String method = getMethodNameFromProperty(prop, type);
      Getter getter = getGetter(prop, method, type);
      getters.put(prop, getter);
    }
    return this;
  }

  /**
   * Registers the specified method names as getters with the specified return type. You
   * can use this method to register getter-type methods (zero parameters, non-void return
   * type) with names that do not conform to the JavaBeans naming conventions. The
   * provided names are supposed to be complete method names of public getters on
   * the bean class. For example: "getLastName". If the bean class is a {@code record}
   * type, there is no difference between calling this method and calling
   * {@link #with(Class, String...) with()}.
   *
   * @param returnType the return type of the specified getters
   * @param names the names of the getters
   * @return this instance
   */
  public BeanReaderBuilder withGetter(Class returnType, String... names) {
    Check.notNull(returnType, "return type");
    Check.notNull(names, "names");
    for (String method : names) {
      String prop = getPropertyFromMethodName(method, returnType);
      Check.on(InvokeException::new, prop)
            .isNot(keyIn(), getters, "duplicate property: \"${arg}\"");
      Getter getter = getGetter(prop, method, returnType);
      getters.put(prop, getter);
    }
    return this;
  }

  public BeanReaderBuilder withTransformer(BeanValueTransformer transformer) {
    Check.notNull(transformer, Private.TRANSFORMER);
    this.transformer = transformer;
    return this;
  }

  /**
   * Returns a new {@code BeanReader} for instances of type {@code T}.
   *
   * @return a new {@code BeanReader} for instances of type {@code T}
   * @throws NoPublicGettersException if no properties have been added yet via the various
   * {@code with***} methods
   */
  public BeanReader build() throws NoPublicGettersException {
    Check.that(getters).isNot(empty(), () -> new NoPublicGettersException(clazz));
    return new BeanReader<>(clazz, Map.copyOf(getters), transformer);
  }

  private Getter getGetter(String property, String method, Class type) {
    try {
      MethodHandle mh = MethodHandles.publicLookup()
            .findVirtual(clazz, method, methodType(type));
      return new Getter(mh, property, type);
    } catch (NoSuchMethodException | IllegalAccessException e) {
      throw new InvokeException(e.toString());
    }
  }

  private String getPropertyFromMethodName(String method, Class type) {
    if (clazz.isRecord()) {
      return method;
    }
    if ((type == boolean.class || type == Boolean.class)
          && method.length() > 2
          && method.startsWith("is")
          && isUpperCase(method.charAt(2))) {
      return extractName(method, 2);
    } else if (method.length() > 3
          && method.startsWith("get")
          && isUpperCase(method.charAt(3))) {
      return extractName(method, 3);
    }
    return method;
  }

  private String getMethodNameFromProperty(String prop, Class type) {
    if (clazz.isRecord()) {
      return prop;
    }
    String methodName;
    if (type == boolean.class || type == Boolean.class) {
      if (prop.length() > 1) {
        methodName = "is" + toUpperCase(prop.charAt(0)) + prop.substring(1);
      } else {
        methodName = "is" + toUpperCase(prop.charAt(0));
      }
    } else if (prop.length() > 1) {
      methodName = "get" + toUpperCase(prop.charAt(0)) + prop.substring(1);
    } else {
      methodName = "get" + toUpperCase(prop.charAt(0));
    }
    return methodName;
  }

  private static String extractName(String n, int from) {
    StringBuilder sb = new StringBuilder(n.length() - 3);
    sb.append(n.substring(from));
    sb.setCharAt(0, toLowerCase(sb.charAt(0)));
    return sb.toString();
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy