Skip to content

Commit 57ce03b

Browse files
feat: Add alternative proxy implementation (#1790)
1 parent abee0bd commit 57ce03b

File tree

7 files changed

+606
-1
lines changed

7 files changed

+606
-1
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ task unitTest( type: Test ) {
198198
testLogging.exceptionFormat = 'full'
199199
filter {
200200
includeTestsMatching 'io.appium.java_client.internal.*'
201+
includeTestsMatching 'io.appium.java_client.proxy.*'
201202
}
202203
}
203204

docs/The-event_firing.md

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ since v8.0.0
33
# The purpose
44

55
This feature allows end user to organize the event logging on the client side.
6-
Also this feature may be useful in a binding with standard or custom reporting
6+
Also, this feature may be useful in a binding with standard or custom reporting
77
frameworks. The feature has been introduced first since Selenium API v4.
88

99
# The API
@@ -40,6 +40,7 @@ Listeners should implement WebDriverListener. It supports three types of events:
4040
To use this decorator you have to prepare a listener, create a decorator using this listener,
4141
decorate the original WebDriver instance with this decorator and use the new WebDriver instance
4242
created by the decorator instead of the original one:
43+
4344
```java
4445
WebDriver original = new AndroidDriver();
4546
// it is expected that MyListener class implements WebDriverListener
@@ -66,6 +67,7 @@ decorated.get("http://example.com/");
6667
WebElement header = decorated.findElement(By.tagName("h1"));
6768
// if an error happens during any of these calls the the onError event is fired
6869
```
70+
6971
The instance of WebDriver created by the decorator implements all the same interfaces
7072
as the original driver. A listener can subscribe to "specific" or "generic" events (or both).
7173
A "specific" event correspond to a single specific method, a "generic" event correspond to any
@@ -74,3 +76,81 @@ implement a method with a name derived from the target method to be watched. The
7476
for "before"-events receive the parameters passed to the decorated method. The listener
7577
methods for "after"-events receive the parameters passed to the decorated method as well as the
7678
result returned by this method.
79+
80+
## createProxy API (since Java Client 8.3.0)
81+
82+
This API is unique to Appium Java Client and does not exist in Selenium. The reason for
83+
its existence is the fact that the original event listeners API provided by Selenium is limited
84+
because it can only use interface types for decorator objects. For example, the code below won't
85+
work:
86+
87+
```java
88+
IOSDriver driver = new IOSDriver(new URL("http://doesnot.matter/"), new ImmutableCapabilities())
89+
{
90+
@Override
91+
protected void startSession(Capabilities capabilities)
92+
{
93+
// Override in a sake of simplicity to avoid the actual session start
94+
}
95+
};
96+
WebDriverListener webDriverListener = new WebDriverListener()
97+
{
98+
};
99+
IOSDriver decoratedDriver = (IOSDriver) new EventFiringDecorator(IOSDriver.class, webDriverListener).decorate(
100+
driver);
101+
```
102+
103+
The last line throws `ClassCastException` because `decoratedDriver` is of type `IOSDriver`,
104+
which is a class rather than an interface.
105+
See the issue [#1694](https://github.com/appium/java-client/issues/1694) for more
106+
details. In order to workaround this limitation a special proxy implementation has been created,
107+
which is capable of decorating class types:
108+
109+
```java
110+
import io.appium.java_client.proxy.MethodCallListener;
111+
import io.appium.java_client.proxy.NotImplementedException;
112+
113+
import static io.appium.java_client.proxy.Helpers.createProxy;
114+
115+
// ...
116+
117+
MethodCallListener listener = new MethodCallListener() {
118+
@Override
119+
public void beforeCall(Object target, Method method, Object[] args) {
120+
if (!method.getName().equals("get")) {
121+
throw new NotImplementedException();
122+
}
123+
acc.append("beforeCall ").append(method.getName()).append("\n");
124+
}
125+
126+
@Override
127+
public void afterCall(Object target, Method method, Object[] args, Object result) {
128+
if (!method.getName().equals("get")) {
129+
throw new NotImplementedException();
130+
}
131+
acc.append("afterCall ").append(method.getName()).append("\n");
132+
}
133+
};
134+
135+
IOSDriver decoratedDriver = createProxy(
136+
IOSDriver.class,
137+
new Object[] {new URL("http://localhost:4723/"), new XCUITestOptions()},
138+
new Class[] {URL.class, Capabilities.class},
139+
listener
140+
);
141+
142+
decoratedDriver.get("http://example.com/");
143+
144+
assertThat(acc.toString().trim()).isEqualTo(
145+
String.join("\n",
146+
"beforeCall get",
147+
"afterCall get"
148+
)
149+
);
150+
```
151+
152+
This proxy is not tied to WebDriver descendants and could be used to any classes that have
153+
**public** constructors. It also allows to intercept exceptions thrown by **public** class methods and/or
154+
change/replace the original methods behavior. It is important to know that callbacks are **not** invoked
155+
for methods derived from the standard `Object` class, like `toString` or `equals`.
156+
Check [unit tests](../src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java) for more examples.
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* See the NOTICE file distributed with this work for additional
5+
* information regarding copyright ownership.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.appium.java_client.proxy;
18+
19+
import com.google.common.base.Preconditions;
20+
import net.bytebuddy.ByteBuddy;
21+
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
22+
import net.bytebuddy.implementation.MethodDelegation;
23+
import net.bytebuddy.matcher.ElementMatchers;
24+
25+
import java.util.Collection;
26+
import java.util.Collections;
27+
28+
public class Helpers {
29+
private Helpers() {
30+
}
31+
32+
/**
33+
* Creates a transparent proxy instance for the given class.
34+
* It is possible to provide one or more method execution listeners
35+
* or replace particular method calls completely. Callbacks
36+
* defined in these listeners are going to be called when any
37+
* **public** method of the given class is invoked. Overridden callbacks
38+
* are expected to be skipped if they throw
39+
* {@link io.appium.java_client.proxy.NotImplementedException}.
40+
*
41+
* @param cls the class to which the proxy should be created.
42+
* Must not be an interface.
43+
* @param constructorArgs Array of constructor arguments. Could be an
44+
* empty array if the class provides a constructor without arguments.
45+
* @param constructorArgTypes Array of constructor argument types. Must
46+
* represent types of constructorArgs.
47+
* @param listeners One or more method invocation listeners.
48+
* @param <T> Any class derived from Object
49+
* @return Proxy instance
50+
*/
51+
public static <T> T createProxy(
52+
Class<T> cls,
53+
Object[] constructorArgs,
54+
Class<?>[] constructorArgTypes,
55+
Collection<MethodCallListener> listeners
56+
) {
57+
Preconditions.checkArgument(constructorArgs.length == constructorArgTypes.length,
58+
String.format(
59+
"Constructor arguments array length %d must be equal to the types array length %d",
60+
constructorArgs.length, constructorArgTypes.length
61+
)
62+
);
63+
Preconditions.checkArgument(!listeners.isEmpty(), "The collection of listeners must not be empty");
64+
Preconditions.checkArgument(cls != null, "Class must not be null");
65+
Preconditions.checkArgument(!cls.isInterface(), "Class must not be an interface");
66+
67+
//noinspection resource
68+
Class<?> proxy = new ByteBuddy()
69+
.subclass(cls)
70+
.method(ElementMatchers.isPublic()
71+
.and(ElementMatchers.not(
72+
ElementMatchers.isDeclaredBy(Object.class)
73+
.or(ElementMatchers.isOverriddenFrom(Object.class))
74+
)))
75+
.intercept(MethodDelegation.to(Interceptor.class))
76+
.make()
77+
.load(cls.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
78+
.getLoaded()
79+
.asSubclass(cls);
80+
81+
try {
82+
//noinspection unchecked
83+
T instance = (T) proxy
84+
.getConstructor(constructorArgTypes)
85+
.newInstance(constructorArgs);
86+
Interceptor.LISTENERS.put(instance, listeners);
87+
return instance;
88+
} catch (SecurityException | ReflectiveOperationException e) {
89+
throw new IllegalStateException(String.format("Unable to create a proxy of %s", cls.getName()), e);
90+
}
91+
}
92+
93+
/**
94+
* Creates a transparent proxy instance for the given class.
95+
* It is possible to provide one or more method execution listeners
96+
* or replace particular method calls completely. Callbacks
97+
* defined in these listeners are going to be called when any
98+
* **public** method of the given class is invoked. Overridden callbacks
99+
* are expected to be skipped if they throw NotImplementedException.
100+
*
101+
* @param cls the class to which the proxy should be created.
102+
* Must not be an interface. Must expose a constructor
103+
* without arguments.
104+
* @param listeners One or more method invocation listeners.
105+
* @param <T> Any class derived from Object
106+
* @return Proxy instance
107+
*/
108+
public static <T> T createProxy(Class<T> cls, Collection<MethodCallListener> listeners) {
109+
return createProxy(cls, new Object[]{}, new Class[]{}, listeners);
110+
}
111+
112+
/**
113+
* Creates a transparent proxy instance for the given class.
114+
* It is possible to provide one or more method execution listeners
115+
* or replace particular method calls completely. Callbacks
116+
* defined in these listeners are going to be called when any
117+
* **public** method of the given class is invoked. Overridden callbacks
118+
* are expected to be skipped if they throw NotImplementedException.
119+
*
120+
* @param cls the class to which the proxy should be created.
121+
* Must not be an interface. Must expose a constructor
122+
* without arguments.
123+
* @param listener Method invocation listener.
124+
* @param <T> Any class derived from Object
125+
* @return Proxy instance
126+
*/
127+
public static <T> T createProxy(Class<T> cls, MethodCallListener listener) {
128+
return createProxy(cls, new Object[]{}, new Class[]{}, Collections.singletonList(listener));
129+
}
130+
131+
/**
132+
* Creates a transparent proxy instance for the given class.
133+
* It is possible to provide one or more method execution listeners
134+
* or replace particular method calls completely. Callbacks
135+
* defined in these listeners are going to be called when any
136+
* **public** method of the given class is invoked. Overridden callbacks
137+
* are expected to be skipped if they throw NotImplementedException.
138+
*
139+
* @param cls the class to which the proxy should be created.
140+
* Must not be an interface.
141+
* @param constructorArgs Array of constructor arguments. Could be an
142+
* empty array if the class provides a constructor without arguments.
143+
* @param constructorArgTypes Array of constructor argument types. Must
144+
* represent types of constructorArgs.
145+
* @param listener Method invocation listener.
146+
* @param <T> Any class derived from Object
147+
* @return Proxy instance
148+
*/
149+
public static <T> T createProxy(
150+
Class<T> cls,
151+
Object[] constructorArgs,
152+
Class<?>[] constructorArgTypes,
153+
MethodCallListener listener
154+
) {
155+
return createProxy(cls, constructorArgs, constructorArgTypes, Collections.singletonList(listener));
156+
}
157+
}

0 commit comments

Comments
 (0)