001/* 002 * Copyright 2002-2016 the original author or authors. 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 * https://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 */ 016 017package org.springframework.web.socket; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.LinkedHashMap; 022import java.util.List; 023import java.util.Locale; 024import java.util.Map; 025 026import org.springframework.util.Assert; 027import org.springframework.util.CollectionUtils; 028import org.springframework.util.LinkedCaseInsensitiveMap; 029import org.springframework.util.StringUtils; 030 031/** 032 * Represents a WebSocket extension as defined in the RFC 6455. 033 * WebSocket extensions add protocol features to the WebSocket protocol. The extensions 034 * used within a session are negotiated during the handshake phase as follows: 035 * <ul> 036 * <li>the client may ask for specific extensions in the HTTP handshake request</li> 037 * <li>the server responds with the final list of extensions to use in the current session</li> 038 * </ul> 039 * 040 * <p>WebSocket Extension HTTP headers may include parameters and follow 041 * <a href="https://tools.ietf.org/html/rfc7230#section-3.2">RFC 7230 section 3.2</a></p> 042 * 043 * <p>Note that the order of extensions in HTTP headers defines their order of execution, 044 * e.g. extensions "foo, bar" will be executed as "bar(foo(message))".</p> 045 * 046 * @author Brian Clozel 047 * @author Juergen Hoeller 048 * @since 4.0 049 * @see <a href="https://tools.ietf.org/html/rfc6455#section-9">WebSocket Protocol Extensions, RFC 6455 - Section 9</a> 050 */ 051public class WebSocketExtension { 052 053 private final String name; 054 055 private final Map<String, String> parameters; 056 057 058 /** 059 * Create a WebSocketExtension with the given name. 060 * @param name the name of the extension 061 */ 062 public WebSocketExtension(String name) { 063 this(name, null); 064 } 065 066 /** 067 * Create a WebSocketExtension with the given name and parameters. 068 * @param name the name of the extension 069 * @param parameters the parameters 070 */ 071 public WebSocketExtension(String name, Map<String, String> parameters) { 072 Assert.hasLength(name, "Extension name must not be empty"); 073 this.name = name; 074 if (!CollectionUtils.isEmpty(parameters)) { 075 Map<String, String> map = new LinkedCaseInsensitiveMap<String>(parameters.size(), Locale.ENGLISH); 076 map.putAll(parameters); 077 this.parameters = Collections.unmodifiableMap(map); 078 } 079 else { 080 this.parameters = Collections.emptyMap(); 081 } 082 } 083 084 085 /** 086 * Return the name of the extension (never {@code null) or empty}. 087 */ 088 public String getName() { 089 return this.name; 090 } 091 092 /** 093 * Return the parameters of the extension (never {@code null}). 094 */ 095 public Map<String, String> getParameters() { 096 return this.parameters; 097 } 098 099 100 @Override 101 public boolean equals(Object other) { 102 if (this == other) { 103 return true; 104 } 105 if (other == null || getClass() != other.getClass()) { 106 return false; 107 } 108 WebSocketExtension otherExt = (WebSocketExtension) other; 109 return (this.name.equals(otherExt.name) && this.parameters.equals(otherExt.parameters)); 110 } 111 112 @Override 113 public int hashCode() { 114 return this.name.hashCode() * 31 + this.parameters.hashCode(); 115 } 116 117 @Override 118 public String toString() { 119 StringBuilder str = new StringBuilder(); 120 str.append(this.name); 121 for (Map.Entry<String, String> entry : this.parameters.entrySet()) { 122 str.append(';'); 123 str.append(entry.getKey()); 124 str.append('='); 125 str.append(entry.getValue()); 126 } 127 return str.toString(); 128 } 129 130 131 /** 132 * Parse the given, comma-separated string into a list of {@code WebSocketExtension} objects. 133 * <p>This method can be used to parse a "Sec-WebSocket-Extension" header. 134 * @param extensions the string to parse 135 * @return the list of extensions 136 * @throws IllegalArgumentException if the string cannot be parsed 137 */ 138 public static List<WebSocketExtension> parseExtensions(String extensions) { 139 if (StringUtils.hasText(extensions)) { 140 String[] tokens = StringUtils.tokenizeToStringArray(extensions, ","); 141 List<WebSocketExtension> result = new ArrayList<WebSocketExtension>(tokens.length); 142 for (String token : tokens) { 143 result.add(parseExtension(token)); 144 } 145 return result; 146 } 147 else { 148 return Collections.emptyList(); 149 } 150 } 151 152 private static WebSocketExtension parseExtension(String extension) { 153 if (extension.contains(",")) { 154 throw new IllegalArgumentException("Expected single extension value: [" + extension + "]"); 155 } 156 String[] parts = StringUtils.tokenizeToStringArray(extension, ";"); 157 String name = parts[0].trim(); 158 159 Map<String, String> parameters = null; 160 if (parts.length > 1) { 161 parameters = new LinkedHashMap<String, String>(parts.length - 1); 162 for (int i = 1; i < parts.length; i++) { 163 String parameter = parts[i]; 164 int eqIndex = parameter.indexOf('='); 165 if (eqIndex != -1) { 166 String attribute = parameter.substring(0, eqIndex); 167 String value = parameter.substring(eqIndex + 1); 168 parameters.put(attribute, value); 169 } 170 } 171 } 172 173 return new WebSocketExtension(name, parameters); 174 } 175 176}