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

com.hazelcast.org.apache.calcite.util.ImmutableBeans Maven / Gradle / Ivy

There is a newer version: 5.5.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you 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 com.hazelcast.org.apache.calcite.util;

import com.hazelcast.org.apache.calcite.sql.validate.SqlConformance;
import com.hazelcast.org.apache.calcite.sql.validate.SqlConformanceEnum;

import com.hazelcast.com.google.common.collect.ImmutableMap;
import com.hazelcast.com.google.common.collect.ImmutableSortedMap;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/** Utilities for creating immutable beans. */
public class ImmutableBeans {
  private ImmutableBeans() {}

  /** Creates an immutable bean that implements a given interface. */
  public static  T create(Class beanClass) {
    if (!beanClass.isInterface()) {
      throw new IllegalArgumentException("must be interface");
    }
    final ImmutableSortedMap.Builder propertyNameBuilder =
        ImmutableSortedMap.naturalOrder();
    final ImmutableMap.Builder> handlers =
        ImmutableMap.builder();
    final Set requiredPropertyNames = new HashSet<>();

    // First pass, add "get" methods and build a list of properties.
    for (Method method : beanClass.getMethods()) {
      if (!Modifier.isPublic(method.getModifiers())) {
        continue;
      }
      final Property property = method.getAnnotation(Property.class);
      if (property == null) {
        continue;
      }
      final boolean hasNonnull = hasAnnotation(method, "javax.annotation.Nonnull");
      final Mode mode;
      final Object defaultValue = getDefault(method);
      final String methodName = method.getName();
      final String propertyName;
      if (methodName.startsWith("get")) {
        propertyName = methodName.substring("get".length());
        mode = Mode.GET;
      } else if (methodName.startsWith("is")) {
        propertyName = methodName.substring("is".length());
        mode = Mode.GET;
      } else if (methodName.startsWith("with")) {
        continue;
      } else {
        propertyName = methodName.substring(0, 1).toUpperCase(Locale.ROOT)
            + methodName.substring(1);
        mode = Mode.GET;
      }
      final Class propertyType = method.getReturnType();
      if (method.getParameterCount() > 0) {
        throw new IllegalArgumentException("method '" + methodName
            + "' has too many parameters");
      }
      final boolean required = property.required()
          || propertyType.isPrimitive()
          || hasNonnull;
      if (required) {
        requiredPropertyNames.add(propertyName);
      }
      propertyNameBuilder.put(propertyName, propertyType);
      final Object defaultValue2 =
          convertDefault(defaultValue, propertyName, propertyType);
      handlers.put(method, (bean, args) -> {
        switch (mode) {
        case GET:
          final Object v = bean.map.get(propertyName);
          if (v != null) {
            return v;
          }
          if (required && defaultValue == null) {
            throw new IllegalArgumentException("property '" + propertyName
                + "' is required and has no default value");
          }
          return defaultValue2;
        default:
          throw new AssertionError();
        }
      });
    }

    // Second pass, add "with" methods if they correspond to a property.
    final ImmutableMap propertyNames =
        propertyNameBuilder.build();
    for (Method method : beanClass.getMethods()) {
      if (!Modifier.isPublic(method.getModifiers())
          || method.isDefault()) {
        continue;
      }
      final Mode mode;
      final String propertyName;
      final String methodName = method.getName();
      if (methodName.startsWith("get")) {
        continue;
      } else if (methodName.startsWith("is")) {
        continue;
      } else if (methodName.startsWith("with")) {
        propertyName = methodName.substring("with".length());
        mode = Mode.WITH;
      } else if (methodName.startsWith("set")) {
        propertyName = methodName.substring("set".length());
        mode = Mode.SET;
      } else {
        continue;
      }
      final Class propertyClass = propertyNames.get(propertyName);
      if (propertyClass == null) {
        throw new IllegalArgumentException("cannot find property '"
            + propertyName + "' for method '" + methodName
            + "'; maybe add a method 'get" + propertyName + "'?'");
      }
      switch (mode) {
      case WITH:
        if (method.getReturnType() != beanClass) {
          throw new IllegalArgumentException("method '" + methodName
              + "' should return the bean class '" + beanClass
              + "', actually returns '" + method.getReturnType() + "'");
        }
        break;
      case SET:
        if (method.getReturnType() != void.class) {
          throw new IllegalArgumentException("method '" + methodName
              + "' should return void, actually returns '"
              + method.getReturnType() + "'");
        }
      }
      if (method.getParameterCount() != 1) {
        throw new IllegalArgumentException("method '" + methodName
            + "' should have one parameter, actually has "
            + method.getParameterCount());
      }
      final Class propertyType = propertyNames.get(propertyName);
      if (!method.getParameterTypes()[0].equals(propertyType)) {
        throw new IllegalArgumentException("method '" + methodName
            + "' should have parameter of type " + propertyType
            + ", actually has " + method.getParameterTypes()[0]);
      }
      final boolean required = requiredPropertyNames.contains(propertyName);
      handlers.put(method, (bean, args) -> {
        switch (mode) {
        case WITH:
          final Object v = bean.map.get(propertyName);
          final ImmutableMap.Builder mapBuilder;
          if (v != null) {
            if (v.equals(args[0])) {
              return bean.asBean();
            }
            // the key already exists; painstakingly copy all entries
            // except the one with this key
            mapBuilder = ImmutableMap.builder();
            bean.map.forEach((key, value) -> {
              if (!key.equals(propertyName)) {
                mapBuilder.put(key, value);
              }
            });
          } else {
            // the key does not exist; put the whole map into the builder
            mapBuilder = ImmutableMap.builder()
                .putAll(bean.map);
          }
          if (args[0] != null) {
            mapBuilder.put(propertyName, args[0]);
          } else {
            if (required) {
              throw new IllegalArgumentException("cannot set required "
                  + "property '" + propertyName + "' to null");
            }
          }
          final ImmutableMap map = mapBuilder.build();
          return bean.withMap(map).asBean();
        default:
          throw new AssertionError();
        }
      });
    }

    // Third pass, add default methods.
    for (Method method : beanClass.getMethods()) {
      if (method.isDefault()) {
        final MethodHandle methodHandle;
        try {
          methodHandle = Compatible.INSTANCE.lookupPrivate(beanClass)
              .unreflectSpecial(method, beanClass);
        } catch (Throwable throwable) {
          throw new RuntimeException("while binding method " + method,
              throwable);
        }
        handlers.put(method, (bean, args) -> {
          try {
            return methodHandle.bindTo(bean.asBean())
                .invokeWithArguments(args);
          } catch (RuntimeException | Error e) {
            throw e;
          } catch (Throwable throwable) {
            throw new RuntimeException("while invoking method " + method,
                throwable);
          }
        });
      }
    }

    handlers.put(getMethod(Object.class, "toString"),
        (bean, args) -> new TreeMap<>(bean.map).toString());
    handlers.put(getMethod(Object.class, "hashCode"),
        (bean, args) -> new TreeMap<>(bean.map).hashCode());
    handlers.put(getMethod(Object.class, "equals", Object.class),
        (bean, args) -> bean == args[0]
            // Use a little arg-swap trick because it's difficult to get inside
            // a proxy
            || beanClass.isInstance(args[0])
            && args[0].equals(bean.map)
            // Strictly, a bean should not equal a Map but it's convenient
            || args[0] instanceof Map
            && bean.map.equals(args[0]));
    return makeBean(beanClass, handlers.build(), ImmutableMap.of());
  }

  /** Looks for an annotation by class name.
   * Useful if you don't want to depend on the class
   * (e.g. "javax.annotation.Nonnull") at compile time. */
  private static boolean hasAnnotation(Method method, String className) {
    for (Annotation annotation : method.getDeclaredAnnotations()) {
      if (annotation.annotationType().getName().equals(className)) {
        return true;
      }
    }
    return false;
  }

  private static Object getDefault(Method method) {
    Object defaultValue = null;
    final IntDefault intDefault = method.getAnnotation(IntDefault.class);
    if (intDefault != null) {
      defaultValue = intDefault.value();
    }
    final BooleanDefault booleanDefault =
        method.getAnnotation(BooleanDefault.class);
    if (booleanDefault != null) {
      defaultValue = booleanDefault.value();
    }
    final StringDefault stringDefault =
        method.getAnnotation(StringDefault.class);
    if (stringDefault != null) {
      defaultValue = stringDefault.value();
    }
    final EnumDefault enumDefault =
        method.getAnnotation(EnumDefault.class);
    if (enumDefault != null) {
      defaultValue = enumDefault.value();
    }
    return defaultValue;
  }

  private static Object convertDefault(Object defaultValue, String propertyName,
      Class propertyType) {
    if (propertyType.equals(SqlConformance.class)) {
      // Workaround for SqlConformance because it is actually not a Enum.
      propertyType = SqlConformanceEnum.class;
    }
    if (defaultValue == null || !propertyType.isEnum()) {
      return defaultValue;
    }
    for (Object enumConstant : propertyType.getEnumConstants()) {
      if (((Enum) enumConstant).name().equals(defaultValue)) {
        return enumConstant;
      }
    }
    throw new IllegalArgumentException("property '" + propertyName
        + "' is an enum but its default value " + defaultValue
        + " is not a valid enum constant");
  }

  private static Method getMethod(Class aClass,
      String methodName, Class... parameterTypes) {
    try {
      return aClass.getMethod(methodName, parameterTypes);
    } catch (NoSuchMethodException e) {
      throw new AssertionError();
    }
  }

  private static  T makeBean(Class beanClass,
      ImmutableMap> handlers,
      ImmutableMap map) {
    return new BeanImpl<>(beanClass, handlers, map).asBean();
  }

  /** Is the method reading or writing? */
  private enum Mode {
    GET, SET, WITH
  }

  /** Handler for a particular method call; called with "this" and arguments.
   *
   * @param  Bean type */
  private interface Handler {
    Object apply(BeanImpl bean, Object[] args);
  }

  /** Property of a bean. Apply this annotation to the "get" method. */
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.METHOD)
  public @interface Property {
    /** Whether the property is required.
     *
     * 

Properties of type {@code int} and {@code boolean} are always * required. * *

If a property is required, it cannot be set to null. * If it has no default value, calling "get" will give a runtime exception. */ boolean required() default false; } /** Default value of an int property. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface IntDefault { int value(); } /** Default value of a boolean property of a bean. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface BooleanDefault { boolean value(); } /** Default value of a String property of a bean. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface StringDefault { String value(); } /** Default value of an enum property of a bean. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface EnumDefault { String value(); } /** Default value of a String or enum property of a bean that is null. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface NullDefault { } /** Implementation of an instance of a bean; stores property * values in a map, and also implements {@code InvocationHandler} * so that it can retrieve calls from a reflective proxy. * * @param Bean type */ private static class BeanImpl implements InvocationHandler { private final ImmutableMap> handlers; private final ImmutableMap map; private final Class beanClass; BeanImpl(Class beanClass, ImmutableMap> handlers, ImmutableMap map) { this.beanClass = beanClass; this.handlers = handlers; this.map = map; } public Object invoke(Object proxy, Method method, Object[] args) { final Handler handler = handlers.get(method); if (handler == null) { throw new IllegalArgumentException("no handler for method " + method); } return handler.apply(this, args); } /** Returns a copy of this bean that has a different map. */ BeanImpl withMap(ImmutableMap map) { return new BeanImpl(beanClass, handlers, map); } /** Wraps this handler in a proxy that implements the required * interface. */ T asBean() { return beanClass.cast( Proxy.newProxyInstance(beanClass.getClassLoader(), new Class[] {beanClass}, this)); } } }