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.messaging.simp.stomp; 018 019import java.io.Serializable; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.LinkedHashMap; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027 028import org.springframework.util.Assert; 029import org.springframework.util.LinkedMultiValueMap; 030import org.springframework.util.MimeType; 031import org.springframework.util.MimeTypeUtils; 032import org.springframework.util.MultiValueMap; 033import org.springframework.util.StringUtils; 034 035/** 036 * Represents STOMP frame headers. 037 * 038 * <p>In addition to the normal methods defined by {@link Map}, this class offers 039 * the following convenience methods: 040 * <ul> 041 * <li>{@link #getFirst(String)} return the first value for a header name</li> 042 * <li>{@link #add(String, String)} add to the list of values for a header name</li> 043 * <li>{@link #set(String, String)} set a header name to a single string value</li> 044 * </ul> 045 * 046 * @author Rossen Stoyanchev 047 * @since 4.2 048 * @see <a href="https://stomp.github.io/stomp-specification-1.2.html#Frames_and_Headers"> 049 * https://stomp.github.io/stomp-specification-1.2.html#Frames_and_Headers</a> 050 */ 051public class StompHeaders implements MultiValueMap<String, String>, Serializable { 052 053 private static final long serialVersionUID = 7514642206528452544L; 054 055 056 // Standard headers (as defined in the spec) 057 058 public static final String CONTENT_TYPE = "content-type"; // SEND, MESSAGE, ERROR 059 060 public static final String CONTENT_LENGTH = "content-length"; // SEND, MESSAGE, ERROR 061 062 public static final String RECEIPT = "receipt"; // any client frame other than CONNECT 063 064 // CONNECT 065 066 public static final String HOST = "host"; 067 068 public static final String LOGIN = "login"; 069 070 public static final String PASSCODE = "passcode"; 071 072 public static final String HEARTBEAT = "heart-beat"; 073 074 // CONNECTED 075 076 public static final String SESSION = "session"; 077 078 public static final String SERVER = "server"; 079 080 // SEND 081 082 public static final String DESTINATION = "destination"; 083 084 // SUBSCRIBE, UNSUBSCRIBE 085 086 public static final String ID = "id"; 087 088 public static final String ACK = "ack"; 089 090 // MESSAGE 091 092 public static final String SUBSCRIPTION = "subscription"; 093 094 public static final String MESSAGE_ID = "message-id"; 095 096 // RECEIPT 097 098 public static final String RECEIPT_ID = "receipt-id"; 099 100 101 private final Map<String, List<String>> headers; 102 103 104 /** 105 * Create a new instance to be populated with new header values. 106 */ 107 public StompHeaders() { 108 this(new LinkedMultiValueMap<String, String>(4), false); 109 } 110 111 private StompHeaders(Map<String, List<String>> headers, boolean readOnly) { 112 Assert.notNull(headers, "'headers' must not be null"); 113 if (readOnly) { 114 Map<String, List<String>> map = new LinkedMultiValueMap<String, String>(headers.size()); 115 for (Entry<String, List<String>> entry : headers.entrySet()) { 116 List<String> values = Collections.unmodifiableList(entry.getValue()); 117 map.put(entry.getKey(), values); 118 } 119 this.headers = Collections.unmodifiableMap(map); 120 } 121 else { 122 this.headers = headers; 123 } 124 } 125 126 127 /** 128 * Set the content-type header. 129 * Applies to the SEND, MESSAGE, and ERROR frames. 130 */ 131 public void setContentType(MimeType mimeType) { 132 Assert.isTrue(!mimeType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'"); 133 Assert.isTrue(!mimeType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'"); 134 set(CONTENT_TYPE, mimeType.toString()); 135 } 136 137 /** 138 * Return the content-type header value. 139 */ 140 public MimeType getContentType() { 141 String value = getFirst(CONTENT_TYPE); 142 return (StringUtils.hasLength(value) ? MimeTypeUtils.parseMimeType(value) : null); 143 } 144 145 /** 146 * Set the content-length header. 147 * Applies to the SEND, MESSAGE, and ERROR frames. 148 */ 149 public void setContentLength(long contentLength) { 150 set(CONTENT_LENGTH, Long.toString(contentLength)); 151 } 152 153 /** 154 * Return the content-length header or -1 if unknown. 155 */ 156 public long getContentLength() { 157 String value = getFirst(CONTENT_LENGTH); 158 return (value != null ? Long.parseLong(value) : -1); 159 } 160 161 /** 162 * Set the receipt header. 163 * Applies to any client frame other than CONNECT. 164 */ 165 public void setReceipt(String receipt) { 166 set(RECEIPT, receipt); 167 } 168 169 /** 170 * Get the receipt header. 171 */ 172 public String getReceipt() { 173 return getFirst(RECEIPT); 174 } 175 176 /** 177 * Set the host header. 178 * Applies to the CONNECT frame. 179 */ 180 public void setHost(String host) { 181 set(HOST, host); 182 } 183 184 /** 185 * Get the host header. 186 */ 187 public String getHost() { 188 return getFirst(HOST); 189 } 190 191 /** 192 * Set the login header. 193 * Applies to the CONNECT frame. 194 */ 195 public void setLogin(String login) { 196 set(LOGIN, login); 197 } 198 199 /** 200 * Get the login header. 201 */ 202 public String getLogin() { 203 return getFirst(LOGIN); 204 } 205 206 /** 207 * Set the passcode header. 208 * Applies to the CONNECT frame. 209 */ 210 public void setPasscode(String passcode) { 211 set(PASSCODE, passcode); 212 } 213 214 /** 215 * Get the passcode header. 216 */ 217 public String getPasscode() { 218 return getFirst(PASSCODE); 219 } 220 221 /** 222 * Set the heartbeat header. 223 * Applies to the CONNECT and CONNECTED frames. 224 */ 225 public void setHeartbeat(long[] heartbeat) { 226 if (heartbeat == null || heartbeat.length != 2) { 227 throw new IllegalArgumentException("Heart-beat array must be of length 2, not " + 228 (heartbeat != null ? heartbeat.length : "null")); 229 } 230 String value = heartbeat[0] + "," + heartbeat[1]; 231 if (heartbeat[0] < 0 || heartbeat[1] < 0) { 232 throw new IllegalArgumentException("Heart-beat values cannot be negative: " + value); 233 } 234 set(HEARTBEAT, value); 235 } 236 237 /** 238 * Get the heartbeat header. 239 */ 240 public long[] getHeartbeat() { 241 String rawValue = getFirst(HEARTBEAT); 242 String[] rawValues = StringUtils.split(rawValue, ","); 243 if (rawValues == null) { 244 return null; 245 } 246 return new long[] {Long.valueOf(rawValues[0]), Long.valueOf(rawValues[1])}; 247 } 248 249 /** 250 * Whether heartbeats are enabled. Returns {@code false} if 251 * {@link #setHeartbeat} is set to "0,0", and {@code true} otherwise. 252 */ 253 public boolean isHeartbeatEnabled() { 254 long[] heartbeat = getHeartbeat(); 255 return (heartbeat != null && heartbeat[0] != 0 && heartbeat[1] != 0); 256 } 257 258 /** 259 * Set the session header. 260 * Applies to the CONNECTED frame. 261 */ 262 public void setSession(String session) { 263 set(SESSION, session); 264 } 265 266 /** 267 * Get the session header. 268 */ 269 public String getSession() { 270 return getFirst(SESSION); 271 } 272 273 /** 274 * Set the server header. 275 * Applies to the CONNECTED frame. 276 */ 277 public void setServer(String server) { 278 set(SERVER, server); 279 } 280 281 /** 282 * Get the server header. 283 * Applies to the CONNECTED frame. 284 */ 285 public String getServer() { 286 return getFirst(SERVER); 287 } 288 289 /** 290 * Set the destination header. 291 */ 292 public void setDestination(String destination) { 293 set(DESTINATION, destination); 294 } 295 296 /** 297 * Get the destination header. 298 * Applies to the SEND, SUBSCRIBE, and MESSAGE frames. 299 */ 300 public String getDestination() { 301 return getFirst(DESTINATION); 302 } 303 304 /** 305 * Set the id header. 306 * Applies to the SUBSCR0BE, UNSUBSCRIBE, and ACK or NACK frames. 307 */ 308 public void setId(String id) { 309 set(ID, id); 310 } 311 312 /** 313 * Get the id header. 314 */ 315 public String getId() { 316 return getFirst(ID); 317 } 318 319 /** 320 * Set the ack header to one of "auto", "client", or "client-individual". 321 * Applies to the SUBSCRIBE and MESSAGE frames. 322 */ 323 public void setAck(String ack) { 324 set(ACK, ack); 325 } 326 327 /** 328 * Get the ack header. 329 */ 330 public String getAck() { 331 return getFirst(ACK); 332 } 333 334 /** 335 * Set the login header. 336 * Applies to the MESSAGE frame. 337 */ 338 public void setSubscription(String subscription) { 339 set(SUBSCRIPTION, subscription); 340 } 341 342 /** 343 * Get the subscription header. 344 */ 345 public String getSubscription() { 346 return getFirst(SUBSCRIPTION); 347 } 348 349 /** 350 * Set the message-id header. 351 * Applies to the MESSAGE frame. 352 */ 353 public void setMessageId(String messageId) { 354 set(MESSAGE_ID, messageId); 355 } 356 357 /** 358 * Get the message-id header. 359 */ 360 public String getMessageId() { 361 return getFirst(MESSAGE_ID); 362 } 363 364 /** 365 * Set the receipt-id header. 366 * Applies to the RECEIPT frame. 367 */ 368 public void setReceiptId(String receiptId) { 369 set(RECEIPT_ID, receiptId); 370 } 371 372 /** 373 * Get the receipt header. 374 */ 375 public String getReceiptId() { 376 return getFirst(RECEIPT_ID); 377 } 378 379 /** 380 * Return the first header value for the given header name, if any. 381 * @param headerName the header name 382 * @return the first header value, or {@code null} if none 383 */ 384 @Override 385 public String getFirst(String headerName) { 386 List<String> headerValues = headers.get(headerName); 387 return headerValues != null ? headerValues.get(0) : null; 388 } 389 390 /** 391 * Add the given, single header value under the given name. 392 * @param headerName the header name 393 * @param headerValue the header value 394 * @throws UnsupportedOperationException if adding headers is not supported 395 * @see #put(String, List) 396 * @see #set(String, String) 397 */ 398 @Override 399 public void add(String headerName, String headerValue) { 400 List<String> headerValues = headers.get(headerName); 401 if (headerValues == null) { 402 headerValues = new LinkedList<String>(); 403 this.headers.put(headerName, headerValues); 404 } 405 headerValues.add(headerValue); 406 } 407 408 /** 409 * Set the given, single header value under the given name. 410 * @param headerName the header name 411 * @param headerValue the header value 412 * @throws UnsupportedOperationException if adding headers is not supported 413 * @see #put(String, List) 414 * @see #add(String, String) 415 */ 416 @Override 417 public void set(String headerName, String headerValue) { 418 List<String> headerValues = new LinkedList<String>(); 419 headerValues.add(headerValue); 420 headers.put(headerName, headerValues); 421 } 422 423 @Override 424 public void setAll(Map<String, String> values) { 425 for (Entry<String, String> entry : values.entrySet()) { 426 set(entry.getKey(), entry.getValue()); 427 } 428 } 429 430 @Override 431 public Map<String, String> toSingleValueMap() { 432 LinkedHashMap<String, String> singleValueMap = new LinkedHashMap<String,String>(this.headers.size()); 433 for (Entry<String, List<String>> entry : headers.entrySet()) { 434 singleValueMap.put(entry.getKey(), entry.getValue().get(0)); 435 } 436 return singleValueMap; 437 } 438 439 440 // Map implementation 441 442 @Override 443 public int size() { 444 return this.headers.size(); 445 } 446 447 @Override 448 public boolean isEmpty() { 449 return this.headers.isEmpty(); 450 } 451 452 @Override 453 public boolean containsKey(Object key) { 454 return this.headers.containsKey(key); 455 } 456 457 @Override 458 public boolean containsValue(Object value) { 459 return this.headers.containsValue(value); 460 } 461 462 @Override 463 public List<String> get(Object key) { 464 return this.headers.get(key); 465 } 466 467 @Override 468 public List<String> put(String key, List<String> value) { 469 return this.headers.put(key, value); 470 } 471 472 @Override 473 public List<String> remove(Object key) { 474 return this.headers.remove(key); 475 } 476 477 @Override 478 public void putAll(Map<? extends String, ? extends List<String>> map) { 479 this.headers.putAll(map); 480 } 481 482 @Override 483 public void clear() { 484 this.headers.clear(); 485 } 486 487 @Override 488 public Set<String> keySet() { 489 return this.headers.keySet(); 490 } 491 492 @Override 493 public Collection<List<String>> values() { 494 return this.headers.values(); 495 } 496 497 @Override 498 public Set<Entry<String, List<String>>> entrySet() { 499 return this.headers.entrySet(); 500 } 501 502 503 @Override 504 public boolean equals(Object other) { 505 return (this == other || (other instanceof StompHeaders && 506 this.headers.equals(((StompHeaders) other).headers))); 507 } 508 509 @Override 510 public int hashCode() { 511 return this.headers.hashCode(); 512 } 513 514 @Override 515 public String toString() { 516 return this.headers.toString(); 517 } 518 519 520 /** 521 * Return a {@code StompHeaders} object that can only be read, not written to. 522 */ 523 public static StompHeaders readOnlyStompHeaders(Map<String, List<String>> headers) { 524 return new StompHeaders(headers, true); 525 } 526 527}