diff --git a/build.gradle b/build.gradle index 54be347a1..51e50d861 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,7 @@ java { ext { seleniumVersion = project.property('selenium.version') + appiumClientVersion = project.property('appiumClient.version') } dependencies { @@ -110,7 +111,7 @@ publishing { mavenJava(MavenPublication) { groupId = 'io.appium' artifactId = 'java-client' - version = '8.2.0' + version = appiumClientVersion from components.java pom { name = 'java-client' @@ -186,7 +187,8 @@ wrapper { processResources { filter ReplaceTokens, tokens: [ - 'selenium.version': seleniumVersion + 'selenium.version': seleniumVersion, + 'appiumClient.version': appiumClientVersion ] } diff --git a/gradle.properties b/gradle.properties index 9b086f575..239efd600 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,3 +8,5 @@ ossrhUsername=your-jira-id ossrhPassword=your-jira-password selenium.version=4.5.0 +# Please increment the value in a release +appiumClient.version=8.2.0 diff --git a/src/main/java/io/appium/java_client/AppiumClientConfig.java b/src/main/java/io/appium/java_client/AppiumClientConfig.java index 2e6128c03..317d85314 100644 --- a/src/main/java/io/appium/java_client/AppiumClientConfig.java +++ b/src/main/java/io/appium/java_client/AppiumClientConfig.java @@ -18,7 +18,6 @@ import org.openqa.selenium.Credentials; import org.openqa.selenium.internal.Require; -import org.openqa.selenium.remote.http.AddSeleniumUserAgent; import org.openqa.selenium.remote.http.ClientConfig; import org.openqa.selenium.remote.http.Filter; @@ -34,7 +33,7 @@ public class AppiumClientConfig extends ClientConfig { private final boolean directConnect; - private static final Filter DEFAULT_FILTER = new AddSeleniumUserAgent(); + private static final Filter DEFAULT_FILTER = new AppiumUserAgentFilter(); private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(10); diff --git a/src/main/java/io/appium/java_client/AppiumUserAgentFilter.java b/src/main/java/io/appium/java_client/AppiumUserAgentFilter.java new file mode 100644 index 000000000..a36da9d08 --- /dev/null +++ b/src/main/java/io/appium/java_client/AppiumUserAgentFilter.java @@ -0,0 +1,91 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HttpHeaders; +import io.appium.java_client.internal.Config; +import org.openqa.selenium.remote.http.AddSeleniumUserAgent; +import org.openqa.selenium.remote.http.Filter; +import org.openqa.selenium.remote.http.HttpHandler; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Manage Appium Client configurations. + */ + +public class AppiumUserAgentFilter implements Filter { + + public static final String VERSION_KEY = "appiumClient.version"; + + private static final String USER_AGENT_PREFIX = "appium/"; + + /** + * A default User Agent name for Appium Java client. + * e.g. appium/8.2.0 (selenium/4.5.0 (java mac)) + */ + public static final String USER_AGENT = buildUserAgentHeaderValue(AddSeleniumUserAgent.USER_AGENT); + + private static String buildUserAgentHeaderValue(@Nonnull String previousUA) { + return String.format("%s%s (%s)", + USER_AGENT_PREFIX, Config.main().getValue(VERSION_KEY, String.class), previousUA); + } + + /** + * Returns true if the given User Agent includes "appium/", which + * implies the User Agent already has the Appium UA by this method. + * The matching is case-insensitive. + * @param userAgent the User Agent in the request headers. + * @return whether the given User Agent includes Appium UA + * like by this filter. + */ + @VisibleForTesting + public static boolean containsAppiumName(@Nullable String userAgent) { + return userAgent != null && userAgent.toLowerCase().contains(USER_AGENT_PREFIX.toLowerCase()); + } + + /** + * Returns the User Agent. If the given UA already has + * {@link USER_AGENT_PREFIX}, it returns the UA. + * IF the given UA does not have {@link USER_AGENT_PREFIX}, + * it returns UA with the Appium prefix. + * @param userAgent the User Agent in the request headers. + * @return the User Agent for the request + */ + public static String buildUserAgent(@Nullable String userAgent) { + if (userAgent == null) { + return USER_AGENT; + } + + if (containsAppiumName(userAgent)) { + return userAgent; + } + + return buildUserAgentHeaderValue(userAgent); + } + + @Override + public HttpHandler apply(HttpHandler next) { + + return req -> { + req.setHeader(HttpHeaders.USER_AGENT, buildUserAgent(req.getHeader(HttpHeaders.USER_AGENT))); + return next.execute(req); + }; + } +} diff --git a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java index ec792852a..43ce9fc01 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java +++ b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java @@ -24,6 +24,8 @@ import com.google.common.base.Supplier; import com.google.common.base.Throwables; +import com.google.common.net.HttpHeaders; +import io.appium.java_client.AppiumUserAgentFilter; import io.appium.java_client.AppiumClientConfig; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.WebDriverException; @@ -192,6 +194,8 @@ private Response createSession(Command command) throws IOException { ProtocolHandshake.Result result = new AppiumProtocolHandshake().createSession( getClient().with((httpHandler) -> (req) -> { + req.setHeader(HttpHeaders.USER_AGENT, + AppiumUserAgentFilter.buildUserAgent(req.getHeader(HttpHeaders.USER_AGENT))); req.setHeader(IDEMPOTENCY_KEY_HEADER, UUID.randomUUID().toString().toLowerCase()); return httpHandler.execute(req); }), command diff --git a/src/main/resources/main.properties b/src/main/resources/main.properties index a4236a9fe..9875b0c49 100644 --- a/src/main/resources/main.properties +++ b/src/main/resources/main.properties @@ -1 +1,2 @@ selenium.version=@selenium.version@ +appiumClient.version=@appiumClient.version@ diff --git a/src/test/java/io/appium/java_client/internal/AppiumUserAgentFilterTest.java b/src/test/java/io/appium/java_client/internal/AppiumUserAgentFilterTest.java new file mode 100644 index 000000000..10e33a1ee --- /dev/null +++ b/src/test/java/io/appium/java_client/internal/AppiumUserAgentFilterTest.java @@ -0,0 +1,67 @@ +package io.appium.java_client.internal; + +import io.appium.java_client.AppiumUserAgentFilter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +public class AppiumUserAgentFilterTest { + @Test + void validateUserAgent() { + assertTrue(AppiumUserAgentFilter.USER_AGENT.startsWith("appium/")); + } + + public static Stream userAgentParams() { + return Stream.of( + Arguments.of("selenium/4.5.0 (java mac)", false), + Arguments.of("appium/8.2.0 (selenium/4.5.0 (java mac))", true), + Arguments.of("APPIUM/8.2.0 (selenium/4.5.0 (java mac))", true), + Arguments.of("something (Appium/8.2.0 (selenium/4.5.0 (java mac)))", true), + Arguments.of("something (appium/8.2.0 (selenium/4.5.0 (java mac)))", true) + ); + } + + @ParameterizedTest + @MethodSource("userAgentParams") + void validUserAgentIfContainsAppiumName(String userAgent, boolean expected) { + assertEquals(AppiumUserAgentFilter.containsAppiumName(userAgent), expected); + } + + @Test + void validBuildUserAgentNoUA() { + assertEquals(AppiumUserAgentFilter.buildUserAgent(null), AppiumUserAgentFilter.USER_AGENT); + } + + @Test + void validBuildUserAgentNoAppium1() { + String ua = AppiumUserAgentFilter.buildUserAgent("selenium/4.5.0 (java mac)"); + assertTrue(ua.startsWith("appium/")); + assertTrue(ua.endsWith("selenium/4.5.0 (java mac))")); + } + + @Test + void validBuildUserAgentNoAppium2() { + String ua = AppiumUserAgentFilter.buildUserAgent("customSelenium/4.5.0 (java mac)"); + assertTrue(ua.startsWith("appium/")); + assertTrue(ua.endsWith("customSelenium/4.5.0 (java mac))")); + } + + @Test + void validBuildUserAgentAlreadyHasAppium1() { + // Won't modify since the UA already has appium prefix + String ua = AppiumUserAgentFilter.buildUserAgent("appium/8.1.0 (selenium/4.5.0 (java mac))"); + assertEquals("appium/8.1.0 (selenium/4.5.0 (java mac))", ua); + } + + @Test + void validBuildUserAgentAlreadyHasAppium2() { + // Won't modify since the UA already has appium prefix + String ua = AppiumUserAgentFilter.buildUserAgent("something (appium/8.1.0 (selenium/4.5.0 (java mac)))"); + assertEquals("something (appium/8.1.0 (selenium/4.5.0 (java mac)))", ua); + } +} diff --git a/src/test/java/io/appium/java_client/internal/ConfigTest.java b/src/test/java/io/appium/java_client/internal/ConfigTest.java index 05559de27..67518eca7 100644 --- a/src/test/java/io/appium/java_client/internal/ConfigTest.java +++ b/src/test/java/io/appium/java_client/internal/ConfigTest.java @@ -6,17 +6,25 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.appium.java_client.AppiumUserAgentFilter; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.stream.Stream; class ConfigTest { private static final String SELENIUM_EXISTING_KEY = "selenium.version"; private static final String MISSING_KEY = "bla"; - @Test - void verifyGettingExistingValue() { - assertThat(Config.main().getValue(SELENIUM_EXISTING_KEY, String.class).length(), greaterThan(0)); - assertTrue(Config.main().getOptionalValue(SELENIUM_EXISTING_KEY, String.class).isPresent()); + @ParameterizedTest + @ValueSource(strings = {SELENIUM_EXISTING_KEY, AppiumUserAgentFilter.VERSION_KEY}) + void verifyGettingExistingValue(String key) { + assertThat(Config.main().getValue(key, String.class).length(), greaterThan(0)); + assertTrue(Config.main().getOptionalValue(key, String.class).isPresent()); } @Test @@ -24,9 +32,10 @@ void verifyGettingNonExistingValue() { assertThrows(IllegalArgumentException.class, () -> Config.main().getValue(MISSING_KEY, String.class)); } - @Test - void verifyGettingExistingValueWithWrongClass() { - assertThrows(ClassCastException.class, () -> Config.main().getValue(SELENIUM_EXISTING_KEY, Integer.class)); + @ParameterizedTest + @ValueSource(strings = {SELENIUM_EXISTING_KEY, AppiumUserAgentFilter.VERSION_KEY}) + void verifyGettingExistingValueWithWrongClass(String key) { + assertThrows(ClassCastException.class, () -> Config.main().getValue(key, Integer.class)); } @Test