From 854ac60c76270214c8f5ae33beef7acf6448d184 Mon Sep 17 00:00:00 2001 From: Simon Stewart Date: Tue, 27 Apr 2021 16:29:55 +0100 Subject: [PATCH] Introduce a CapabilitiesUtils class for common operations on Capabilities --- .../selenium/remote/CapabilitiesUtils.java | 216 ++++++++++++++++++ .../selenium/remote/NewSessionPayload.java | 186 +-------------- .../RemoteWebDriverInitializationTest.java | 2 +- 3 files changed, 227 insertions(+), 177 deletions(-) create mode 100644 java/client/src/org/openqa/selenium/remote/CapabilitiesUtils.java diff --git a/java/client/src/org/openqa/selenium/remote/CapabilitiesUtils.java b/java/client/src/org/openqa/selenium/remote/CapabilitiesUtils.java new file mode 100644 index 0000000000000..71eff312a6182 --- /dev/null +++ b/java/client/src/org/openqa/selenium/remote/CapabilitiesUtils.java @@ -0,0 +1,216 @@ +// 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 com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.ImmutableCapabilities; +import org.openqa.selenium.Proxy; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.session.CapabilitiesFilter; +import org.openqa.selenium.remote.session.CapabilityTransform; +import org.openqa.selenium.remote.session.ChromeFilter; +import org.openqa.selenium.remote.session.EdgeFilter; +import org.openqa.selenium.remote.session.FirefoxFilter; +import org.openqa.selenium.remote.session.InternetExplorerFilter; +import org.openqa.selenium.remote.session.OperaFilter; +import org.openqa.selenium.remote.session.ProxyTransform; +import org.openqa.selenium.remote.session.SafariFilter; +import org.openqa.selenium.remote.session.StripAnyPlatform; +import org.openqa.selenium.remote.session.W3CPlatformNameNormaliser; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.openqa.selenium.remote.CapabilityType.PLATFORM; +import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; +import static org.openqa.selenium.remote.CapabilityType.PROXY; + +public class CapabilitiesUtils { + + private static final Predicate ACCEPTED_W3C_PATTERNS = new AcceptedW3CCapabilityKeys(); + + private CapabilitiesUtils() { + // Helper class + } + + public static Stream makeW3CSafe(Capabilities possiblyInvalidCapabilities) { + Require.nonNull("Capabilities", possiblyInvalidCapabilities); + + return makeW3CSafe(possiblyInvalidCapabilities.asMap()).map(ImmutableCapabilities::new); + } + + public static Stream> makeW3CSafe(Map possiblyInvalidCapabilities) { + Require.nonNull("Capabilities", possiblyInvalidCapabilities); + + Set adapters = getCapabilityFilters(); + + // If there's an OSS value, generate a stream of capabilities from that using the transforms, + // then add magic to generate each of the w3c capabilities. For the sake of simplicity, we're + // going to make the (probably wrong) assumption we can hold all of the firstMatch values and + // alwaysMatch value in memory at the same time. + Map oss = convertOssToW3C(possiblyInvalidCapabilities); + Stream> fromOss; + Set usedKeys = new HashSet<>(); + + // Are there any values we care want to pull out into a mapping of their own? + List> firsts = adapters.stream() + .map(adapter -> adapter.apply(oss)) + .filter(Objects::nonNull) + .filter(map -> !map.isEmpty()) + .map( + map -> map.entrySet().stream() + .filter(entry -> entry.getKey() != null) + .filter(entry -> ACCEPTED_W3C_PATTERNS.test(entry.getKey())) + .filter(entry -> entry.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) + .peek(map -> usedKeys.addAll(map.keySet())) + .collect(ImmutableList.toImmutableList()); + if (firsts.isEmpty()) { + firsts = ImmutableList.of(ImmutableMap.of()); + } + + // Are there any remaining unused keys? + Map always = oss.entrySet().stream() + .filter(entry -> !usedKeys.contains(entry.getKey())) + .filter(entry -> entry.getValue() != null) + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); + + // Firsts contains at least one entry, always contains everything else. Let's combine them + // into the stream to form a unified set of capabilities. Woohoo! + fromOss = firsts.stream() + .map(first -> ImmutableMap.builder().putAll(always).putAll(first).build()) + .map(CapabilitiesUtils::applyTransforms) + .map(map -> map.entrySet().stream() + .filter(entry -> ACCEPTED_W3C_PATTERNS.test(entry.getKey())) + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))); + + return fromOss; + } + + private static Map convertOssToW3C(Map capabilities) { + Map toReturn = new TreeMap<>(capabilities); + + // Platform name + if (capabilities.containsKey(PLATFORM) && !capabilities.containsKey(PLATFORM_NAME)) { + toReturn.put(PLATFORM_NAME, String.valueOf(capabilities.get(PLATFORM))); + } + + if (capabilities.containsKey(PROXY)) { + Map proxyMap = getProxyFromCapabilities(capabilities); + if (proxyMap.containsKey("noProxy")) { + Map w3cProxyMap = new HashMap<>(proxyMap); + Object rawData = proxyMap.get("noProxy"); + if (rawData instanceof String) { + w3cProxyMap.put("noProxy", Arrays.asList(((String) rawData).split(",\\s*"))); + } + toReturn.put(CapabilityType.PROXY, w3cProxyMap); + } + } + + return toReturn; + } + + private static Map getProxyFromCapabilities(Map capabilities) { + Object rawProxy = capabilities.get(CapabilityType.PROXY); + if (rawProxy instanceof Proxy) { + return ((Proxy) rawProxy).toJson(); + } else if (rawProxy instanceof Map) { + //noinspection unchecked + return (Map) rawProxy; + } else { + return new HashMap<>(); + } + } + + private static Map applyTransforms(Map caps) { + Queue> toExamine = new LinkedList<>(caps.entrySet()); + Set seenKeys = new HashSet<>(); + Map toReturn = new TreeMap<>(); + + Set transforms = getCapabilityTransforms(); + + // Take each entry and apply the transforms + while (!toExamine.isEmpty()) { + Map.Entry entry = toExamine.remove(); + seenKeys.add(entry.getKey()); + + if (entry.getValue() == null) { + continue; + } + + for (CapabilityTransform transform : transforms) { + Collection> result = transform.apply(entry); + if (result == null) { + toReturn.remove(entry.getKey()); + break; + } + + for (Map.Entry newEntry : result) { + if (!seenKeys.contains(newEntry.getKey())) { + toExamine.add(newEntry); + } else { + if (newEntry.getKey().equals(entry.getKey())) { + entry = newEntry; + } + toReturn.put(newEntry.getKey(), newEntry.getValue()); + } + } + } + } + return toReturn; + } + + private static Set getCapabilityFilters() { + ImmutableSet.Builder adapters = ImmutableSet.builder(); + ServiceLoader.load(CapabilitiesFilter.class).forEach(adapters::add); + adapters + .add(new ChromeFilter()) + .add(new EdgeFilter()) + .add(new FirefoxFilter()) + .add(new InternetExplorerFilter()) + .add(new OperaFilter()) + .add(new SafariFilter()); + return adapters.build(); + } + + private static Set getCapabilityTransforms() { + ImmutableSet.Builder transforms = ImmutableSet.builder(); + ServiceLoader.load(CapabilityTransform.class).forEach(transforms::add); + transforms + .add(new ProxyTransform()) + .add(new StripAnyPlatform()) + .add(new W3CPlatformNameNormaliser()); + return transforms.build(); + } +} diff --git a/java/client/src/org/openqa/selenium/remote/NewSessionPayload.java b/java/client/src/org/openqa/selenium/remote/NewSessionPayload.java index 2c40358ba7f1f..b049843ec1c3a 100644 --- a/java/client/src/org/openqa/selenium/remote/NewSessionPayload.java +++ b/java/client/src/org/openqa/selenium/remote/NewSessionPayload.java @@ -17,13 +17,6 @@ package org.openqa.selenium.remote; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.openqa.selenium.json.Json.LIST_OF_MAPS_TYPE; -import static org.openqa.selenium.json.Json.MAP_TYPE; -import static org.openqa.selenium.remote.CapabilityType.PLATFORM; -import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; -import static org.openqa.selenium.remote.CapabilityType.PROXY; - import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -33,25 +26,12 @@ import com.google.common.io.CharSource; import com.google.common.io.CharStreams; import com.google.common.io.FileBackedOutputStream; - import org.openqa.selenium.Capabilities; import org.openqa.selenium.ImmutableCapabilities; -import org.openqa.selenium.Proxy; import org.openqa.selenium.internal.Require; import org.openqa.selenium.json.Json; import org.openqa.selenium.json.JsonInput; import org.openqa.selenium.json.JsonOutput; -import org.openqa.selenium.remote.session.CapabilitiesFilter; -import org.openqa.selenium.remote.session.CapabilityTransform; -import org.openqa.selenium.remote.session.ChromeFilter; -import org.openqa.selenium.remote.session.EdgeFilter; -import org.openqa.selenium.remote.session.FirefoxFilter; -import org.openqa.selenium.remote.session.InternetExplorerFilter; -import org.openqa.selenium.remote.session.OperaFilter; -import org.openqa.selenium.remote.session.ProxyTransform; -import org.openqa.selenium.remote.session.SafariFilter; -import org.openqa.selenium.remote.session.StripAnyPlatform; -import org.openqa.selenium.remote.session.W3CPlatformNameNormaliser; import java.io.Closeable; import java.io.IOException; @@ -60,26 +40,19 @@ import java.io.StringReader; import java.io.UncheckedIOException; import java.io.Writer; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Queue; -import java.util.ServiceLoader; import java.util.Set; -import java.util.TreeMap; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; -public class NewSessionPayload implements Closeable { +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.openqa.selenium.json.Json.LIST_OF_MAPS_TYPE; +import static org.openqa.selenium.json.Json.MAP_TYPE; - private final Set adapters; - private final Set transforms; +public class NewSessionPayload implements Closeable { private static final Dialect DEFAULT_DIALECT = Dialect.OSS; private static final Predicate ACCEPTED_W3C_PATTERNS = new AcceptedW3CCapabilityKeys(); @@ -119,25 +92,6 @@ private NewSessionPayload(Reader source) { throw new UncheckedIOException(e); } - ImmutableSet.Builder adapters = ImmutableSet.builder(); - ServiceLoader.load(CapabilitiesFilter.class).forEach(adapters::add); - adapters - .add(new ChromeFilter()) - .add(new EdgeFilter()) - .add(new FirefoxFilter()) - .add(new InternetExplorerFilter()) - .add(new OperaFilter()) - .add(new SafariFilter()); - this.adapters = adapters.build(); - - ImmutableSet.Builder transforms = ImmutableSet.builder(); - ServiceLoader.load(CapabilityTransform.class).forEach(transforms::add); - transforms - .add(new ProxyTransform()) - .add(new StripAnyPlatform()) - .add(new W3CPlatformNameNormaliser()); - this.transforms = transforms.build(); - ImmutableSet.Builder dialects = ImmutableSet.builder(); try { if (getOss() != null) { @@ -195,9 +149,8 @@ private void validate() throws IOException { } }) .peek(map -> { - ImmutableSortedSet illegalKeys = map.entrySet().stream() - .filter(entry -> !ACCEPTED_W3C_PATTERNS.test(entry.getKey())) - .map(Map.Entry::getKey) + ImmutableSortedSet illegalKeys = map.keySet().stream() + .filter(ACCEPTED_W3C_PATTERNS.negate()) .collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural())); if (!illegalKeys.isEmpty()) { throw new IllegalArgumentException( @@ -218,17 +171,6 @@ public void writeTo(Appendable appendable) throws IOException { .asMap(); } Map ossFirst = new HashMap<>(first); - if (first.containsKey(CapabilityType.PROXY)) { - Map proxyMap = getProxyFromCapabilities(first); - if (proxyMap.containsKey("noProxy")) { - Map ossProxyMap = new HashMap<>(proxyMap); - Object rawData = proxyMap.get("noProxy"); - if (rawData instanceof List) { - ossProxyMap.put("noProxy", String.join(",", ((List) rawData))); - } - ossFirst.put(CapabilityType.PROXY, ossProxyMap); - } - } // Write the first capability we get as the desired capability. json.name("desiredCapabilities"); @@ -254,18 +196,6 @@ public void writeTo(Appendable appendable) throws IOException { } } - private Map getProxyFromCapabilities(Map capabilities) { - Object rawProxy = capabilities.get(CapabilityType.PROXY); - if (rawProxy instanceof Proxy) { - return ((Proxy) rawProxy).toJson(); - } else if (rawProxy instanceof Map) { - //noinspection unchecked - return (Map) rawProxy; - } else { - return new HashMap<>(); - } - } - private void writeMetaData(JsonOutput out) throws IOException { CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8); try (Reader reader = charSource.openBufferedStream(); @@ -308,8 +238,6 @@ public Stream stream() { Stream> w3c = getW3C(); return Stream.concat(oss, w3c) - .filter(Objects::nonNull) - .map(this::applyTransforms) .filter(Objects::nonNull) .distinct() .map(ImmutableCapabilities::new); @@ -384,42 +312,11 @@ private Stream> getW3C() throws IOException { // then add magic to generate each of the w3c capabilities. For the sake of simplicity, we're // going to make the (probably wrong) assumption we can hold all of the firstMatch values and // alwaysMatch value in memory at the same time. - Map oss = convertOssToW3C(getOss()); - Stream> fromOss; - if (oss != null) { - Set usedKeys = new HashSet<>(); - - // Are there any values we care want to pull out into a mapping of their own? - List> firsts = adapters.stream() - .map(adapter -> adapter.apply(oss)) - .filter(Objects::nonNull) - .filter(map -> !map.isEmpty()) - .map( - map -> map.entrySet().stream() - .filter(entry -> entry.getKey() != null) - .filter(entry -> ACCEPTED_W3C_PATTERNS.test(entry.getKey())) - .filter(entry -> entry.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) - .peek(map -> usedKeys.addAll(map.keySet())) - .collect(ImmutableList.toImmutableList()); - if (firsts.isEmpty()) { - firsts = ImmutableList.of(ImmutableMap.of()); - } - // Are there any remaining unused keys? - Map always = oss.entrySet().stream() - .filter(entry -> !usedKeys.contains(entry.getKey())) - .filter(entry -> entry.getValue() != null) - .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); - - // Firsts contains at least one entry, always contains everything else. Let's combine them - // into the stream to form a unified set of capabilities. Woohoo! - fromOss = firsts.stream() - .map(first -> ImmutableMap.builder().putAll(always).putAll(first).build()) - .map(this::applyTransforms) - .map(map -> map.entrySet().stream() - .filter(entry -> ACCEPTED_W3C_PATTERNS.test(entry.getKey())) - .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))); + Stream> fromOss; + Map ossCapabilities = getOss(); + if (ossCapabilities != null) { + fromOss = CapabilitiesUtils.makeW3CSafe(ossCapabilities); } else { fromOss = Stream.of(); } @@ -446,33 +343,6 @@ private Stream> getW3C() throws IOException { return Stream.concat(fromOss, fromW3c).distinct(); } - private Map convertOssToW3C(Map capabilities) { - if (capabilities == null) { - return null; - } - - Map toReturn = new TreeMap<>(capabilities); - - // Platform name - if (capabilities.containsKey(PLATFORM) && !capabilities.containsKey(PLATFORM_NAME)) { - toReturn.put(PLATFORM_NAME, String.valueOf(capabilities.get(PLATFORM))); - } - - if (capabilities.containsKey(PROXY)) { - Map proxyMap = getProxyFromCapabilities(capabilities); - if (proxyMap.containsKey("noProxy")) { - Map w3cProxyMap = new HashMap<>(proxyMap); - Object rawData = proxyMap.get("noProxy"); - if (rawData instanceof String) { - w3cProxyMap.put("noProxy", Arrays.asList(((String) rawData).split(",\\s*"))); - } - toReturn.put(CapabilityType.PROXY, w3cProxyMap); - } - } - - return toReturn; - } - private Map getAlwaysMatch() throws IOException { CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8); try (Reader reader = charSource.openBufferedStream(); @@ -525,42 +395,6 @@ private Collection> getFirstMatches() throws IOException { return null; } - private Map applyTransforms(Map caps) { - Queue> toExamine = new LinkedList<>(caps.entrySet()); - Set seenKeys = new HashSet<>(); - Map toReturn = new TreeMap<>(); - - // Take each entry and apply the transforms - while (!toExamine.isEmpty()) { - Map.Entry entry = toExamine.remove(); - seenKeys.add(entry.getKey()); - - if (entry.getValue() == null) { - continue; - } - - for (CapabilityTransform transform : transforms) { - Collection> result = transform.apply(entry); - if (result == null) { - toReturn.remove(entry.getKey()); - break; - } - - for (Map.Entry newEntry : result) { - if (!seenKeys.contains(newEntry.getKey())) { - toExamine.add(newEntry); - } else { - if (newEntry.getKey().equals(entry.getKey())) { - entry = newEntry; - } - toReturn.put(newEntry.getKey(), newEntry.getValue()); - } - } - } - } - return toReturn; - } - @Override public String toString() { StringBuilder res = new StringBuilder(); diff --git a/java/client/test/org/openqa/selenium/remote/RemoteWebDriverInitializationTest.java b/java/client/test/org/openqa/selenium/remote/RemoteWebDriverInitializationTest.java index dc6121598cd0a..76f25cec0583e 100644 --- a/java/client/test/org/openqa/selenium/remote/RemoteWebDriverInitializationTest.java +++ b/java/client/test/org/openqa/selenium/remote/RemoteWebDriverInitializationTest.java @@ -83,7 +83,7 @@ public void constructorShouldThrowIfExecutorThrowsOnAnAttemptToStartASession() { .isThrownBy(() -> new RemoteWebDriver(executor, new ImmutableCapabilities())) .withMessageContaining("Build info: ") .withMessageContaining("Driver info: org.openqa.selenium.remote.RemoteWebDriver") - .withMessageContaining("Command: [null, newSession {desiredCapabilities=Capabilities {}}]"); + .withMessageContaining("Command: [null, newSession {capabilities=[Capabilities {}], desiredCapabilities=Capabilities {}}]"); verifyNoCommands(executor); }