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 <[email protected]> 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 + } +}
