Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions java/src/org/openqa/selenium/support/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ java_export(
":page-factory",
"//java/src/org/openqa/selenium/support/events",
"//java/src/org/openqa/selenium/support/locators",
"//java/src/org/openqa/selenium/support/proxy",
"//java/src/org/openqa/selenium/support/ui:clock",
"//java/src/org/openqa/selenium/support/ui:components",
"//java/src/org/openqa/selenium/support/ui:elements",
Expand Down
16 changes: 16 additions & 0 deletions java/src/org/openqa/selenium/support/proxy/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("@rules_jvm_external//:defs.bzl", "artifact")
load("//java:defs.bzl", "java_library")

java_library(
name = "proxy",
srcs = glob(["*.java"]),
visibility = [
"//java/src/org/openqa/selenium/support:__subpackages__",
"//java/test/org/openqa/selenium/support/proxy:__pkg__",
],
deps = [
artifact("com.google.guava:guava"),
"//java/src/org/openqa/selenium:core",
artifact("net.bytebuddy:byte-buddy"),
],
)
157 changes: 157 additions & 0 deletions java/src/org/openqa/selenium/support/proxy/Helpers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC 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.openqa.selenium.support.proxy;

import com.google.common.base.Preconditions;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;

import java.util.Collection;
import java.util.Collections;

public class Helpers {
private Helpers() {}

/**
* Creates a transparent proxy instance for the given class.
* It is possible to provide one or more method execution listeners
* or replace particular method calls completely. Callbacks
* defined in these listeners are going to be called when any
* **public** method of the given class is invoked. Overridden callbacks
* are expected to be skipped if they throw
* {@link org.openqa.selenium.support.proxy.NotImplementedException}.
*
* @param cls the class to which the proxy should be created.
* Must not be an interface.
* @param constructorArgs Array of constructor arguments. Could be an
* empty array if the class provides a constructor without arguments.
* @param constructorArgTypes Array of constructor argument types. Must
* represent types of constructorArgs.
* @param listeners One or more method invocation listeners.
* @return Proxy instance
* @param <T> Any class derived from Object
*/
public static <T> T createProxy(
Class<T> cls,
Object[] constructorArgs,
Class<?>[] constructorArgTypes,
Collection<MethodCallListener> listeners
) {
Preconditions.checkArgument(constructorArgs.length == constructorArgTypes.length,
String.format(
"Constructor arguments array length %d must be equal to the types array length %d",
constructorArgs.length, constructorArgTypes.length
)
);
Preconditions.checkArgument(!listeners.isEmpty(), "The collection of listeners must not be empty");
Preconditions.checkArgument(cls != null, "Class must not be null");
Preconditions.checkArgument(!cls.isInterface(), "Class must not be an interface");

//noinspection resource
Class<?> proxy = new ByteBuddy()
.subclass(cls)
.method(ElementMatchers.isPublic()
.and(ElementMatchers.not(
ElementMatchers.isDeclaredBy(Object.class)
.or(ElementMatchers.isOverriddenFrom(Object.class))
)))
.intercept(MethodDelegation.to(Interceptor.class))
.make()
.load(cls.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded()
.asSubclass(cls);

try {
//noinspection unchecked
T instance = (T) proxy
.getConstructor(constructorArgTypes)
.newInstance(constructorArgs);
Interceptor.LISTENERS.put(instance, listeners);
return instance;
} catch (SecurityException | ReflectiveOperationException e) {
throw new IllegalStateException(String.format("Unable to create a proxy of %s", cls.getName()), e);
}
}

/**
* Creates a transparent proxy instance for the given class.
* It is possible to provide one or more method execution listeners
* or replace particular method calls completely. Callbacks
* defined in these listeners are going to be called when any
* **public** method of the given class is invoked. Overridden callbacks
* are expected to be skipped if they throw NotImplementedException.
*
* @param cls the class to which the proxy should be created.
* Must not be an interface. Must expose a constructor
* without arguments.
* @param listeners One or more method invocation listeners.
* @return Proxy instance
* @param <T> Any class derived from Object
*/
public static <T> T createProxy(Class<T> cls, Collection<MethodCallListener> listeners) {
return createProxy(cls, new Object[]{}, new Class[] {}, listeners);
}

/**
* Creates a transparent proxy instance for the given class.
* It is possible to provide one or more method execution listeners
* or replace particular method calls completely. Callbacks
* defined in these listeners are going to be called when any
* **public** method of the given class is invoked. Overridden callbacks
* are expected to be skipped if they throw NotImplementedException.
*
* @param cls the class to which the proxy should be created.
* Must not be an interface. Must expose a constructor
* without arguments.
* @param listener Method invocation listener.
* @return Proxy instance
* @param <T> Any class derived from Object
*/
public static <T> T createProxy(Class<T> cls, MethodCallListener listener) {
return createProxy(cls, new Object[]{}, new Class[] {}, Collections.singletonList(listener));
}

/**
* Creates a transparent proxy instance for the given class.
* It is possible to provide one or more method execution listeners
* or replace particular method calls completely. Callbacks
* defined in these listeners are going to be called when any
* **public** method of the given class is invoked. Overridden callbacks
* are expected to be skipped if they throw NotImplementedException.
*
* @param cls the class to which the proxy should be created.
* Must not be an interface.
* @param constructorArgs Array of constructor arguments. Could be an
* empty array if the class provides a constructor without arguments.
* @param constructorArgTypes Array of constructor argument types. Must
* represent types of constructorArgs.
* @param listener Method invocation listener.
* @return Proxy instance
* @param <T> Any class derived from Object
*/
public static <T> T createProxy(
Class<T> cls,
Object[] constructorArgs,
Class<?>[] constructorArgTypes,
MethodCallListener listener
) {
return createProxy(cls, constructorArgs, constructorArgTypes, Collections.singletonList(listener));
}
}
114 changes: 114 additions & 0 deletions java/src/org/openqa/selenium/support/proxy/Interceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC 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.openqa.selenium.support.proxy;

import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Interceptor {
private static final Logger logger = Logger.getLogger(Interceptor.class.getName());
public static final Map<Object, Collection<MethodCallListener>> LISTENERS = new WeakHashMap<>();
private static final Set<String> OBJECT_METHOD_NAMES = Stream.of(Object.class.getMethods())
.map(Method::getName)
.collect(Collectors.toSet());

@RuntimeType
public static Object intercept(
@This Object self,
@Origin Method method,
@AllArguments Object[] args,
@SuperCall Callable<?> callable
) throws Throwable {
if (OBJECT_METHOD_NAMES.contains(method.getName())) {
return callable.call();
}
Collection<MethodCallListener> listeners = LISTENERS.get(self);
if (listeners == null || listeners.isEmpty()) {
return callable.call();
}

listeners.forEach(listener -> {
try {
listener.beforeCall(self, method, args);
} catch (NotImplementedException e) {
// ignore
} catch (Exception e) {
logger.log(Level.SEVERE, "Got an unexpected error in beforeCall listener", e);
}
});

final UUID noResult = UUID.randomUUID();
Object result = noResult;
for (MethodCallListener listener: listeners) {
try {
result = listener.call(self, method, args, callable);
break;
} catch (NotImplementedException e) {
// ignore
} catch (Exception e) {
try {
return listener.onError(self, method, args, e);
} catch (NotImplementedException e1) {
// ignore
}
throw e;
}
}
if (noResult.equals(result)) {
try {
result = callable.call();
} catch (Exception e) {
for (MethodCallListener listener: listeners) {
try {
return listener.onError(self, method, args, e);
} catch (NotImplementedException e1) {
// ignore
}
}
throw e;
}
}

final Object endResult = result == noResult ? null : result;
listeners.forEach(listener -> {
try {
listener.afterCall(self, method, args, endResult);
} catch (NotImplementedException e) {
// ignore
} catch (Exception e) {
logger.log(Level.SEVERE, "Got an unexpected error in afterCall listener", e);
}
});
return endResult;
}
}
85 changes: 85 additions & 0 deletions java/src/org/openqa/selenium/support/proxy/MethodCallListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC 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.openqa.selenium.support.proxy;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;

public interface MethodCallListener {

/**
* The callback to be invoked before any public method of the proxy is called.
* The implementation is not expected to throw any exceptions. If a
* runtime exception is thrown then it is going to be silently logged.
*
* @param obj The proxy instance
* @param method Method to be called
* @param args Array of method arguments
*/
default void beforeCall(Object obj, Method method, Object[] args) {
throw new NotImplementedException();
}

/**
* Override this callback in order to change/customize the behavior
* of a single or multiple methods. The original method result
* will be replaced with the result returned by this callback.
* Also, any exception thrown by it will replace original method(s)
* exception.
*
* @param obj The proxy instance
* @param method Method to be replaced
* @param args Array of method arguments
* @param original The reference to the original method in case it is necessary to
* instrument its result.
* @return It is expected that the type of the returned argument could be cast to
* the returned type of the original method.
*/
default Object call(Object obj, Method method, Object[] args, Callable<?> original) throws Throwable {
throw new NotImplementedException();
}

/**
* The callback to be invoked after any public method of the proxy is called.
* The implementation is not expected to throw any exceptions. If a
* runtime exception is thrown then it is going to be silently logged.
*
* @param obj The proxy instance
* @param method Method to be called
* @param args Array of method arguments
*/
default void afterCall(Object obj, Method method, Object[] args, Object result) {
throw new NotImplementedException();
}

/**
* The callback to be invoked if a public method or its
* {@link #call(Object, Method, Object[], Callable) Call} replacement throws an exception.
*
* @param obj The proxy instance
* @param method Method to be called
* @param args Array of method arguments
* @param e Exception instance thrown by the original method invocation.
* @return You could either (re)throw the exception in this callback or
* overwrite the behavior and return a result from it. It is expected that the
* type of the returned argument could be cast to the returned type of the original method.
*/
default Object onError(Object obj, Method method, Object[] args, Throwable e) throws Throwable {
throw new NotImplementedException();
}
}
Loading