30
30
import static org .openqa .selenium .remote .BrowserType .SAFARI ;
31
31
32
32
import com .google .common .base .Preconditions ;
33
- import com .google .common .collect .ImmutableList ;
34
33
import com .google .common .collect .ImmutableSet ;
34
+ import com .google .gson .Gson ;
35
+ import com .google .gson .JsonArray ;
36
+ import com .google .gson .JsonElement ;
37
+ import com .google .gson .JsonObject ;
38
+ import com .google .gson .stream .JsonWriter ;
35
39
36
40
import org .openqa .selenium .Capabilities ;
37
41
import org .openqa .selenium .SessionNotCreatedException ;
40
44
import org .openqa .selenium .remote .http .HttpRequest ;
41
45
import org .openqa .selenium .remote .http .HttpResponse ;
42
46
47
+ import java .io .BufferedInputStream ;
48
+ import java .io .BufferedWriter ;
43
49
import java .io .IOException ;
50
+ import java .io .InputStream ;
44
51
import java .net .HttpURLConnection ;
52
+ import java .nio .file .Files ;
53
+ import java .nio .file .Path ;
45
54
import java .util .Collection ;
46
55
import java .util .HashMap ;
47
- import java .util .List ;
48
56
import java .util .Map ;
49
57
import java .util .Optional ;
50
58
import java .util .Set ;
59
+ import java .util .logging .Level ;
51
60
import java .util .logging .Logger ;
61
+ import java .util .stream .Collector ;
52
62
import java .util .stream .Collectors ;
53
63
import java .util .stream .Stream ;
54
64
55
65
public class ProtocolHandshake {
56
66
57
67
private final static Logger LOG = Logger .getLogger (ProtocolHandshake .class .getName ());
58
68
69
+ /**
70
+ * Capability names that should never be sent across the wire to a w3c compliant remote end.
71
+ */
72
+ private final Set <String > UNUSED_W3C_NAMES = ImmutableSet .<String >builder ()
73
+ .add ("firefox_binary" )
74
+ .add ("firefox_profile" )
75
+ .add ("marionette" )
76
+ .build ();
77
+
59
78
public Result createSession (HttpClient client , Command command )
60
79
throws IOException {
61
- // Avoid serialising the capabilities too many times. Things like profiles are expensive.
62
-
63
80
Capabilities desired = (Capabilities ) command .getParameters ().get ("desiredCapabilities" );
64
81
desired = desired == null ? new DesiredCapabilities () : desired ;
65
82
Capabilities required = (Capabilities ) command .getParameters ().get ("requiredCapabilities" );
66
83
required = required == null ? new DesiredCapabilities () : required ;
67
84
68
- String des = new BeanToJsonConverter ().convert (desired );
69
- String req = new BeanToJsonConverter ().convert (required );
70
-
71
- // Assume the remote end obeys the robustness principle.
72
- StringBuilder parameters = new StringBuilder ("{" );
73
- amendW3cParameters (parameters , desired , required );
74
- parameters .append ("," );
75
- amendGeckoDriver013Parameters (parameters , des , req );
76
- parameters .append ("," );
77
- amendOssParameters (parameters , des , req );
78
- parameters .append ("}" );
79
- LOG .fine ("Attempting multi-dialect session, assuming Postel's Law holds true on the remote end" );
80
- Optional <Result > result = createSession (client , parameters );
81
-
82
- if (result .isPresent ()) {
83
- Result toReturn = result .get ();
84
- LOG .info (String .format ("Detected dialect: %s" , toReturn .dialect ));
85
- return toReturn ;
85
+ BeanToJsonConverter converter = new BeanToJsonConverter ();
86
+ JsonObject des = (JsonObject ) converter .convertObject (desired );
87
+ JsonObject req = (JsonObject ) converter .convertObject (required );
88
+
89
+ // We don't know how large the generated JSON is going to be. Spool it to disk, and then read
90
+ // the file size, then stream it to the remote end. If we could be sure the remote end could
91
+ // cope with chunked requests we'd use those. I don't think we can. *sigh*
92
+ Path jsonFile = Files .createTempFile ("new-session" , ".json" );
93
+
94
+ try (
95
+ BufferedWriter fileWriter = Files .newBufferedWriter (jsonFile , UTF_8 );
96
+ JsonWriter out = new JsonWriter (fileWriter )) {
97
+ out .setHtmlSafe (true );
98
+ out .setIndent (" " );
99
+ Gson gson = new Gson ();
100
+ out .beginObject ();
101
+
102
+ streamJsonWireProtocolParameters (out , gson , des , req );
103
+ streamGeckoDriver013Parameters (out , gson , des , req );
104
+ streamW3CProtocolParameters (out , gson , des , req );
105
+
106
+ out .endObject ();
107
+ out .flush ();
108
+
109
+ long size = Files .size (jsonFile );
110
+ try (InputStream rawIn = Files .newInputStream (jsonFile );
111
+ BufferedInputStream contentStream = new BufferedInputStream (rawIn )) {
112
+ LOG .fine ("Attempting multi-dialect session, assuming Postel's Law holds true on the remote end" );
113
+ Optional <Result > result = createSession (client , contentStream , size );
114
+
115
+ if (result .isPresent ()) {
116
+ Result toReturn = result .get ();
117
+ LOG .info (String .format ("Detected dialect: %s" , toReturn .dialect ));
118
+ return toReturn ;
119
+ }
120
+ }
121
+ } finally {
122
+ Files .deleteIfExists (jsonFile );
86
123
}
87
124
88
125
throw new SessionNotCreatedException (
@@ -93,10 +130,22 @@ public Result createSession(HttpClient client, Command command)
93
130
required ));
94
131
}
95
132
96
- private void amendW3cParameters (
97
- StringBuilder parameters ,
98
- Capabilities desired ,
99
- Capabilities required ) {
133
+ private void streamJsonWireProtocolParameters (
134
+ JsonWriter out ,
135
+ Gson gson ,
136
+ JsonObject des ,
137
+ JsonObject req ) throws IOException {
138
+ out .name ("desiredCapabilities" );
139
+ gson .toJson (des , out );
140
+ out .name ("requiredCapabilities" );
141
+ gson .toJson (req , out );
142
+ }
143
+
144
+ private void streamW3CProtocolParameters (
145
+ JsonWriter out ,
146
+ Gson gson ,
147
+ JsonObject des ,
148
+ JsonObject req ) throws IOException {
100
149
// Technically we should be building up a combination of "alwaysMatch" and "firstMatch" options.
101
150
// We're going to do a little processing to figure out what we might be able to do, and assume
102
151
// that people don't really understand the difference between required and desired (which is
@@ -117,40 +166,37 @@ private void amendW3cParameters(
117
166
// We can't use the constants defined in the classes because it would introduce circular
118
167
// dependencies between the remote library and the implementations. Yay!
119
168
120
- Map <String , ?> req = required .asMap ();
121
- Map <String , ?> des = desired .asMap ();
122
-
123
169
Map <String , ?> chrome = Stream .of (des , req )
124
- .map (Map ::entrySet )
170
+ .map (JsonObject ::entrySet )
125
171
.flatMap (Collection ::stream )
126
172
.filter (entry ->
127
- ("browserName" .equals (entry .getKey ()) && CHROME .equals (entry .getValue ())) ||
173
+ ("browserName" .equals (entry .getKey ()) && CHROME .equals (entry .getValue (). getAsString () )) ||
128
174
"chromeOptions" .equals (entry .getKey ()))
129
175
.distinct ()
130
176
.collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
131
177
132
178
Map <String , ?> edge = Stream .of (des , req )
133
- .map (Map ::entrySet )
179
+ .map (JsonObject ::entrySet )
134
180
.flatMap (Collection ::stream )
135
- .filter (entry -> ("browserName" .equals (entry .getKey ()) && EDGE .equals (entry .getValue ())))
181
+ .filter (entry -> ("browserName" .equals (entry .getKey ()) && EDGE .equals (entry .getValue (). getAsString () )))
136
182
.distinct ()
137
183
.collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
138
184
139
185
Map <String , ?> firefox = Stream .of (des , req )
140
- .map (Map ::entrySet )
186
+ .map (JsonObject ::entrySet )
141
187
.flatMap (Collection ::stream )
142
188
.filter (entry ->
143
- ("browserName" .equals (entry .getKey ()) && FIREFOX .equals (entry .getValue ())) ||
189
+ ("browserName" .equals (entry .getKey ()) && FIREFOX .equals (entry .getValue (). getAsString () )) ||
144
190
entry .getKey ().startsWith ("firefox_" ) ||
145
191
entry .getKey ().startsWith ("moz:" ))
146
192
.distinct ()
147
193
.collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
148
194
149
195
Map <String , ?> ie = Stream .of (req , des )
150
- .map (Map ::entrySet )
196
+ .map (JsonObject ::entrySet )
151
197
.flatMap (Collection ::stream )
152
198
.filter (entry ->
153
- ("browserName" .equals (entry .getKey ()) && IE .equals (entry .getValue ())) ||
199
+ ("browserName" .equals (entry .getKey ()) && IE .equals (entry .getValue (). getAsString () )) ||
154
200
"browserAttachTimeout" .equals (entry .getKey ()) ||
155
201
"enableElementCacheCleanup" .equals (entry .getKey ()) ||
156
202
"enablePersistentHover" .equals (entry .getKey ()) ||
@@ -167,20 +213,20 @@ private void amendW3cParameters(
167
213
.collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
168
214
169
215
Map <String , ?> opera = Stream .of (des , req )
170
- .map (Map ::entrySet )
216
+ .map (JsonObject ::entrySet )
171
217
.flatMap (Collection ::stream )
172
218
.filter (entry ->
173
- ("browserName" .equals (entry .getKey ()) && OPERA_BLINK .equals (entry .getValue ())) ||
174
- ("browserName" .equals (entry .getKey ()) && OPERA .equals (entry .getValue ())) ||
219
+ ("browserName" .equals (entry .getKey ()) && OPERA_BLINK .equals (entry .getValue (). getAsString () )) ||
220
+ ("browserName" .equals (entry .getKey ()) && OPERA .equals (entry .getValue (). getAsString () )) ||
175
221
"operaOptions" .equals (entry .getKey ()))
176
222
.distinct ()
177
223
.collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
178
224
179
225
Map <String , ?> safari = Stream .of (des , req )
180
- .map (Map ::entrySet )
226
+ .map (JsonObject ::entrySet )
181
227
.flatMap (Collection ::stream )
182
228
.filter (entry ->
183
- ("browserName" .equals (entry .getKey ()) && SAFARI .equals (entry .getValue ())) ||
229
+ ("browserName" .equals (entry .getKey ()) && SAFARI .equals (entry .getValue (). getAsString () )) ||
184
230
"safari.options" .equals (entry .getKey ()))
185
231
.distinct ()
186
232
.collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
@@ -191,35 +237,62 @@ private void amendW3cParameters(
191
237
.distinct ()
192
238
.collect (ImmutableSet .toImmutableSet ());
193
239
194
- Map < String , ?> alwaysMatch = Stream .of (des , req )
195
- .map (Map ::entrySet )
240
+ JsonObject alwaysMatch = Stream .of (des , req )
241
+ .map (JsonObject ::entrySet )
196
242
.flatMap (Collection ::stream )
197
243
.filter (entry -> !excludedKeys .contains (entry .getKey ()))
198
244
.filter (entry -> entry .getValue () != null )
199
- .filter (entry -> !"marionette" . equals (entry .getKey ())) // We never want to send this
245
+ .filter (entry -> !UNUSED_W3C_NAMES . contains (entry .getKey ())) // We never want to send this
200
246
.distinct ()
201
- .collect (Collectors .toMap (Map .Entry ::getKey , Map .Entry ::getValue ));
247
+ .collect (Collector .of (
248
+ JsonObject ::new ,
249
+ (obj , e ) -> obj .add (e .getKey (), e .getValue ()),
250
+ (left , right ) -> {
251
+ for (Map .Entry <String , JsonElement > entry : right .entrySet ()) {
252
+ left .add (entry .getKey (), entry .getValue ());
253
+ }
254
+ return left ;
255
+ }));
202
256
203
257
// Now, hopefully we're left with just the browser-specific pieces. Skip the empty ones.
204
- List < Map < String , ?>> firstMatch = Stream .of (chrome , edge , firefox , ie , opera , safari )
258
+ JsonArray firstMatch = Stream .of (chrome , edge , firefox , ie , opera , safari )
205
259
.filter (map -> !map .isEmpty ())
206
- .collect (ImmutableList .toImmutableList ());
207
-
208
- BeanToJsonConverter converter = new BeanToJsonConverter ();
209
- parameters .append ("\" alwaysMatch\" : " ).append (converter .convert (alwaysMatch )).append ("," );
210
- parameters .append ("\" firstMatch\" : " ).append (converter .convert (firstMatch ));
260
+ .map (map -> {
261
+ JsonObject json = new JsonObject ();
262
+ for (Map .Entry <String , ?> entry : map .entrySet ()) {
263
+ if (!UNUSED_W3C_NAMES .contains (entry .getKey ())) {
264
+ json .add (entry .getKey (), gson .toJsonTree (entry .getValue ()));
265
+ }
266
+ }
267
+ return json ;
268
+ })
269
+ .collect (Collector .of (
270
+ JsonArray ::new ,
271
+ JsonArray ::add ,
272
+ (left , right ) -> {
273
+ for (JsonElement element : right ) {
274
+ left .add (element );
275
+ }
276
+ return left ;
277
+ }
278
+ ));
279
+
280
+ // TODO(simon): transform some capabilities that changed in the spec (timeout's "pageLoad")
281
+
282
+ out .name ("alwaysMatch" );
283
+ gson .toJson (alwaysMatch , out );
284
+ out .name ("firstMatch" );
285
+ gson .toJson (firstMatch , out );
211
286
}
212
287
213
- private Optional <Result > createSession (HttpClient client , StringBuilder params )
288
+ private Optional <Result > createSession (HttpClient client , InputStream newSessionBlob , long size )
214
289
throws IOException {
215
290
// Create the http request and send it
216
291
HttpRequest request = new HttpRequest (HttpMethod .POST , "/session" );
217
- String content = params .toString ();
218
- byte [] data = content .getBytes (UTF_8 );
219
292
220
- request .setHeader (CONTENT_LENGTH , String .valueOf (data . length ));
293
+ request .setHeader (CONTENT_LENGTH , String .valueOf (size ));
221
294
request .setHeader (CONTENT_TYPE , JSON_UTF_8 .toString ());
222
- request .setContent (data );
295
+ request .setContent (newSessionBlob );
223
296
HttpResponse response = client .execute (request , true );
224
297
225
298
Map <?, ?> jsonBlob = null ;
@@ -231,6 +304,10 @@ private Optional<Result> createSession(HttpClient client, StringBuilder params)
231
304
return Optional .empty ();
232
305
} catch (JsonException e ) {
233
306
// Fine. Handle that below
307
+ LOG .log (
308
+ Level .FINE ,
309
+ "Unable to parse json response. Will continue but diagnostic follows" ,
310
+ e );
234
311
}
235
312
236
313
if (jsonBlob == null ) {
@@ -292,27 +369,20 @@ private Optional<Result> createSession(HttpClient client, StringBuilder params)
292
369
return Optional .empty ();
293
370
}
294
371
295
- private void amendGeckoDriver013Parameters (
296
- StringBuilder params ,
297
- String desired ,
298
- String required ) {
299
- params .append ("\" capabilities\" : {" );
300
- params .append ("\" desiredCapabilities\" : " ).append (desired );
301
- params .append ("," );
302
- params .append ("\" requiredCapabilities\" : " ).append (required );
303
- params .append ("}" );
372
+ private void streamGeckoDriver013Parameters (
373
+ JsonWriter out ,
374
+ Gson gson ,
375
+ JsonObject des ,
376
+ JsonObject req ) throws IOException {
377
+ out .name ("capabilities" );
378
+ out .beginObject ();
379
+ out .name ("desiredCapabilities" );
380
+ gson .toJson (des , out );
381
+ out .name ("requiredCapabilities" );
382
+ gson .toJson (req , out );
383
+ out .endObject (); // End "capabilities"
304
384
}
305
385
306
- private void amendOssParameters (
307
- StringBuilder params ,
308
- String desired ,
309
- String required ) {
310
- params .append ("\" desiredCapabilities\" : " ).append (desired );
311
- params .append ("," );
312
- params .append ("\" requiredCapabilities\" : " ).append (required );
313
- }
314
-
315
-
316
386
public class Result {
317
387
private final Dialect dialect ;
318
388
private final Map <String , ?> capabilities ;
0 commit comments