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 2c78bac48bfde94c0c77e6e7bb1f95814c8b23c7 Author: Julian Hyde <[email protected]> AuthorDate: Tue Jan 14 12:07:13 2020 -0800 [CALCITE-3735] In ImmutableBeans, allow interfaces to have default methods Revive interface Compatible, which we deleted a while ago. --- .../java/org/apache/calcite/util/Compatible.java | 81 ++++++++++++++++++++++ .../org/apache/calcite/util/ImmutableBeans.java | 29 +++++++- .../org/apache/calcite/util/ImmutableBeanTest.java | 20 ++++++ 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/calcite/util/Compatible.java b/core/src/main/java/org/apache/calcite/util/Compatible.java new file mode 100644 index 0000000..97e5f16 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/Compatible.java @@ -0,0 +1,81 @@ +/* + * 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 java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** Compatibility layer. + * + * <p>Allows to use advanced functionality if the latest JDK or Guava version + * is present. + */ +public interface Compatible { + Compatible INSTANCE = new Factory().create(); + + /** Same as {@code MethodHandles#privateLookupIn()}. + * (On JDK 8, only {@link MethodHandles#lookup()} is available. */ + <T> MethodHandles.Lookup lookupPrivate(Class<T> clazz); + + /** Creates the implementation of Compatible suitable for the + * current environment. */ + class Factory { + Compatible create() { + return (Compatible) Proxy.newProxyInstance( + Compatible.class.getClassLoader(), + new Class<?>[] {Compatible.class}, (proxy, method, args) -> { + if (method.getName().equals("lookupPrivate")) { + // Use MethodHandles.privateLookupIn if it is available (JDK 9 + // and above) + @SuppressWarnings("rawtypes") + final Class<?> clazz = (Class) args[0]; + try { + final Method privateLookupMethod = + MethodHandles.class.getMethod("privateLookupIn", + Class.class, MethodHandles.Lookup.class); + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + return privateLookupMethod.invoke(null, clazz, lookup); + } catch (NoSuchMethodException e) { + return privateLookupJdk8(clazz); + } + } + return null; + }); + } + + /** Emulates MethodHandles.privateLookupIn on JDK 8; + * in later JDK versions, throws. */ + @SuppressWarnings("deprecation") + static <T> MethodHandles.Lookup privateLookupJdk8(Class<T> clazz) { + try { + final Constructor<MethodHandles.Lookup> constructor = + MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, + int.class); + if (!constructor.isAccessible()) { + constructor.setAccessible(true); + } + return constructor.newInstance(clazz, MethodHandles.Lookup.PRIVATE); + } catch (InstantiationException | IllegalAccessException + | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/core/src/main/java/org/apache/calcite/util/ImmutableBeans.java b/core/src/main/java/org/apache/calcite/util/ImmutableBeans.java index 18954c9..d9019a6 100644 --- a/core/src/main/java/org/apache/calcite/util/ImmutableBeans.java +++ b/core/src/main/java/org/apache/calcite/util/ImmutableBeans.java @@ -24,6 +24,7 @@ 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; @@ -112,7 +113,8 @@ public class ImmutableBeans { final ImmutableMap<String, Class> propertyNames = propertyNameBuilder.build(); for (Method method : beanClass.getMethods()) { - if (!Modifier.isPublic(method.getModifiers())) { + if (!Modifier.isPublic(method.getModifiers()) + || method.isDefault()) { continue; } final Mode mode; @@ -202,6 +204,31 @@ public class ImmutableBeans { }); } + // 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"), diff --git a/core/src/test/java/org/apache/calcite/util/ImmutableBeanTest.java b/core/src/test/java/org/apache/calcite/util/ImmutableBeanTest.java index 866a1ef..1540b7a 100644 --- a/core/src/test/java/org/apache/calcite/util/ImmutableBeanTest.java +++ b/core/src/test/java/org/apache/calcite/util/ImmutableBeanTest.java @@ -248,6 +248,11 @@ public class ImmutableBeanTest { is("method 'setFoo' should have one parameter, actually has 0")); } + @Test public void testDefaultMethod() { + assertThat(ImmutableBeans.create(BeanWithDefault.class) + .withChar('a').nTimes(2), is("aa")); + } + /** Bean whose default value is not a valid value for the enum; * used in {@link #testValidate()}. */ interface BeanWhoseDefaultIsBadEnumValue { @@ -430,4 +435,19 @@ public class ImmutableBeanTest { BLUE, GREEN } + + /** Bean interface that has a default method and one property. */ + interface BeanWithDefault { + default String nTimes(int x) { + if (x <= 0) { + return ""; + } + final char c = getChar(); + return c + nTimes(x - 1); + } + + @ImmutableBeans.Property + char getChar(); + BeanWithDefault withChar(char c); + } }
