blob: 4fe5fe9039edde510307238cd8d9d5a788e3e877 [file]
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.util;
import static java.util.stream.Collectors.toMap;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BinaryOperator;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* Dynamic proxy dispatching method calls to multiple objects. It is aimed at
* creating objects from mixins. The class provides functionality similar to
* that of <code>net.sf.cglib.proxy.Mixin</code> class from the cglib library.
*
* Sample usage:
* {@snippet :
* interface Sailboat {
* default void trimSails() {
* }
* }
*
* interface WithMain {
* void trimMain();
* }
*
* interface WithJib {
* void trimJib();
* }
*
* interface Sloop extends Sailboat, WithMain, WithJib {
* @Override
* public default void trimSails() {
* System.out.println("On the sloop:");
* trimMain();
* trimJib();
* }
* }
*
* interface Catboat extends Sailboat, WithMain {
* @Override
* public default void trimSails() {
* System.out.println("On the catboat:");
* trimMain();
* }
* }
*
* final var withMain = new WithMain() {
* @Override
* public void trimMain() {
* System.out.println(" trim the main");
* }
* };
*
* final var withJib = new WithJib() {
* @Override
* public void trimJib() {
* System.out.println(" trim the jib");
* }
* };
*
* Sloop sloop = CompositeProxy.create(Sloop.class, new Sailboat() {
* }, withMain, withJib);
*
* Catboat catboat = CompositeProxy.create(Catboat.class, new Sailboat() {
* }, withMain);
*
* sloop.trimSails();
* catboat.trimSails();
* }
*
* Output:
*
* <pre>
* On the sloop:
* trim the main
* trim the jib
* On the cat:
* trim the main
* </pre>
*
* @see Proxy
*/
public final class CompositeProxy {
/**
* Builder of {@link CompositeProxy} instances.
*/
public static final class Builder {
/**
* Returns a proxy instance for the specified interface that dispatches method
* invocations to the specified handlers. Uses previously configured invocation
* tunnel and conflict resolver objects with the created proxy object.
*
* @param <T> the interface type
* @param interfaceType the interface class composite proxy instance should
* implement
* @param slices handlers for the method calls of the interface
* @return a new instance of {@link Proxy} implementing the given interface and
* dispatching the interface method invocations to the given handlers
*/
public <T> T create(Class<T> interfaceType, Object... slices) {
return CompositeProxy.createCompositeProxy(interfaceType, conflictResolver, invokeTunnel, slices);
}
/**
* Sets the method dispatch conflict resolver for this builder. The conflict
* resolver is used by composite proxy to select a method call handler from
* multiple candidates.
*
* @param v the conflict resolver for this builder or <code>null</code> if the
* default conflict resolver should be used
* @return this
*/
public Builder conflictResolver(BinaryOperator<Method> v) {
conflictResolver = v;
return this;
}
/**
* Sets the invocation tunnel for this builder.
*
* @param v the invocation tunnel for this builder or <code>null</code> if no
* invocation tunnel should be used
* @return this
*/
public Builder invokeTunnel(InvokeTunnel v) {
invokeTunnel = v;
return this;
}
private Builder() {}
private BinaryOperator<Method> conflictResolver = STANDARD_CONFLICT_RESOLVER;
private InvokeTunnel invokeTunnel;
}
/**
* Invocation tunnel. Must be used when building a composite proxy from objects
* that implement package-private interfaces to prevent
* {@link IllegalAccessException} exceptions being thrown by {@link Proxy}
* instances. Must be implemented by classes from packages with package-private
* interfaces used with {@link CompositeProxy} class.
*
* Assumed implementation:
* {@snippet :
*
* package org.foo;
*
* import java.lang.reflect.InvocationHandler;
* import java.lang.reflect.Method;
* import jdk.jpackage.internal.util.CompositeProxy;
*
* final class CompositeProxyTunnel implements CompositeProxy.InvokeTunnel {
*
* @Override
* public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
* return method.invoke(obj, args);
* }
*
* @Override
* public Object invokeDefault(Object proxy, Method method, Object[] args) throws Throwable {
* return InvocationHandler.invokeDefault(proxy, method, args);
* }
*
* static final CompositeProxyTunnel INSTANCE = new CompositeProxyTunnel();
* }
*/
public interface InvokeTunnel {
/**
* Processes a method invocation on an object of composite proxy and returns the result.
*
* @implNote Implementation should call the given method on the given object
* with the given arguments and return the result of the call.
* @param obj the object on which to invoke the method
* @param method the method to invoke
* @param args the arguments to use in the method call
* @return the result of the method call
* @throws Throwable if the method throws
*/
Object invoke(Object obj, Method method, Object[] args) throws Throwable;
/**
* Processes a default interface method invocation on a composite proxy and
* returns the result.
*
* @implNote Implementation should call
* {@link InvocationHandler#invokeDefault(Object, Method, Object...)}
* method on the given proxy object with the given arguments and
* return the result of the call.
* @param proxy the <code>proxy</code> parameter for
* {@link InvocationHandler#invokeDefault(Object, Method, Object...)}
* call
* @param method the <code>method</code> parameter for
* {@link InvocationHandler#invokeDefault(Object, Method, Object...)}
* call
* @param args the <code>args</code> parameter for
* {@link InvocationHandler#invokeDefault(Object, Method, Object...)}
* call
* @return the result of the
* {@link InvocationHandler#invokeDefault(Object, Method, Object...)}
* call
* @throws Throwable if the {@link InvocationHandler#invokeDefault(Object, Method, Object...)} call throws
*/
Object invokeDefault(Object proxy, Method method, Object[] args) throws Throwable;
}
/**
* Creates a new proxy builder.
* @return a new proxy builder
*/
public static Builder build() {
return new Builder();
}
/**
* Shortcut for
* <code>CompositeProxy.build().create(interfaceType, slices)</code>.
*
* @see Builder#create(Class, Object...)
*/
public static <T> T create(Class<T> interfaceType, Object... slices) {
return build().create(interfaceType, slices);
}
private CompositeProxy() {
}
private static <T> T createCompositeProxy(Class<T> interfaceType, BinaryOperator<Method> conflictResolver,
InvokeTunnel invokeTunnel, Object... slices) {
validateTypeIsInterface(interfaceType);
final var interfaces = interfaceType.getInterfaces();
List.of(interfaces).forEach(CompositeProxy::validateTypeIsInterface);
if (interfaces.length != slices.length) {
throw new IllegalArgumentException(
String.format("type %s must extend %d interfaces", interfaceType.getName(), slices.length));
}
final Map<Class<?>, Object> interfaceDispatch = createInterfaceDispatch(interfaces, slices);
final Map<Method, Handler> methodDispatch = getProxyableMethods(interfaceType).map(method -> {
var handler = createHandler(interfaceType, method, interfaceDispatch, conflictResolver, invokeTunnel);
if (handler != null) {
return Map.entry(method, handler);
} else {
return null;
}
}).filter(Objects::nonNull).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
@SuppressWarnings("unchecked")
T proxy = (T) Proxy.newProxyInstance(interfaceType.getClassLoader(), new Class<?>[] { interfaceType },
new CompositeProxyInvocationHandler(methodDispatch));
return proxy;
}
private static Map<Class<?>, Object> createInterfaceDispatch(Class<?>[] interfaces, Object[] slices) {
final Map<Class<?>, Object> interfaceDispatch = Stream.of(interfaces).collect(toMap(x -> x, iface -> {
return Stream.of(slices).filter(obj -> {
return Set.of(obj.getClass().getInterfaces()).contains(iface);
}).reduce((a, b) -> {
throw new IllegalArgumentException(
String.format("both [%s] and [%s] slices implement %s", a, b, iface));
}).orElseThrow(() -> createInterfaceNotImplementedException(List.of(iface)));
}));
if (interfaceDispatch.size() != interfaces.length) {
final List<Class<?>> missingInterfaces = new ArrayList<>(Set.of(interfaces));
missingInterfaces.removeAll(interfaceDispatch.keySet());
throw createInterfaceNotImplementedException(missingInterfaces);
}
return Stream.of(interfaces).flatMap(iface -> {
return unfoldInterface(iface).map(unfoldedIface -> {
return Map.entry(unfoldedIface, interfaceDispatch.get(iface));
});
}).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private static Stream<Class<?>> unfoldInterface(Class<?> interfaceType) {
return Stream.concat(Stream.of(interfaceType),
Stream.of(interfaceType.getInterfaces()).flatMap(CompositeProxy::unfoldInterface));
}
private static IllegalArgumentException createInterfaceNotImplementedException(
Collection<Class<?>> missingInterfaces) {
return new IllegalArgumentException(String.format("none of the slices implement %s", missingInterfaces));
}
private static void validateTypeIsInterface(Class<?> type) {
if (!type.isInterface()) {
throw new IllegalArgumentException(String.format("type %s must be an interface", type.getName()));
}
}
private static Handler createHandler(Class<?> interfaceType, Method method, Map<Class<?>, Object> interfaceDispatch,
BinaryOperator<Method> conflictResolver, InvokeTunnel invokeTunnel) {
final var methodDeclaringClass = method.getDeclaringClass();
if (!methodDeclaringClass.equals(interfaceType)) {
// The method is declared in one of the superinterfaces.
final var slice = interfaceDispatch.get(methodDeclaringClass);
if (isInvokeDefault(method, slice)) {
return createHandlerForDefaultMethod(method, invokeTunnel);
} else {
return createHandlerForMethod(slice, method, invokeTunnel);
}
} else if (method.isDefault()) {
return createHandlerForDefaultMethod(method, invokeTunnel);
} else {
// Find a slice handling the method.
var handler = interfaceDispatch.entrySet().stream().map(e -> {
try {
Class<?> iface = e.getKey();
Object slice = e.getValue();
return createHandlerForMethod(slice, iface.getMethod(method.getName(), method.getParameterTypes()),
invokeTunnel);
} catch (NoSuchMethodException ex) {
return null;
}
}).filter(Objects::nonNull).reduce(new ConflictResolverAdapter(conflictResolver)).orElseThrow(() -> {
return new IllegalArgumentException(String.format("none of the slices can handle %s", method));
});
return handler;
}
}
private static Stream<Method> getProxyableMethods(Class<?> interfaceType) {
return Stream.of(interfaceType.getMethods()).filter(method -> !Modifier.isStatic(method.getModifiers()));
}
private static boolean isInvokeDefault(Method method, Object slice) {
if (!method.isDefault()) {
return false;
}
// The "method" is default.
// See if is overridden by any non-abstract method in the "slice".
// If it is, InvocationHandler.invokeDefault() should not be used to call it.
final var sliceClass = slice.getClass();
final var methodOverriden = Stream.of(sliceClass.getMethods()).filter(Predicate.not(Predicate.isEqual(method)))
.filter(sliceMethod -> !Modifier.isAbstract(sliceMethod.getModifiers()))
.anyMatch(sliceMethod -> signatureEquals(sliceMethod, method));
return !methodOverriden;
}
private static boolean signatureEquals(Method a, Method b) {
if (!Objects.equals(a.getName(), b.getName()) || !Arrays.equals(a.getParameterTypes(), b.getParameterTypes())) {
return false;
}
return Objects.equals(a.getReturnType(), b.getReturnType());
}
private record CompositeProxyInvocationHandler(Map<Method, Handler> dispatch) implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
var handler = dispatch.get(method);
if (handler != null) {
return handler.invoke(proxy, args);
} else if (method.isDefault()) {
return InvocationHandler.invokeDefault(proxy, method, args);
} else {
handler = OBJECT_METHOD_DISPATCH.get(method);
if (handler != null) {
return handler.invoke(proxy, args);
} else {
throw new UnsupportedOperationException(String.format("No handler for %s", method));
}
}
}
private static String objectToString(Object obj) {
return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj));
}
private static boolean objectEquals(Object obj, Object other) {
return obj == other;
}
private static Method getMethod(Class<?> type, String methodName, Class<?>...paramaterTypes) {
try {
return type.getDeclaredMethod(methodName, paramaterTypes);
} catch (NoSuchMethodException|SecurityException ex) {
throw new InternalError(ex);
}
}
static class ObjectMethodHandler extends HandlerOfMethod {
ObjectMethodHandler(Method method) {
super(method);
}
@Override
public Object invoke(Object proxy, Object[] args) throws Throwable {
if (args == null) {
return method.invoke(null, proxy);
} else {
final var newArgs = new Object[args.length + 1];
newArgs[0] = proxy;
System.arraycopy(args, 0, newArgs, 1, args.length);
return method.invoke(null, newArgs);
}
}
}
private static final Map<Method, Handler> OBJECT_METHOD_DISPATCH = Map.of(
getMethod(Object.class, "toString"),
new ObjectMethodHandler(getMethod(CompositeProxyInvocationHandler.class, "objectToString", Object.class)),
getMethod(Object.class, "equals", Object.class),
new ObjectMethodHandler(getMethod(CompositeProxyInvocationHandler.class, "objectEquals", Object.class, Object.class)),
getMethod(Object.class, "hashCode"),
new ObjectMethodHandler(getMethod(System.class, "identityHashCode", Object.class))
);
}
private static HandlerOfMethod createHandlerForDefaultMethod(Method method, InvokeTunnel invokeTunnel) {
if (invokeTunnel != null) {
return new HandlerOfMethod(method) {
@Override
public Object invoke(Object proxy, Object[] args) throws Throwable {
return invokeTunnel.invokeDefault(proxy, this.method, args);
}
};
} else {
return null;
}
}
private static HandlerOfMethod createHandlerForMethod(Object obj, Method method, InvokeTunnel invokeTunnel) {
if (invokeTunnel != null) {
return new HandlerOfMethod(method) {
@Override
public Object invoke(Object proxy, Object[] args) throws Throwable {
return invokeTunnel.invoke(obj, this.method, args);
}
};
} else {
return new HandlerOfMethod(method) {
@Override
public Object invoke(Object proxy, Object[] args) throws Throwable {
return this.method.invoke(obj, args);
}
};
}
}
@FunctionalInterface
private interface Handler {
Object invoke(Object proxy, Object[] args) throws Throwable;
}
private abstract static class HandlerOfMethod implements Handler {
HandlerOfMethod(Method method) {
this.method = method;
}
protected final Method method;
}
private record ConflictResolverAdapter(BinaryOperator<Method> conflictResolver)
implements BinaryOperator<HandlerOfMethod> {
@Override
public HandlerOfMethod apply(HandlerOfMethod a, HandlerOfMethod b) {
var m = conflictResolver.apply(a.method, b.method);
if (m == a.method) {
return a;
} else if (m == b.method) {
return b;
} else {
throw new UnsupportedOperationException();
}
}
}
private static final BinaryOperator<Method> STANDARD_CONFLICT_RESOLVER = (a, b) -> {
if (a.isDefault() == b.isDefault()) {
throw new IllegalArgumentException(String.format("ambiguous choice between %s and %s", a, b));
} else if (!a.isDefault()) {
return a;
} else {
return b;
}
};
}