/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.StringUtils;
/**
* Represents a WebSocket extension as defined in the RFC 6455.
* WebSocket extensions add protocol features to the WebSocket protocol. The extensions
* used within a session are negotiated during the handshake phase as follows:
* <ul>
* <li>the client may ask for specific extensions in the HTTP handshake request</li>
* <li>the server responds with the final list of extensions to use in the current session</li>
* </ul>
*
* <p>WebSocket Extension HTTP headers may include parameters and follow
* <a href="http://tools.ietf.org/html/rfc7230#section-3.2">RFC 7230 section 3.2</a></p>
*
* <p>Note that the order of extensions in HTTP headers defines their order of execution,
* e.g. extensions "foo, bar" will be executed as "bar(foo(message))".</p>
*
* @author Brian Clozel
* @author Juergen Hoeller
* @since 4.0
* @see <a href="https://tools.ietf.org/html/rfc6455#section-9">WebSocket Protocol Extensions, RFC 6455 - Section 9</a>
*/
public class WebSocketExtension {
private final String name;
private final Map<String, String> parameters;
/**
* Create a WebSocketExtension with the given name.
* @param name the name of the extension
*/
public WebSocketExtension(String name) {
this(name, null);
}
/**
* Create a WebSocketExtension with the given name and parameters.
* @param name the name of the extension
* @param parameters the parameters
*/
public WebSocketExtension(String name, Map<String, String> parameters) {
Assert.hasLength(name, "Extension name must not be empty");
this.name = name;
if (!CollectionUtils.isEmpty(parameters)) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ENGLISH);
map.putAll(parameters);
this.parameters = Collections.unmodifiableMap(map);
}
else {
this.parameters = Collections.emptyMap();
}
}
/**
* Return the name of the extension (never {@code null) or empty}.
*/
public String getName() {
return this.name;
}
/**
* Return the parameters of the extension (never {@code null}).
*/
public Map<String, String> getParameters() {
return this.parameters;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
WebSocketExtension otherExt = (WebSocketExtension) other;
return (this.name.equals(otherExt.name) && this.parameters.equals(otherExt.parameters));
}
@Override
public int hashCode() {
return this.name.hashCode() * 31 + this.parameters.hashCode();
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append(this.name);
for (Map.Entry<String, String> entry : this.parameters.entrySet()) {
str.append(';');
str.append(entry.getKey());
str.append('=');
str.append(entry.getValue());
}
return str.toString();
}
/**
* Parse the given, comma-separated string into a list of {@code WebSocketExtension} objects.
* <p>This method can be used to parse a "Sec-WebSocket-Extension" header.
* @param extensions the string to parse
* @return the list of extensions
* @throws IllegalArgumentException if the string cannot be parsed
*/
public static List<WebSocketExtension> parseExtensions(String extensions) {
if (StringUtils.hasText(extensions)) {
String[] tokens = StringUtils.tokenizeToStringArray(extensions, ",");
List<WebSocketExtension> result = new ArrayList<WebSocketExtension>(tokens.length);
for (String token : tokens) {
result.add(parseExtension(token));
}
return result;
}
else {
return Collections.emptyList();
}
}
private static WebSocketExtension parseExtension(String extension) {
if (extension.contains(",")) {
throw new IllegalArgumentException("Expected single extension value: [" + extension + "]");
}
String[] parts = StringUtils.tokenizeToStringArray(extension, ";");
String name = parts[0].trim();
Map<String, String> parameters = null;
if (parts.length > 1) {
parameters = new LinkedHashMap<>(parts.length - 1);
for (int i = 1; i < parts.length; i++) {
String parameter = parts[i];
int eqIndex = parameter.indexOf('=');
if (eqIndex != -1) {
String attribute = parameter.substring(0, eqIndex);
String value = parameter.substring(eqIndex + 1, parameter.length());
parameters.put(attribute, value);
}
}
}
return new WebSocketExtension(name, parameters);
}
}