001/* 002 * Copyright (C) 2015-2016 Neo Visionaries Inc. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package NeoVisionaries.WebSockets; 017 018 019import java.util.LinkedHashMap; 020import java.util.Map; 021 022 023/** 024 * A class to hold the name and the parameters of 025 * a WebSocket extension. 026 * 027 * <EMBED CLASS='external-html' DATA-FILE-ID=LICENSE> 028 */ 029public class WebSocketExtension 030{ 031 /** 032 * The name of <code>permessage-deflate</code> extension that is 033 * defined in <a href="https://tools.ietf.org/html/rfc7692#section-7" 034 * >7. The "permessage-deflate" Extension</a> in <a href= 035 * "https://tools.ietf.org/html/rfc7692">RFC 7692</a>. 036 * 037 * @since 1.17 038 */ 039 public static final String PERMESSAGE_DEFLATE = "permessage-deflate"; 040 041 042 private final String mName; 043 private final Map<String, String> mParameters; 044 045 046 /** 047 * Constructor with an extension name. 048 * 049 * @param name 050 * The extension name. 051 * 052 * @throws IllegalArgumentException 053 * The given name is not a valid token. 054 */ 055 public WebSocketExtension(String name) 056 { 057 // Check the validity of the name. 058 if (Token.isValid(name) == false) 059 { 060 // The name is not a valid token. 061 throw new IllegalArgumentException("'name' is not a valid token."); 062 } 063 064 mName = name; 065 mParameters = new LinkedHashMap<String, String>(); 066 } 067 068 069 /** 070 * Copy constructor. 071 * 072 * @param source 073 * A source extension. Must not be {@code null}. 074 * 075 * @throws IllegalArgumentException 076 * The given argument is {@code null}. 077 * 078 * @since 1.6 079 */ 080 public WebSocketExtension(WebSocketExtension source) 081 { 082 if (source == null) 083 { 084 // If the given instance is null. 085 throw new IllegalArgumentException("'source' is null."); 086 } 087 088 mName = source.getName(); 089 mParameters = new LinkedHashMap<String, String>(source.getParameters()); 090 } 091 092 093 /** 094 * Get the extension name. 095 * 096 * @return 097 * The extension name. 098 */ 099 public String getName() 100 { 101 return mName; 102 } 103 104 105 /** 106 * Get the parameters. 107 * 108 * @return 109 * The parameters. 110 */ 111 public Map<String, String> getParameters() 112 { 113 return mParameters; 114 } 115 116 117 /** 118 * Check if the parameter identified by the key is contained. 119 * 120 * @param key 121 * The name of the parameter. 122 * 123 * @return 124 * {@code true} if the parameter is contained. 125 */ 126 public boolean containsParameter(String key) 127 { 128 return mParameters.containsKey(key); 129 } 130 131 132 /** 133 * Get the value of the specified parameter. 134 * 135 * @param key 136 * The name of the parameter. 137 * 138 * @return 139 * The value of the parameter. {@code null} may be returned. 140 */ 141 public String getParameter(String key) 142 { 143 return mParameters.get(key); 144 } 145 146 147 /** 148 * Set a value to the specified parameter. 149 * 150 * @param key 151 * The name of the parameter. 152 * 153 * @param value 154 * The value of the parameter. If not {@code null}, it must be 155 * a valid token. Note that <a href="http://tools.ietf.org/html/rfc6455" 156 * >RFC 6455</a> says "<i>When using the quoted-string syntax 157 * variant, the value after quoted-string unescaping MUST 158 * conform to the 'token' ABNF.</i>" 159 * 160 * @return 161 * {@code this} object. 162 * 163 * @throws IllegalArgumentException 164 * <ul> 165 * <li>The key is not a valid token.</li> 166 * <li>The value is not {@code null} and it is not a valid token.</li> 167 * </ul> 168 */ 169 public WebSocketExtension setParameter(String key, String value) 170 { 171 // Check the validity of the key. 172 if (Token.isValid(key) == false) 173 { 174 // The key is not a valid token. 175 throw new IllegalArgumentException("'key' is not a valid token."); 176 } 177 178 // If the value is not null. 179 if (value != null) 180 { 181 // Check the validity of the value. 182 if (Token.isValid(value) == false) 183 { 184 // The value is not a valid token. 185 throw new IllegalArgumentException("'value' is not a valid token."); 186 } 187 } 188 189 mParameters.put(key, value); 190 191 return this; 192 } 193 194 195 /** 196 * Stringify this object into the format "{name}[; {key}[={value}]]*". 197 */ 198 @Override 199 public String toString() 200 { 201 StringBuilder builder = new StringBuilder(mName); 202 203 for (Map.Entry<String, String> entry : mParameters.entrySet()) 204 { 205 // "; {key}" 206 builder.append("; ").append(entry.getKey()); 207 208 String value = entry.getValue(); 209 210 if (value != null && value.length() != 0) 211 { 212 // "={value}" 213 builder.append("=").append(value); 214 } 215 } 216 217 return builder.toString(); 218 } 219 220 221 /** 222 * Validate this instance. This method is expected to be overridden. 223 */ 224 void validate() throws WebSocketException 225 { 226 } 227 228 229 /** 230 * Parse a string as a {@link WebSocketExtension}. The input string 231 * should comply with the format described in <a href= 232 * "https://tools.ietf.org/html/rfc6455#section-9.1">9.1. Negotiating 233 * Extensions</a> in <a href="https://tools.ietf.org/html/rfc6455" 234 * >RFC 6455</a>. 235 * 236 * @param string 237 * A string that represents a WebSocket extension. 238 * 239 * @return 240 * A new {@link WebSocketExtension} instance that represents 241 * the given string. If the input string does not comply with 242 * RFC 6455, {@code null} is returned. 243 */ 244 public static WebSocketExtension parse(String string) 245 { 246 if (string == null) 247 { 248 return null; 249 } 250 251 // Split the string by semi-colons. 252 String[] elements = string.trim().split("\\s*;\\s*"); 253 254 if (elements.length == 0) 255 { 256 // Even an extension name is not included. 257 return null; 258 } 259 260 // The first element is the extension name. 261 String name = elements[0]; 262 263 if (Token.isValid(name) == false) 264 { 265 // The extension name is not a valid token. 266 return null; 267 } 268 269 // Create an instance for the extension name. 270 WebSocketExtension extension = createInstance(name); 271 272 // For each "{key}[={value}]". 273 for (int i = 1; i < elements.length; ++i) 274 { 275 // Split by '=' to get the key and the value. 276 String[] pair = elements[i].split("\\s*=\\s*", 2); 277 278 // If {key} is not contained. 279 if (pair.length == 0 || pair[0].length() == 0) 280 { 281 // Ignore. 282 continue; 283 } 284 285 // The name of the parameter. 286 String key = pair[0]; 287 288 if (Token.isValid(key) == false) 289 { 290 // The parameter name is not a valid token. 291 // Ignore this parameter. 292 continue; 293 } 294 295 // The value of the parameter. 296 String value = extractValue(pair); 297 298 if (value != null) 299 { 300 if (Token.isValid(value) == false) 301 { 302 // The parameter value is not a valid token. 303 // Ignore this parameter. 304 continue; 305 } 306 } 307 308 // Add the pair of the key and the value. 309 extension.setParameter(key, value); 310 } 311 312 return extension; 313 } 314 315 316 private static String extractValue(String[] pair) 317 { 318 if (pair.length != 2) 319 { 320 return null; 321 } 322 323 return Token.unquote(pair[1]); 324 } 325 326 327 private static WebSocketExtension createInstance(String name) 328 { 329 if (PERMESSAGE_DEFLATE.equals(name)) 330 { 331 return new PerMessageDeflateExtension(name); 332 } 333 334 return new WebSocketExtension(name); 335 } 336}