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
+ }
+}