PropertyAccessor.java

/*
 *    Copyright 2018-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
 *
 *       https://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.mybatis.scripting.thymeleaf;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * The interface for accessing a property. <br>
 * If you want to customize a default {@code PropertyAccessor}, you implements class of this interface and you need to
 * specify to a {@link SqlGenerator}. <br>
 *
 * @author Kazuki Shimizu
 *
 * @version 1.0.2
 */
public interface PropertyAccessor {

  /**
   * Get property names of specified type.
   *
   * @param type
   *          a target type
   *
   * @return property names
   */
  Set<String> getPropertyNames(Class<?> type);

  /**
   * Get a property type of specified property.
   *
   * @param type
   *          a target type
   * @param name
   *          a property name
   *
   * @return a property type
   */
  Class<?> getPropertyType(Class<?> type, String name);

  /**
   * Get a property value from specified target object.
   *
   * @param target
   *          a target object
   * @param name
   *          a property name
   *
   * @return a property value
   */
  Object getPropertyValue(Object target, String name);

  /**
   * Set a property value to the specified target object.
   *
   * @param target
   *          a target object
   * @param name
   *          a property name
   * @param value
   *          a property value
   */
  void setPropertyValue(Object target, String name, Object value);

  /**
   * The built-in property accessors.
   */
  enum BuiltIn implements PropertyAccessor {

    /**
     * The implementation using Java Beans API provided by JDK.
     */
    STANDARD(new StandardPropertyAccessor());

    private final PropertyAccessor delegate;

    BuiltIn(PropertyAccessor delegate) {
      this.delegate = delegate;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<String> getPropertyNames(Class<?> type) {
      return delegate.getPropertyNames(type);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Class<?> getPropertyType(Class<?> type, String name) {
      return delegate.getPropertyType(type, name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getPropertyValue(Object target, String name) {
      return delegate.getPropertyValue(target, name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setPropertyValue(Object target, String name, Object value) {
      delegate.setPropertyValue(target, name, value);
    }

    static class StandardPropertyAccessor implements PropertyAccessor {

      private static Map<Class<?>, Map<String, PropertyDescriptor>> cache = new ConcurrentHashMap<>();

      /**
       * {@inheritDoc}
       */
      @Override
      public Set<String> getPropertyNames(Class<?> type) {
        return getPropertyDescriptors(type).keySet();
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public Class<?> getPropertyType(Class<?> type, String name) {
        return Optional.ofNullable(getPropertyDescriptors(type).get(name))
            .orElseThrow(() -> new IllegalArgumentException(String.format(
                "Does not get a property type because property '%s' not found on '%s' class.", name, type.getName())))
            .getPropertyType();
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public Object getPropertyValue(Object target, String name) {
        try {
          return Optional.ofNullable(getPropertyDescriptors(target.getClass()).get(name))
              .map(PropertyDescriptor::getReadMethod)
              .orElseThrow(() -> new IllegalArgumentException(
                  String.format("Does not get a property value because property '%s' not found on '%s' class.", name,
                      target.getClass().getName())))
              .invoke(target);
        } catch (IllegalAccessException | InvocationTargetException e) {
          throw new IllegalStateException(e);
        }
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public void setPropertyValue(Object target, String name, Object value) {
        try {
          Optional.ofNullable(getPropertyDescriptors(target.getClass()).get(name))
              .map(PropertyDescriptor::getWriteMethod)
              .orElseThrow(() -> new IllegalArgumentException(
                  String.format("Does not set a property value because property '%s' not found on '%s' class.", name,
                      target.getClass().getName())))
              .invoke(target, value);
        } catch (IllegalAccessException | InvocationTargetException e) {
          throw new IllegalStateException(e);
        }
      }

      /**
       * Clear cache.
       * <p>
       * This method use by internal processing.
       * </p>
       */
      static void clearCache() {
        cache.clear();
      }

      private static Map<String, PropertyDescriptor> getPropertyDescriptors(Class<?> type) {
        return cache.computeIfAbsent(type, key -> {
          try {
            BeanInfo beanInfo = Introspector.getBeanInfo(type);
            return Stream.of(beanInfo.getPropertyDescriptors()).filter(x -> !x.getName().equals("class"))
                .collect(Collectors.toMap(PropertyDescriptor::getName, v -> v));
          } catch (IntrospectionException e) {
            throw new IllegalStateException(e);
          } finally {
            Introspector.flushFromCaches(type);
          }
        });
      }

    }

  }

}