15
15
// specific language governing permissions and limitations
16
16
// under the License.
17
17
18
- package org .openqa .selenium .remote . server ;
18
+ package org .openqa .selenium .remote ;
19
19
20
20
import static java .nio .charset .StandardCharsets .UTF_8 ;
21
21
import static java .nio .file .StandardOpenOption .CREATE ;
25
25
import com .google .common .collect .ImmutableList ;
26
26
import com .google .common .collect .ImmutableMap ;
27
27
import com .google .common .collect .ImmutableSet ;
28
+ import com .google .common .collect .ImmutableSortedMap ;
28
29
import com .google .common .collect .ImmutableSortedSet ;
29
30
import com .google .common .collect .Ordering ;
30
31
import com .google .common .collect .Sets ;
36
37
import org .openqa .selenium .json .Json ;
37
38
import org .openqa .selenium .json .JsonInput ;
38
39
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 ;
40
52
41
53
import java .io .ByteArrayInputStream ;
42
54
import java .io .Closeable ;
43
55
import java .io .IOException ;
44
56
import java .io .InputStream ;
57
+ import java .io .InputStreamReader ;
45
58
import java .io .Reader ;
46
- import java .io .StringReader ;
47
59
import java .io .UncheckedIOException ;
48
60
import java .io .Writer ;
49
61
import java .nio .file .Files ;
50
62
import java .nio .file .Path ;
51
63
import java .util .Collection ;
64
+ import java .util .HashSet ;
52
65
import java .util .LinkedList ;
53
66
import java .util .List ;
54
67
import java .util .Map ;
55
68
import java .util .Objects ;
56
69
import java .util .Optional ;
70
+ import java .util .Queue ;
71
+ import java .util .ServiceLoader ;
57
72
import java .util .Set ;
58
73
import java .util .TreeMap ;
59
74
import java .util .TreeSet ;
@@ -68,6 +83,9 @@ public class NewSessionPayload implements Closeable {
68
83
69
84
private static final Logger LOG = Logger .getLogger (NewSessionPayload .class .getName ());
70
85
86
+ private final Set <CapabilitiesFilter > adapters ;
87
+ private final Set <CapabilityTransform > transforms ;
88
+
71
89
private static final Dialect DEFAULT_DIALECT = Dialect .OSS ;
72
90
private final static Predicate <String > ACCEPTED_W3C_PATTERNS = Stream .of (
73
91
"^[\\ w-]+:.*$" ,
@@ -111,29 +129,23 @@ public static NewSessionPayload create(Capabilities caps) throws IOException {
111
129
"capabilities" , ImmutableMap .of (
112
130
"firstMatch" , ImmutableList .of (w3cCaps .build ())));
113
131
114
- return new NewSessionPayload (builder .build ());
132
+ return create (builder .build ());
115
133
}
116
134
117
- public NewSessionPayload (Map <String , ?> source ) throws IOException {
135
+ public static NewSessionPayload create (Map <String , ?> source ) throws IOException {
118
136
Objects .requireNonNull (source , "Payload must be set" );
119
137
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
+ }
131
143
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 );
134
146
}
135
147
136
- public NewSessionPayload (long size , Reader source ) throws IOException {
148
+ private NewSessionPayload (long size , Reader source ) throws IOException {
137
149
Sources sources ;
138
150
if (size > THRESHOLD || Runtime .getRuntime ().freeMemory () < size ) {
139
151
this .root = Files .createTempDirectory ("new-session" );
@@ -145,6 +157,63 @@ public NewSessionPayload(long size, Reader source) throws IOException {
145
157
146
158
validate (sources );
147
159
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
+ }
148
217
}
149
218
150
219
private void validate (Sources sources ) {
@@ -181,6 +250,91 @@ private void validateSpecCompliance(Map<String, Object> fragment) {
181
250
}
182
251
}
183
252
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
+
184
338
/**
185
339
* If the local end sent a request with a JSON Wire Protocol payload that does not have a matching
186
340
* W3C payload, then we need to synthesize one that matches.
@@ -433,7 +587,6 @@ public void close() {
433
587
}
434
588
}
435
589
436
-
437
590
private static class Sources {
438
591
439
592
private final Supplier <InputStream > originalPayload ;
0 commit comments