Skip to content

Commit fe6383b

Browse files
committed
Move NewSessionPayload so it can be used by ProtocolHandshake
There's a lot of overlap between the ProtocolHandshake and the NewSessionPayload. Time to deal with this overlap between the two. The main change is to make the NewSessionPayload capable of writing to an Appendable. It does so by streaming the Capabilities used to create the payload as the OSS capabilities and those used by old geckodrivers, and then streaming every capability through transforms to generate spec compliant capabilities. There appears to be a bug where we always generate synthetic capabilities, but we can deal with that later.
1 parent 761717d commit fe6383b

File tree

10 files changed

+196
-211
lines changed

10 files changed

+196
-211
lines changed

java/client/src/org/openqa/selenium/remote/BUCK

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ java_library(name = 'remote-lib',
8787
'JsonToBeanConverter.java',
8888
'JsonWireProtocolResponse.java',
8989
'LocalFileDetector.java',
90+
'NewSessionPayload.java',
9091
'ProtocolHandshake.java',
9192
'RemoteExecuteMethod.java',
9293
'RemoteKeyboard.java',

java/server/src/org/openqa/selenium/remote/server/NewSessionPayload.java renamed to java/client/src/org/openqa/selenium/remote/NewSessionPayload.java

+173-20
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
package org.openqa.selenium.remote.server;
18+
package org.openqa.selenium.remote;
1919

2020
import static java.nio.charset.StandardCharsets.UTF_8;
2121
import static java.nio.file.StandardOpenOption.CREATE;
@@ -25,6 +25,7 @@
2525
import com.google.common.collect.ImmutableList;
2626
import com.google.common.collect.ImmutableMap;
2727
import com.google.common.collect.ImmutableSet;
28+
import com.google.common.collect.ImmutableSortedMap;
2829
import com.google.common.collect.ImmutableSortedSet;
2930
import com.google.common.collect.Ordering;
3031
import com.google.common.collect.Sets;
@@ -36,24 +37,38 @@
3637
import org.openqa.selenium.json.Json;
3738
import org.openqa.selenium.json.JsonInput;
3839
import org.openqa.selenium.json.JsonOutput;
39-
import org.openqa.selenium.remote.Dialect;
40+
import org.openqa.selenium.remote.session.CapabilitiesFilter;
41+
import org.openqa.selenium.remote.session.CapabilityTransform;
42+
import org.openqa.selenium.remote.session.ChromeFilter;
43+
import org.openqa.selenium.remote.session.EdgeFilter;
44+
import org.openqa.selenium.remote.session.FirefoxFilter;
45+
import org.openqa.selenium.remote.session.InternetExplorerFilter;
46+
import org.openqa.selenium.remote.session.OperaFilter;
47+
import org.openqa.selenium.remote.session.ProxyTransform;
48+
import org.openqa.selenium.remote.session.SafariFilter;
49+
import org.openqa.selenium.remote.session.StripAnyPlatform;
50+
import org.openqa.selenium.remote.session.W3CNameTransform;
51+
import org.openqa.selenium.remote.session.W3CPlatformNameNormaliser;
4052

4153
import java.io.ByteArrayInputStream;
4254
import java.io.Closeable;
4355
import java.io.IOException;
4456
import java.io.InputStream;
57+
import java.io.InputStreamReader;
4558
import java.io.Reader;
46-
import java.io.StringReader;
4759
import java.io.UncheckedIOException;
4860
import java.io.Writer;
4961
import java.nio.file.Files;
5062
import java.nio.file.Path;
5163
import java.util.Collection;
64+
import java.util.HashSet;
5265
import java.util.LinkedList;
5366
import java.util.List;
5467
import java.util.Map;
5568
import java.util.Objects;
5669
import java.util.Optional;
70+
import java.util.Queue;
71+
import java.util.ServiceLoader;
5772
import java.util.Set;
5873
import java.util.TreeMap;
5974
import java.util.TreeSet;
@@ -68,6 +83,9 @@ public class NewSessionPayload implements Closeable {
6883

6984
private static final Logger LOG = Logger.getLogger(NewSessionPayload.class.getName());
7085

86+
private final Set<CapabilitiesFilter> adapters;
87+
private final Set<CapabilityTransform> transforms;
88+
7189
private static final Dialect DEFAULT_DIALECT = Dialect.OSS;
7290
private final static Predicate<String> ACCEPTED_W3C_PATTERNS = Stream.of(
7391
"^[\\w-]+:.*$",
@@ -111,29 +129,23 @@ public static NewSessionPayload create(Capabilities caps) throws IOException {
111129
"capabilities", ImmutableMap.of(
112130
"firstMatch", ImmutableList.of(w3cCaps.build())));
113131

114-
return new NewSessionPayload(builder.build());
132+
return create(builder.build());
115133
}
116134

117-
public NewSessionPayload(Map<String, ?> source) throws IOException {
135+
public static NewSessionPayload create(Map<String, ?> source) throws IOException {
118136
Objects.requireNonNull(source, "Payload must be set");
119137

120-
String json = new Json().toJson(source);
121-
122-
Sources sources;
123-
long size = json.length() * 2; // Each character takes two bytes
124-
if (size > THRESHOLD || Runtime.getRuntime().freeMemory() < size) {
125-
this.root = Files.createTempDirectory("new-session");
126-
sources = diskBackedSource(new StringReader(json));
127-
} else {
128-
this.root = null;
129-
sources = memoryBackedSource(source);
130-
}
138+
byte[] json = new Json().toJson(source).getBytes(UTF_8);
139+
return new NewSessionPayload(
140+
json.length,
141+
new InputStreamReader(new ByteArrayInputStream(json), UTF_8));
142+
}
131143

132-
validate(sources);
133-
this.sources = rewrite(sources);
144+
public static NewSessionPayload create(long size, Reader source) throws IOException {
145+
return new NewSessionPayload(size, source);
134146
}
135147

136-
public NewSessionPayload(long size, Reader source) throws IOException {
148+
private NewSessionPayload(long size, Reader source) throws IOException {
137149
Sources sources;
138150
if (size > THRESHOLD || Runtime.getRuntime().freeMemory() < size) {
139151
this.root = Files.createTempDirectory("new-session");
@@ -145,6 +157,63 @@ public NewSessionPayload(long size, Reader source) throws IOException {
145157

146158
validate(sources);
147159
this.sources = rewrite(sources);
160+
161+
ImmutableSet.Builder<CapabilitiesFilter> adapters = ImmutableSet.builder();
162+
ServiceLoader.load(CapabilitiesFilter.class).forEach(adapters::add);
163+
adapters
164+
.add(new ChromeFilter())
165+
.add(new EdgeFilter())
166+
.add(new FirefoxFilter())
167+
.add(new InternetExplorerFilter())
168+
.add(new OperaFilter())
169+
.add(new SafariFilter());
170+
this.adapters = adapters.build();
171+
172+
ImmutableSet.Builder<CapabilityTransform> transforms = ImmutableSet.builder();
173+
ServiceLoader.load(CapabilityTransform.class).forEach(transforms::add);
174+
transforms
175+
.add(new ProxyTransform())
176+
.add(new StripAnyPlatform())
177+
.add(new W3CPlatformNameNormaliser())
178+
.add(new W3CNameTransform());
179+
this.transforms = transforms.build();
180+
}
181+
182+
public void writeTo(Appendable appendable) throws IOException {
183+
try (JsonOutput json = new Json().newOutput(appendable)) {
184+
json.beginObject();
185+
186+
@SuppressWarnings("unchecked")
187+
Map<String, Object> first = (Map<String, Object>) stream().findFirst()
188+
.orElse(new ImmutableCapabilities())
189+
.asMap();
190+
191+
// Write the first capability we get as the desired capability.
192+
json.name("desiredCapabilities");
193+
json.write(first, Json.MAP_TYPE);
194+
195+
// And write the first capability for gecko13
196+
json.name("capabilities");
197+
json.beginObject();
198+
199+
json.name("desiredCapabilities");
200+
json.write(first, Json.MAP_TYPE);
201+
202+
// Then write everything into the w3c payload. Because of the way we do this, it's easiest
203+
// to just populate the "firstMatch" section. The spec says it's fine to omit the
204+
// "alwaysMatch" field, so we do this.
205+
json.name("firstMatch");
206+
json.beginArray();
207+
//noinspection unchecked
208+
stream()
209+
.map(Capabilities::asMap)
210+
.map(map -> (Map<String, Object>) map)
211+
.forEach(map -> streamW3CProtocolParameters(json, map));
212+
json.endArray();
213+
214+
json.endObject(); // Close "capabilities" object
215+
json.endObject();
216+
}
148217
}
149218

150219
private void validate(Sources sources) {
@@ -181,6 +250,91 @@ private void validateSpecCompliance(Map<String, Object> fragment) {
181250
}
182251
}
183252

253+
private void streamW3CProtocolParameters(JsonOutput out, Map<String, Object> des) {
254+
// Technically we should be building up a combination of "alwaysMatch" and "firstMatch" options.
255+
// We're going to do a little processing to figure out what we might be able to do, and assume
256+
// that people don't really understand the difference between required and desired (which is
257+
// commonly the case). Wish us luck. Looking at the current implementations, people may have
258+
// set options for multiple browsers, in which case a compliant W3C remote end won't start
259+
// a session. If we find this, then we create multiple firstMatch capabilities. Furrfu.
260+
// The table of options are:
261+
//
262+
// Chrome: chromeOptions
263+
// Firefox: moz:.*, firefox_binary, firefox_profile, marionette
264+
// Edge: none given
265+
// IEDriver: ignoreZoomSetting, initialBrowserUrl, enableElementCacheCleanup,
266+
// browserAttachTimeout, enablePersistentHover, requireWindowFocus, logFile, logLevel, host,
267+
// extractPath, silent, ie.*
268+
// Opera: operaOptions
269+
// SafariDriver: safari.options
270+
//
271+
// We can't use the constants defined in the classes because it would introduce circular
272+
// dependencies between the remote library and the implementations. Yay!
273+
274+
ImmutableList<Map<String, Object>> firstMatch = adapters.stream()
275+
.map(adapter -> adapter.apply(des))
276+
.filter(Objects::nonNull)
277+
.map(this::applyTransforms)
278+
.filter(w3cCaps -> !w3cCaps.isEmpty())
279+
.collect(ImmutableList.toImmutableList());
280+
281+
Set<String> excludedKeys = firstMatch.stream()
282+
.map(Map::keySet)
283+
.flatMap(Collection::stream)
284+
.distinct()
285+
.collect(ImmutableSet.toImmutableSet());
286+
287+
Map<String, Object> alwaysMatch = applyTransforms(des).entrySet().stream()
288+
.filter(entry -> !excludedKeys.contains(entry.getKey()))
289+
.filter(entry -> entry.getValue() != null)
290+
.collect(ImmutableSortedMap.toImmutableSortedMap(
291+
Ordering.natural(),
292+
Map.Entry::getKey,
293+
Map.Entry::getValue));
294+
295+
firstMatch.stream()
296+
.map(first -> ImmutableSortedMap.naturalOrder().putAll(alwaysMatch).putAll(first).build())
297+
.forEach(map -> out.write(map, MAP_TYPE));
298+
}
299+
300+
private Map<String, Object> applyTransforms(Map<String, Object> caps) {
301+
Queue<Map.Entry<String, Object>> toExamine = new LinkedList<>();
302+
toExamine.addAll(caps.entrySet());
303+
Set<String> seenKeys = new HashSet<>();
304+
Map<String, Object> toReturn = new TreeMap<>();
305+
306+
// Take each entry and apply the transforms
307+
while (!toExamine.isEmpty()) {
308+
Map.Entry<String, Object> entry = toExamine.remove();
309+
seenKeys.add(entry.getKey());
310+
311+
if (entry.getValue() == null) {
312+
continue;
313+
}
314+
315+
for (CapabilityTransform transform : transforms) {
316+
Collection<Map.Entry<String, Object>> result = transform.apply(entry);
317+
if (result == null) {
318+
toReturn.remove(entry.getKey());
319+
break;
320+
}
321+
322+
for (Map.Entry<String, Object> newEntry : result) {
323+
if (!seenKeys.contains(newEntry.getKey())) {
324+
toExamine.add(newEntry);
325+
} else {
326+
if (newEntry.getKey().equals(entry.getKey())) {
327+
entry = newEntry;
328+
}
329+
toReturn.put(newEntry.getKey(), newEntry.getValue());
330+
}
331+
}
332+
}
333+
}
334+
335+
return toReturn;
336+
}
337+
184338
/**
185339
* If the local end sent a request with a JSON Wire Protocol payload that does not have a matching
186340
* W3C payload, then we need to synthesize one that matches.
@@ -433,7 +587,6 @@ public void close() {
433587
}
434588
}
435589

436-
437590
private static class Sources {
438591

439592
private final Supplier<InputStream> originalPayload;

0 commit comments

Comments
 (0)