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