A hint: This file contains one or more very long lines, so maybe it is better readable using the pure text view mode that shows the contents as wrapped lines within the browser window.
1 // <copyright file="RemoteSessionSettings.cs" company="WebDriver Committers"> 2 // Licensed to the Software Freedom Conservancy (SFC) under one 3 // or more contributor license agreements. See the NOTICE file 4 // distributed with this work for additional information 5 // regarding copyright ownership. The SFC licenses this file 6 // to you under the Apache License, Version 2.0 (the "License"); 7 // you may not use this file except in compliance with the License. 8 // You may obtain a copy of the License at 9 // 10 // http://www.apache.org/licenses/LICENSE-2.0 11 // 12 // Unless required by applicable law or agreed to in writing, software 13 // distributed under the License is distributed on an "AS IS" BASIS, 14 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 // See the License for the specific language governing permissions and 16 // limitations under the License. 17 // </copyright> 18 19 using System; 20 using System.Collections; 21 using System.Collections.Generic; 22 using System.Globalization; 23 using Newtonsoft.Json; 24 using OpenQA.Selenium.Remote; 25 26 namespace OpenQA.Selenium 27 { 28 /// <summary> 29 /// Base class for managing options specific to a browser driver. 30 /// </summary> 31 public class RemoteSessionSettings : ICapabilities 32 { 33 private const string FirstMatchCapabilityName = "firstMatch"; 34 private const string AlwaysMatchCapabilityName = "alwaysMatch"; 35 36 private readonly List<string> reservedSettingNames = new List<string>() { FirstMatchCapabilityName, AlwaysMatchCapabilityName }; 37 private DriverOptions mustMatchDriverOptions; 38 private List<DriverOptions> firstMatchOptions = new List<DriverOptions>(); 39 private Dictionary<string, object> remoteMetadataSettings = new Dictionary<string, object>(); 40 41 /// <summary> 42 /// Creates a new instance of the <see cref="RemoteSessionSettings"/> class. 43 /// </summary> 44 public RemoteSessionSettings() 45 { 46 } 47 48 /// <summary> 49 /// Creates a new instance of the <see cref="RemoteSessionSettings"/> class, 50 /// containing the specified <see cref="DriverOptions"/> to use in the remote 51 /// session. 52 /// </summary> 53 /// <param name="mustMatchDriverOptions"> 54 /// A <see cref="DriverOptions"/> object that contains values that must be matched 55 /// by the remote end to create the remote session. 56 /// </param> 57 /// <param name="firstMatchDriverOptions"> 58 /// A list of <see cref="DriverOptions"/> objects that contain values that may be matched 59 /// by the remote end to create the remote session. 60 /// </param> 61 public RemoteSessionSettings(DriverOptions mustMatchDriverOptions, params DriverOptions[] firstMatchDriverOptions) 62 { 63 this.mustMatchDriverOptions = mustMatchDriverOptions; 64 foreach (DriverOptions firstMatchOption in firstMatchDriverOptions) 65 { 66 this.AddFirstMatchDriverOption(firstMatchOption); 67 } 68 } 69 70 /// <summary> 71 /// Gets a value indicating the options that must be matched by the remote end to create a session. 72 /// </summary> 73 internal DriverOptions MustMatchDriverOptions 74 { 75 get { return this.mustMatchDriverOptions; } 76 } 77 78 /// <summary> 79 /// Gets a value indicating the number of options that may be matched by the remote end to create a session. 80 /// </summary> 81 internal int FirstMatchOptionsCount 82 { 83 get { return this.firstMatchOptions.Count; } 84 } 85 86 /// <summary> 87 /// Gets the capability value with the specified name. 88 /// </summary> 89 /// <param name="capabilityName">The name of the capability to get.</param> 90 /// <returns>The value of the capability.</returns> 91 /// <exception cref="ArgumentException"> 92 /// The specified capability name is not in the set of capabilities. 93 /// </exception> 94 public object this[string capabilityName] 95 { 96 get 97 { 98 if (capabilityName == AlwaysMatchCapabilityName) 99 { 100 return this.GetAlwaysMatchOptionsAsSerializableDictionary(); 101 } 102 103 if (capabilityName == FirstMatchCapabilityName) 104 { 105 return this.GetFirstMatchOptionsAsSerializableList(); 106 } 107 108 if (!this.remoteMetadataSettings.ContainsKey(capabilityName)) 109 { 110 throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "The capability {0} is not present in this set of capabilities", capabilityName)); 111 } 112 113 return this.remoteMetadataSettings[capabilityName]; 114 } 115 116 } 117 118 /// <summary> 119 /// Add a metadata setting to this set of remote session settings. 120 /// </summary> 121 /// <param name="settingName">The name of the setting to set.</param> 122 /// <param name="settingValue">The value of the setting.</param> 123 /// <remarks> 124 /// The value to be set must be serializable to JSON for transmission 125 /// across the wire to the remote end. To be JSON-serializable, the value 126 /// must be a string, a numeric value, a boolean value, an object that 127 /// implmeents <see cref="IEnumerable"/> that contains JSON-serializable 128 /// objects, or a <see cref="Dictionary{TKey, TValue}"/> where the keys 129 /// are strings and the values are JSON-serializable. 130 /// </remarks> 131 /// <exception cref="ArgumentException"> 132 /// Thrown if the setting name is null, the empty string, or one of the 133 /// reserved names of metadata settings; or if the setting value is not 134 /// JSON serializable. 135 /// </exception> 136 public void AddMetadataSetting(string settingName, object settingValue) 137 { 138 if (string.IsNullOrEmpty(settingName)) 139 { 140 throw new ArgumentException("Metadata setting name cannot be null or empty", nameof(settingName)); 141 } 142 143 if (this.reservedSettingNames.Contains(settingName)) 144 { 145 throw new ArgumentException(string.Format("'{0}' is a reserved name for a metadata setting, and cannot be used as a name.", settingName), nameof(settingName)); 146 } 147 148 if (!this.IsJsonSerializable(settingValue)) 149 { 150 throw new ArgumentException("Metadata setting value must be JSON serializable.", nameof(settingValue)); 151 } 152 153 this.remoteMetadataSettings[settingName] = settingValue; 154 } 155 156 /// <summary> 157 /// Adds a <see cref="DriverOptions"/> object to the list of options containing values to be 158 /// "first matched" by the remote end. 159 /// </summary> 160 /// <param name="options">The <see cref="DriverOptions"/> to add to the list of "first matched" options.</param> 161 public void AddFirstMatchDriverOption(DriverOptions options) 162 { 163 if (mustMatchDriverOptions != null) 164 { 165 DriverOptionsMergeResult mergeResult = mustMatchDriverOptions.GetMergeResult(options); 166 if (mergeResult.IsMergeConflict) 167 { 168 string msg = string.Format(CultureInfo.InvariantCulture, "You cannot request the same capability in both must-match and first-match capabilities. You are attempting to add a first-match driver options object that defines a capability, '{0}', that is already defined in the must-match driver options.", mergeResult.MergeConflictOptionName); 169 throw new ArgumentException(msg, nameof(options)); 170 } 171 } 172 173 firstMatchOptions.Add(options); 174 } 175 176 /// <summary> 177 /// Adds a <see cref="DriverOptions"/> object containing values that must be matched 178 /// by the remote end to successfully create a session. 179 /// </summary> 180 /// <param name="options">The <see cref="DriverOptions"/> that must be matched by 181 /// the remote end to successfully create a session.</param> 182 public void SetMustMatchDriverOptions(DriverOptions options) 183 { 184 if (this.firstMatchOptions.Count > 0) 185 { 186 int driverOptionIndex = 0; 187 foreach (DriverOptions firstMatchOption in this.firstMatchOptions) 188 { 189 DriverOptionsMergeResult mergeResult = firstMatchOption.GetMergeResult(options); 190 if (mergeResult.IsMergeConflict) 191 { 192 string msg = string.Format(CultureInfo.InvariantCulture, "You cannot request the same capability in both must-match and first-match capabilities. You are attempting to add a must-match driver options object that defines a capability, '{0}', that is already defined in the first-match driver options with index {1}.", mergeResult.MergeConflictOptionName, driverOptionIndex); 193 throw new ArgumentException(msg, nameof(options)); 194 } 195 196 driverOptionIndex++; 197 } 198 } 199 200 this.mustMatchDriverOptions = options; 201 } 202 203 /// <summary> 204 /// Gets a value indicating whether the browser has a given capability. 205 /// </summary> 206 /// <param name="capability">The capability to get.</param> 207 /// <returns>Returns <see langword="true"/> if this set of capabilities has the capability; 208 /// otherwise, <see langword="false"/>.</returns> 209 public bool HasCapability(string capability) 210 { 211 if (capability == AlwaysMatchCapabilityName || capability == FirstMatchCapabilityName) 212 { 213 return true; 214 } 215 216 return this.remoteMetadataSettings.ContainsKey(capability); 217 } 218 219 /// <summary> 220 /// Gets a capability of the browser. 221 /// </summary> 222 /// <param name="capability">The capability to get.</param> 223 /// <returns>An object associated with the capability, or <see langword="null"/> 224 /// if the capability is not set in this set of capabilities.</returns> 225 public object GetCapability(string capability) 226 { 227 if (capability == AlwaysMatchCapabilityName) 228 { 229 return this.GetAlwaysMatchOptionsAsSerializableDictionary(); 230 } 231 232 if (capability == FirstMatchCapabilityName) 233 { 234 return this.GetFirstMatchOptionsAsSerializableList(); 235 } 236 237 if (this.remoteMetadataSettings.ContainsKey(capability)) 238 { 239 return this.remoteMetadataSettings[capability]; 240 } 241 242 return null; 243 } 244 245 /// <summary> 246 /// Return a dictionary representation of this <see cref="RemoteSessionSettings"/>. 247 /// </summary> 248 /// <returns>A <see cref="Dictionary{TKey, TValue}"/> representation of this <see cref="RemoteSessionSettings"/>.</returns> 249 public Dictionary<string, object> ToDictionary() 250 { 251 Dictionary<string, object> capabilitiesDictionary = new Dictionary<string, object>(); 252 253 foreach (KeyValuePair<string, object> remoteMetadataSetting in this.remoteMetadataSettings) 254 { 255 capabilitiesDictionary[remoteMetadataSetting.Key] = remoteMetadataSetting.Value; 256 } 257 258 if (this.mustMatchDriverOptions != null) 259 { 260 capabilitiesDictionary["alwaysMatch"] = GetAlwaysMatchOptionsAsSerializableDictionary(); 261 } 262 263 if (this.firstMatchOptions.Count > 0) 264 { 265 List<object> optionsMatches = GetFirstMatchOptionsAsSerializableList(); 266 267 capabilitiesDictionary["firstMatch"] = optionsMatches; 268 } 269 270 return capabilitiesDictionary; 271 } 272 273 /// <summary> 274 /// Return a string representation of the remote session settings to be sent. 275 /// </summary> 276 /// <returns>String representation of the remote session settings to be sent.</returns> 277 public override string ToString() 278 { 279 return JsonConvert.SerializeObject(this.ToDictionary(), Formatting.Indented); 280 } 281 282 internal DriverOptions GetFirstMatchDriverOptions(int firstMatchIndex) 283 { 284 if (firstMatchIndex < 0 || firstMatchIndex >= this.firstMatchOptions.Count) 285 { 286 throw new ArgumentException("Requested index must be greater than zero and less than the count of firstMatch options added."); 287 } 288 289 return this.firstMatchOptions[firstMatchIndex]; 290 } 291 292 private IDictionary<string, object> GetAlwaysMatchOptionsAsSerializableDictionary() 293 { 294 return this.mustMatchDriverOptions.ToDictionary(); 295 } 296 297 private List<object> GetFirstMatchOptionsAsSerializableList() 298 { 299 List<object> optionsMatches = new List<object>(); 300 foreach (DriverOptions options in this.firstMatchOptions) 301 { 302 optionsMatches.Add(options.ToDictionary()); 303 } 304 305 return optionsMatches; 306 } 307 308 private bool IsJsonSerializable(object arg) 309 { 310 IEnumerable argAsEnumerable = arg as IEnumerable; 311 IDictionary argAsDictionary = arg as IDictionary; 312 313 if (arg is string || arg is float || arg is double || arg is int || arg is long || arg is bool || arg == null) 314 { 315 return true; 316 } 317 else if (argAsDictionary != null) 318 { 319 foreach (object key in argAsDictionary.Keys) 320 { 321 if (!(key is string)) 322 { 323 return false; 324 } 325 } 326 327 foreach (object value in argAsDictionary.Values) 328 { 329 if (!IsJsonSerializable(value)) 330 { 331 return false; 332 } 333 } 334 } 335 else if (argAsEnumerable != null) 336 { 337 foreach (object item in argAsEnumerable) 338 { 339 if (!IsJsonSerializable(item)) 340 { 341 return false; 342 } 343 } 344 } 345 else 346 { 347 return false; 348 } 349 350 return true; 351 } 352 } 353 }