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}