From 15dfc625760ac568a7467955ddb2d1de545109c3 Mon Sep 17 00:00:00 2001 From: Simon Stewart Date: Mon, 20 Sep 2021 21:22:12 +0100 Subject: [PATCH] Allow commands to be loaded via the ServiceLoader (#9854) When adding commands via the Augmenter, it is sometimes helpful to provide an actual command too. This change allows drivers to specify new commands and add them as necessary. --- .../selenium/firefox/AddHasExtensions.java | 29 ++++++++++++---- .../selenium/firefox/FirefoxDriver.java | 27 ++++++++------- .../remote/AdditionalHttpCommands.java | 33 +++++++++++++++++++ .../openqa/selenium/remote/CommandCodec.java | 5 +++ .../org/openqa/selenium/remote/Dialect.java | 18 ++++++++-- .../codec/AbstractHttpCommandCodec.java | 6 ++++ 6 files changed, 98 insertions(+), 20 deletions(-) create mode 100644 java/src/org/openqa/selenium/remote/AdditionalHttpCommands.java diff --git a/java/src/org/openqa/selenium/firefox/AddHasExtensions.java b/java/src/org/openqa/selenium/firefox/AddHasExtensions.java index 725a4bf679205..21591e7da0d0e 100644 --- a/java/src/org/openqa/selenium/firefox/AddHasExtensions.java +++ b/java/src/org/openqa/selenium/firefox/AddHasExtensions.java @@ -20,17 +20,34 @@ import com.google.auto.service.AutoService; import com.google.common.collect.ImmutableMap; import org.openqa.selenium.Capabilities; +import org.openqa.selenium.remote.AdditionalHttpCommands; import org.openqa.selenium.remote.AugmenterProvider; +import org.openqa.selenium.remote.CommandInfo; import org.openqa.selenium.remote.ExecuteMethod; +import org.openqa.selenium.remote.http.HttpMethod; import java.nio.file.Path; +import java.util.Map; import java.util.function.Predicate; import static java.util.Collections.singletonMap; import static org.openqa.selenium.remote.BrowserType.FIREFOX; -@AutoService(AugmenterProvider.class) -public class AddHasExtensions implements AugmenterProvider { +@AutoService({AdditionalHttpCommands.class, AugmenterProvider.class}) +public class AddHasExtensions implements AugmenterProvider, AdditionalHttpCommands { + + public static final String INSTALL_EXTENSION = "installExtension"; + public static final String UNINSTALL_EXTENSION = "uninstallExtension"; + + private static final Map COMMANDS = ImmutableMap.of( + INSTALL_EXTENSION, new CommandInfo("/session/:sessionId/moz/addon/install",HttpMethod.POST), + UNINSTALL_EXTENSION, new CommandInfo("/session/:sessionId/moz/addon/uninstall", HttpMethod.POST)); + + @Override + public Map getAdditionalCommands() { + return COMMANDS; + } + @Override public Predicate isApplicable() { return caps -> FIREFOX.equals(caps.getBrowserName()); @@ -46,15 +63,15 @@ public HasExtensions getImplementation(Capabilities capabilities, ExecuteMethod return new HasExtensions() { @Override public String installExtension(Path path) { - return (String) executeMethod.execute(FirefoxDriver.ExtraCommands.INSTALL_EXTENSION, - ImmutableMap.of("path", path.toAbsolutePath().toString(), - "temporary", false)); + return (String) executeMethod.execute( + INSTALL_EXTENSION, + ImmutableMap.of("path", path.toAbsolutePath().toString(), "temporary", false)); } @Override public void uninstallExtension(String extensionId) { - executeMethod.execute(FirefoxDriver.ExtraCommands.UNINSTALL_EXTENSION, singletonMap("id", extensionId)); + executeMethod.execute(UNINSTALL_EXTENSION, singletonMap("id", extensionId)); } }; } diff --git a/java/src/org/openqa/selenium/firefox/FirefoxDriver.java b/java/src/org/openqa/selenium/firefox/FirefoxDriver.java index 5cf0695c06275..82e221fdf3ae4 100644 --- a/java/src/org/openqa/selenium/firefox/FirefoxDriver.java +++ b/java/src/org/openqa/selenium/firefox/FirefoxDriver.java @@ -52,13 +52,13 @@ import java.net.URI; import java.nio.file.Path; +import java.util.Map; import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; import java.util.stream.StreamSupport; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Collections.singletonMap; import static org.openqa.selenium.remote.CapabilityType.PROXY; /** @@ -141,29 +141,31 @@ public static final class Capability { } static class ExtraCommands { - static String INSTALL_EXTENSION = "installExtension"; - static String UNINSTALL_EXTENSION = "uninstallExtension"; static String FULL_PAGE_SCREENSHOT = "fullPageScreenshot"; } private static final ImmutableMap EXTRA_COMMANDS = ImmutableMap.of( - ExtraCommands.INSTALL_EXTENSION, - new CommandInfo("/session/:sessionId/moz/addon/install", HttpMethod.POST), - ExtraCommands.UNINSTALL_EXTENSION, - new CommandInfo("/session/:sessionId/moz/addon/uninstall", HttpMethod.POST), ExtraCommands.FULL_PAGE_SCREENSHOT, new CommandInfo("/session/:sessionId/moz/screenshot/full", HttpMethod.GET) ); private static class FirefoxDriverCommandExecutor extends DriverCommandExecutor { public FirefoxDriverCommandExecutor(DriverService service) { - super(service, EXTRA_COMMANDS); + super(service, getExtraCommands()); + } + + private static Map getExtraCommands() { + return ImmutableMap.builder() + .putAll(EXTRA_COMMANDS) + .putAll(new AddHasExtensions().getAdditionalCommands()) + .build(); } } private final Capabilities capabilities; protected FirefoxBinary binary; private final RemoteWebStorage webStorage; + private final HasExtensions extensions; private final Optional cdpUri; private DevTools devTools; @@ -204,6 +206,7 @@ public FirefoxDriver(FirefoxDriverService service, FirefoxOptions options) { private FirefoxDriver(FirefoxDriverCommandExecutor executor, FirefoxOptions options) { super(executor, dropCapabilities(options)); webStorage = new RemoteWebStorage(getExecuteMethod()); + extensions = new AddHasExtensions().getImplementation(getCapabilities(), getExecuteMethod()); Capabilities capabilities = super.getCapabilities(); HttpClient.Factory clientFactory = HttpClient.Factory.createDefault(); @@ -269,14 +272,14 @@ private static boolean isLegacy(Capabilities desiredCapabilities) { @Override public String installExtension(Path path) { - return (String) execute(ExtraCommands.INSTALL_EXTENSION, - ImmutableMap.of("path", path.toAbsolutePath().toString(), - "temporary", false)).getValue(); + Require.nonNull("Path", path); + return extensions.installExtension(path); } @Override public void uninstallExtension(String extensionId) { - execute(ExtraCommands.UNINSTALL_EXTENSION, singletonMap("id", extensionId)); + Require.nonNull("Extension ID", extensionId); + extensions.uninstallExtension(extensionId); } /** diff --git a/java/src/org/openqa/selenium/remote/AdditionalHttpCommands.java b/java/src/org/openqa/selenium/remote/AdditionalHttpCommands.java new file mode 100644 index 0000000000000..d0f647d67c5eb --- /dev/null +++ b/java/src/org/openqa/selenium/remote/AdditionalHttpCommands.java @@ -0,0 +1,33 @@ +// 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.remote; + +import java.util.Map; + +/** + * Used to allow a {@link CommandExecutor} using HTTP to find additional + * commands that should be supported. Implementations of this interface + * are found using the {@link java.util.ServiceLoader} + */ +@FunctionalInterface +public interface AdditionalHttpCommands { + /** + * @return Additional commands to add to the {@link CommandExecutor}. + */ + Map getAdditionalCommands(); +} diff --git a/java/src/org/openqa/selenium/remote/CommandCodec.java b/java/src/org/openqa/selenium/remote/CommandCodec.java index 7da2c81ad9579..99fca715a667b 100644 --- a/java/src/org/openqa/selenium/remote/CommandCodec.java +++ b/java/src/org/openqa/selenium/remote/CommandCodec.java @@ -27,6 +27,11 @@ */ public interface CommandCodec { + /** + * @return Whether this {@link CommandCodec} supports the given command name. + */ + boolean isSupported(String commandName); + /** * Encodes a command. * diff --git a/java/src/org/openqa/selenium/remote/Dialect.java b/java/src/org/openqa/selenium/remote/Dialect.java index 722aeaa7293e3..bb58f494c46c1 100644 --- a/java/src/org/openqa/selenium/remote/Dialect.java +++ b/java/src/org/openqa/selenium/remote/Dialect.java @@ -24,11 +24,13 @@ import org.openqa.selenium.remote.codec.w3c.W3CHttpCommandCodec; import org.openqa.selenium.remote.codec.w3c.W3CHttpResponseCodec; +import java.util.ServiceLoader; + public enum Dialect { OSS { @Override public CommandCodec getCommandCodec() { - return new JsonHttpCommandCodec(); + return bindAdditionalCommands(new JsonHttpCommandCodec()); } @Override @@ -49,7 +51,7 @@ public String getShadowRootElementKey() { W3C { @Override public CommandCodec getCommandCodec() { - return new W3CHttpCommandCodec(); + return bindAdditionalCommands(new W3CHttpCommandCodec()); } @Override @@ -72,4 +74,16 @@ public String getShadowRootElementKey() { public abstract ResponseCodec getResponseCodec(); public abstract String getEncodedElementKey(); public abstract String getShadowRootElementKey(); + + private static CommandCodec bindAdditionalCommands(CommandCodec toCodec) { + ServiceLoader.load(AdditionalHttpCommands.class).forEach(cmds -> { + cmds.getAdditionalCommands().forEach((name, info) -> { + if (!toCodec.isSupported(name)) { + toCodec.defineCommand(name, info.getMethod(), info.getUrl()); + } + }); + }); + + return toCodec; + } } diff --git a/java/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java b/java/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java index 686a030c9034b..a6955bb67c18f 100644 --- a/java/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java +++ b/java/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java @@ -236,6 +236,12 @@ public AbstractHttpCommandCodec() { defineCommand(SET_USER_VERIFIED, post(webauthnId + "/uv")); } + @Override + public boolean isSupported(String commandName) { + Require.nonNull("Command name", commandName); + return nameToSpec.containsKey(commandName) || aliases.containsKey(commandName); + } + @Override public HttpRequest encode(Command command) { String name = aliases.getOrDefault(command.getName(), command.getName());