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}