You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2020/01/06 08:12:04 UTC

[calcite] 06/07: [CALCITE-3328] Immutable beans, powered by reflection

This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit e88809e7288db9b7d33be8a67840e02a71327675
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Fri Sep 6 23:24:56 2019 -0700

    [CALCITE-3328] Immutable beans, powered by reflection
---
 .../org/apache/calcite/util/ImmutableBeans.java    | 386 ++++++++++++++++++
 .../org/apache/calcite/util/ImmutableBeanTest.java | 433 +++++++++++++++++++++
 2 files changed, 819 insertions(+)

diff --git a/core/src/main/java/org/apache/calcite/util/ImmutableBeans.java b/core/src/main/java/org/apache/calcite/util/ImmutableBeans.java
new file mode 100644
index 0000000..18954c9
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/util/ImmutableBeans.java
@@ -0,0 +1,386 @@
+/*
+ * 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 org.apache.calcite.util;
+
+import com.google.common.collect.ImmutableMap;
+import 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.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> T create(Class<T> beanClass) {
+    if (!beanClass.isInterface()) {
+      throw new IllegalArgumentException("must be interface");
+    }
+    final ImmutableSortedMap.Builder<String, Class> propertyNameBuilder =
+        ImmutableSortedMap.naturalOrder();
+    final ImmutableMap.Builder<Method, Handler<T>> handlers =
+        ImmutableMap.builder();
+    final Set<String> 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<String, Class> propertyNames =
+        propertyNameBuilder.build();
+    for (Method method : beanClass.getMethods()) {
+      if (!Modifier.isPublic(method.getModifiers())) {
+        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<String, Object> 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.<String, Object>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<String, Object> map = mapBuilder.build();
+          return bean.withMap(map).asBean();
+        default:
+          throw new AssertionError();
+        }
+      });
+    }
+
+    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 (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<Object> aClass,
+      String methodName, Class... parameterTypes) {
+    try {
+      return aClass.getMethod(methodName, parameterTypes);
+    } catch (NoSuchMethodException e) {
+      throw new AssertionError();
+    }
+  }
+
+  private static <T> T makeBean(Class<T> beanClass,
+      ImmutableMap<Method, Handler<T>> handlers,
+      ImmutableMap<String, Object> 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 <T> Bean type */
+  private interface Handler<T> {
+    Object apply(BeanImpl<T> 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.
+     *
+     * <p>Properties of type {@code int} and {@code boolean} are always
+     * required.
+     *
+     * <p>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 <T> Bean type */
+  private static class BeanImpl<T> implements InvocationHandler {
+    private final ImmutableMap<Method, Handler<T>> handlers;
+    private final ImmutableMap<String, Object> map;
+    private final Class<T> beanClass;
+
+    BeanImpl(Class<T> beanClass, ImmutableMap<Method, Handler<T>> handlers,
+        ImmutableMap<String, Object> 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<T> withMap(ImmutableMap<String, Object> map) {
+      return new BeanImpl<T>(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));
+    }
+  }
+}
diff --git a/core/src/test/java/org/apache/calcite/util/ImmutableBeanTest.java b/core/src/test/java/org/apache/calcite/util/ImmutableBeanTest.java
new file mode 100644
index 0000000..866a1ef
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/util/ImmutableBeanTest.java
@@ -0,0 +1,433 @@
+/*
+ * 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 org.apache.calcite.util;
+
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.TreeMap;
+import javax.annotation.Nonnull;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/** Unit test for {@link ImmutableBeans}. */
+public class ImmutableBeanTest {
+
+  @Test public void testSimple() {
+    final MyBean b = ImmutableBeans.create(MyBean.class);
+    assertThat(b.withFoo(1).getFoo(), is(1));
+    assertThat(b.withBar(false).isBar(), is(false));
+    assertThat(b.withBaz("a").getBaz(), is("a"));
+    assertThat(b.withBaz("a").withBaz("a").getBaz(), is("a"));
+
+    // Calling "with" on b2 does not change the "foo" property
+    final MyBean b2 = b.withFoo(2);
+    final MyBean b3 = b2.withFoo(3);
+    assertThat(b3.getFoo(), is(3));
+    assertThat(b2.getFoo(), is(2));
+
+    final MyBean b4 = b2.withFoo(3).withBar(true).withBaz("xyz");
+    final Map<String, Object> map = new TreeMap<>();
+    map.put("Foo", b4.getFoo());
+    map.put("Bar", b4.isBar());
+    map.put("Baz", b4.getBaz());
+    assertThat(b4.toString(), is(map.toString()));
+    assertThat(b4.hashCode(), is(map.hashCode()));
+    final MyBean b5 = b2.withFoo(3).withBar(true).withBaz("xyz");
+    assertThat(b4.equals(b5), is(true));
+    assertThat(b4.equals(b), is(false));
+    assertThat(b4.equals(b2), is(false));
+    assertThat(b4.equals(b3), is(false));
+  }
+
+  @Test public void testDefault() {
+    final Bean2 b = ImmutableBeans.create(Bean2.class);
+
+    // int, no default
+    try {
+      final int v = b.getIntSansDefault();
+      throw new AssertionError("expected error, got " + v);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(),
+          is("property 'IntSansDefault' is required and has no default value"));
+    }
+    assertThat(b.withIntSansDefault(4).getIntSansDefault(), is(4));
+
+    // int, with default
+    assertThat(b.getIntWithDefault(), is(1));
+    assertThat(b.withIntWithDefault(10).getIntWithDefault(), is(10));
+    assertThat(b.withIntWithDefault(1).getIntWithDefault(), is(1));
+
+    // boolean, no default
+    try {
+      final boolean v = b.isBooleanSansDefault();
+      throw new AssertionError("expected error, got " + v);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(),
+          is("property 'BooleanSansDefault' is required and has no default "
+              + "value"));
+    }
+    assertThat(b.withBooleanSansDefault(false).isBooleanSansDefault(),
+        is(false));
+
+    // boolean, with default
+    assertThat(b.isBooleanWithDefault(), is(true));
+    assertThat(b.withBooleanWithDefault(false).isBooleanWithDefault(),
+        is(false));
+    assertThat(b.withBooleanWithDefault(true).isBooleanWithDefault(),
+        is(true));
+
+    // string, no default
+    try {
+      final String v = b.getStringSansDefault();
+      throw new AssertionError("expected error, got " + v);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(),
+          is("property 'StringSansDefault' is required and has no default "
+              + "value"));
+    }
+    assertThat(b.withStringSansDefault("a").getStringSansDefault(), is("a"));
+
+    // string, no default
+    try {
+      final String v = b.getNonnullString();
+      throw new AssertionError("expected error, got " + v);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(),
+          is("property 'NonnullString' is required and has no default value"));
+    }
+    assertThat(b.withNonnullString("a").getNonnullString(), is("a"));
+
+    // string, with default
+    assertThat(b.getStringWithDefault(), is("abc"));
+    assertThat(b.withStringWithDefault("").getStringWithDefault(), is(""));
+    assertThat(b.withStringWithDefault("x").getStringWithDefault(), is("x"));
+    assertThat(b.withStringWithDefault("abc").getStringWithDefault(),
+        is("abc"));
+    try {
+      final Bean2 v = b.withStringWithDefault(null);
+      throw new AssertionError("expected error, got " + v);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(),
+          is("cannot set required property 'StringWithDefault' to null"));
+    }
+
+    // string, optional
+    assertThat(b.getOptionalString(), nullValue());
+    assertThat(b.withOptionalString("").getOptionalString(), is(""));
+    assertThat(b.withOptionalString("x").getOptionalString(), is("x"));
+    assertThat(b.withOptionalString("abc").getOptionalString(), is("abc"));
+    assertThat(b.withOptionalString(null).getOptionalString(), nullValue());
+
+    // string, optional
+    assertThat(b.getStringWithNullDefault(), nullValue());
+    assertThat(b.withStringWithNullDefault("").getStringWithNullDefault(),
+        is(""));
+    assertThat(b.withStringWithNullDefault("x").getStringWithNullDefault(),
+        is("x"));
+    assertThat(b.withStringWithNullDefault("abc").getStringWithNullDefault(),
+        is("abc"));
+    assertThat(b.withStringWithNullDefault(null).getStringWithNullDefault(),
+        nullValue());
+
+    // enum, with default
+    assertThat(b.getColorWithDefault(), is(Color.RED));
+    assertThat(b.withColorWithDefault(Color.GREEN).getColorWithDefault(),
+        is(Color.GREEN));
+    assertThat(b.withColorWithDefault(Color.RED).getColorWithDefault(),
+        is(Color.RED));
+    try {
+      final Bean2 v = b.withColorWithDefault(null);
+      throw new AssertionError("expected error, got " + v);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(),
+          is("cannot set required property 'ColorWithDefault' to null"));
+    }
+
+    // color, optional
+    assertThat(b.getColorOptional(), nullValue());
+    assertThat(b.withColorOptional(Color.RED).getColorOptional(),
+        is(Color.RED));
+    assertThat(b.withColorOptional(Color.RED).withColorOptional(null)
+        .getColorOptional(), nullValue());
+    assertThat(b.withColorOptional(null).getColorOptional(), nullValue());
+    assertThat(b.withColorOptional(Color.RED).withColorOptional(Color.GREEN)
+        .getColorOptional(), is(Color.GREEN));
+
+    // color, optional with null default
+    assertThat(b.getColorWithNullDefault(), nullValue());
+    assertThat(b.withColorWithNullDefault(null).getColorWithNullDefault(),
+        nullValue());
+    assertThat(b.withColorWithNullDefault(Color.RED).getColorWithNullDefault(),
+        is(Color.RED));
+    assertThat(b.withColorWithNullDefault(Color.RED)
+        .withColorWithNullDefault(null).getColorWithNullDefault(), nullValue());
+    assertThat(b.withColorWithNullDefault(Color.RED)
+        .withColorWithNullDefault(Color.GREEN).getColorWithNullDefault(),
+        is(Color.GREEN));
+
+    // Default values do not appear in toString().
+    // (Maybe they should... but then they'd be initial values?)
+    assertThat(b.toString(), is("{}"));
+
+    // Beans with values explicitly set are not equal to
+    // beans with the same values via defaults.
+    // (I could be persuaded that this is the wrong behavior.)
+    assertThat(b.equals(b.withIntWithDefault(1)), is(false));
+    assertThat(b.withIntWithDefault(1).equals(b.withIntWithDefault(1)),
+        is(true));
+    assertThat(b.withIntWithDefault(1).equals(b.withIntWithDefault(2)),
+        is(false));
+  }
+
+  private void check(Class<?> beanClass, Matcher<String> matcher) {
+    try {
+      final Object v = ImmutableBeans.create(beanClass);
+      fail("expected error, got " + v);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(), matcher);
+    }
+  }
+
+  @Test public void testValidate() {
+    check(BeanWhoseDefaultIsBadEnumValue.class,
+        is("property 'Color' is an enum but its default value YELLOW is not a "
+            + "valid enum constant"));
+    check(BeanWhoseWithMethodHasBadReturnType.class,
+        is("method 'withFoo' should return the bean class 'interface "
+            + "org.apache.calcite.util.ImmutableBeanTest$"
+            + "BeanWhoseWithMethodHasBadReturnType', actually returns "
+            + "'interface org.apache.calcite.util.ImmutableBeanTest$MyBean'"));
+    check(BeanWhoseWithMethodDoesNotMatchProperty.class,
+        is("method 'withFoo' should return the bean class 'interface "
+            + "org.apache.calcite.util.ImmutableBeanTest$"
+            + "BeanWhoseWithMethodDoesNotMatchProperty', actually returns "
+            + "'interface org.apache.calcite.util.ImmutableBeanTest$MyBean'"));
+    check(BeanWhoseWithMethodHasArgOfWrongType.class,
+        is("method 'withFoo' should return the bean class 'interface "
+            + "org.apache.calcite.util.ImmutableBeanTest$"
+            + "BeanWhoseWithMethodHasArgOfWrongType', actually returns "
+            + "'interface org.apache.calcite.util.ImmutableBeanTest$"
+            + "BeanWhoseWithMethodHasTooManyArgs'"));
+    check(BeanWhoseWithMethodHasTooManyArgs.class,
+        is("method 'withFoo' should have one parameter, actually has 2"));
+    check(BeanWhoseWithMethodHasTooFewArgs.class,
+        is("method 'withFoo' should have one parameter, actually has 0"));
+    check(BeanWhoseSetMethodHasBadReturnType.class,
+        is("method 'setFoo' should return void, actually returns "
+            + "'interface org.apache.calcite.util.ImmutableBeanTest$MyBean'"));
+    check(BeanWhoseGetMethodHasTooManyArgs.class,
+        is("method 'getFoo' has too many parameters"));
+    check(BeanWhoseSetMethodDoesNotMatchProperty.class,
+        is("cannot find property 'Foo' for method 'setFoo'; maybe add a method "
+            + "'getFoo'?'"));
+    check(BeanWhoseSetMethodHasArgOfWrongType.class,
+        is("method 'setFoo' should have parameter of type int, actually has "
+            + "float"));
+    check(BeanWhoseSetMethodHasTooManyArgs.class,
+        is("method 'setFoo' should have one parameter, actually has 2"));
+    check(BeanWhoseSetMethodHasTooFewArgs.class,
+        is("method 'setFoo' should have one parameter, actually has 0"));
+  }
+
+  /** Bean whose default value is not a valid value for the enum;
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseDefaultIsBadEnumValue {
+    @ImmutableBeans.Property
+    @ImmutableBeans.EnumDefault("YELLOW")
+    Color getColor();
+    BeanWhoseDefaultIsBadEnumValue withColor(Color color);
+  }
+
+  /** Bean that has a 'with' method that has a bad return type;
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseWithMethodHasBadReturnType {
+    @ImmutableBeans.Property int getFoo();
+    MyBean withFoo(int x);
+  }
+
+  /** Bean that has a 'with' method that does not correspond to a property
+   * (declared using a {@link ImmutableBeans.Property} annotation on a
+   * 'get' method;
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseWithMethodDoesNotMatchProperty {
+    @ImmutableBeans.Property int getFoo();
+    MyBean withFoo(int x);
+  }
+
+  /** Bean that has a 'with' method whose argument type is not the same as the
+   * type of the property (the return type of a 'get{PropertyName}' method);
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseWithMethodHasArgOfWrongType {
+    @ImmutableBeans.Property int getFoo();
+    BeanWhoseWithMethodHasTooManyArgs withFoo(float x);
+  }
+
+  /** Bean that has a 'with' method that has too many arguments;
+   * it should have just one;
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseWithMethodHasTooManyArgs {
+    @ImmutableBeans.Property int getFoo();
+    BeanWhoseWithMethodHasTooManyArgs withFoo(int x, int y);
+  }
+
+  /** Bean that has a 'with' method that has too few arguments;
+   * it should have just one;
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseWithMethodHasTooFewArgs {
+    @ImmutableBeans.Property int getFoo();
+    BeanWhoseWithMethodHasTooFewArgs withFoo();
+  }
+
+  /** Bean that has a 'set' method that has a bad return type;
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseSetMethodHasBadReturnType {
+    @ImmutableBeans.Property int getFoo();
+    MyBean setFoo(int x);
+  }
+
+  /** Bean that has a 'get' method that has one arg, whereas 'get' must have no
+   * args;
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseGetMethodHasTooManyArgs {
+    @ImmutableBeans.Property int getFoo(int x);
+    void setFoo(int x);
+  }
+
+  /** Bean that has a 'set' method that does not correspond to a property
+   * (declared using a {@link ImmutableBeans.Property} annotation on a
+   * 'get' method;
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseSetMethodDoesNotMatchProperty {
+    @ImmutableBeans.Property int getBar();
+    void setFoo(int x);
+  }
+
+  /** Bean that has a 'set' method whose argument type is not the same as the
+   * type of the property (the return type of a 'get{PropertyName}' method);
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseSetMethodHasArgOfWrongType {
+    @ImmutableBeans.Property int getFoo();
+    void setFoo(float x);
+  }
+
+  /** Bean that has a 'set' method that has too many arguments;
+   * it should have just one;
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseSetMethodHasTooManyArgs {
+    @ImmutableBeans.Property int getFoo();
+    void setFoo(int x, int y);
+  }
+
+  /** Bean that has a 'set' method that has too few arguments;
+   * it should have just one;
+   * used in {@link #testValidate()}. */
+  interface BeanWhoseSetMethodHasTooFewArgs {
+    @ImmutableBeans.Property int getFoo();
+    void setFoo();
+  }
+
+  // ditto setXxx
+
+    // TODO it is an error to declare an int property to be not required
+    // TODO it is an error to declare an boolean property to be not required
+
+  /** A simple bean with properties of various types, no defaults. */
+  interface MyBean {
+    @ImmutableBeans.Property
+    int getFoo();
+    MyBean withFoo(int x);
+
+    @ImmutableBeans.Property
+    boolean isBar();
+    MyBean withBar(boolean x);
+
+    @ImmutableBeans.Property
+    String getBaz();
+    MyBean withBaz(String s);
+  }
+
+  /** A bean class with just about every combination of default values
+   * missing and present, and required or not. */
+  interface Bean2 {
+    @ImmutableBeans.Property
+    @ImmutableBeans.IntDefault(1)
+    int getIntWithDefault();
+    Bean2 withIntWithDefault(int x);
+
+    @ImmutableBeans.Property
+    int getIntSansDefault();
+    Bean2 withIntSansDefault(int x);
+
+    @ImmutableBeans.Property
+    @ImmutableBeans.BooleanDefault(true)
+    boolean isBooleanWithDefault();
+    Bean2 withBooleanWithDefault(boolean x);
+
+    @ImmutableBeans.Property
+    boolean isBooleanSansDefault();
+    Bean2 withBooleanSansDefault(boolean x);
+
+    @ImmutableBeans.Property(required = true)
+    String getStringSansDefault();
+    Bean2 withStringSansDefault(String x);
+
+    @ImmutableBeans.Property
+    String getOptionalString();
+    Bean2 withOptionalString(String s);
+
+    /** Property is required because it has 'Nonnull' annotation. */
+    @ImmutableBeans.Property
+    @Nonnull String getNonnullString();
+    Bean2 withNonnullString(String s);
+
+    @ImmutableBeans.Property
+    @ImmutableBeans.StringDefault("abc")
+    @Nonnull String getStringWithDefault();
+    Bean2 withStringWithDefault(String s);
+
+    @ImmutableBeans.Property
+    @ImmutableBeans.NullDefault
+    String getStringWithNullDefault();
+    Bean2 withStringWithNullDefault(String s);
+
+    @ImmutableBeans.Property
+    @ImmutableBeans.EnumDefault("RED")
+    @Nonnull Color getColorWithDefault();
+    Bean2 withColorWithDefault(Color color);
+
+    @ImmutableBeans.Property
+    @ImmutableBeans.NullDefault
+    Color getColorWithNullDefault();
+    Bean2 withColorWithNullDefault(Color color);
+
+    @ImmutableBeans.Property()
+    Color getColorOptional();
+    Bean2 withColorOptional(Color color);
+  }
+
+  /** Red, blue, green. */
+  enum Color {
+    RED,
+    BLUE,
+    GREEN
+  }
+}