1 /*
2  * Copyright 2002-2018 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 module hunt.stomp.simp.stomp.StompHeaders;
18 
19 import hunt.collection;
20 import hunt.Exceptions;
21 import hunt.Long;
22 import hunt.Object;
23 import hunt.text.Common;
24 import hunt.text.StringUtils;
25 import hunt.util.AcceptMimeType;
26 import hunt.util.MimeType;
27 import hunt.util.MimeTypeUtils;
28 import hunt.util.ObjectUtils;
29 
30 import std.conv;
31 import std.range;
32 
33 /**
34  * Represents STOMP frame headers.
35  *
36  * <p>In addition to the normal methods defined by {@link Map}, this class offers
37  * the following convenience methods:
38  * <ul>
39  * <li>{@link #getFirst(string)} return the first value for a header name</li>
40  * <li>{@link #add(string, string)} add to the list of values for a header name</li>
41  * <li>{@link #set(string, string)} set a header name to a single string value</li>
42  * </ul>
43  *
44  * @author Rossen Stoyanchev
45  * @since 4.2
46  * @see <a href="http://stomp.github.io/stomp-specification-1.2.html#Frames_and_Headers">
47  * http://stomp.github.io/stomp-specification-1.2.html#Frames_and_Headers</a>
48  */
49 class StompHeaders : MultiValueMap!(string, string) {
50 
51 	// Standard headers (as defined in the spec)
52 
53 	enum string CONTENT_TYPE = "content-type"; // SEND, MESSAGE, ERROR
54 
55 	enum string CONTENT_LENGTH = "content-length"; // SEND, MESSAGE, ERROR
56 
57 	enum string RECEIPT = "receipt"; // any client frame other than CONNECT
58 
59 	// CONNECT
60 
61 	enum string HOST = "host";
62 
63 	enum string ACCEPT_VERSION = "accept-version";
64 
65 	enum string LOGIN = "login";
66 
67 	enum string PASSCODE = "passcode";
68 
69 	enum string HEARTBEAT = "heart-beat";
70 
71 	// CONNECTED
72 
73 	enum string SESSION = "session";
74 
75 	enum string SERVER = "server";
76 
77 	// SEND
78 
79 	enum string DESTINATION = "destination";
80 
81 	// SUBSCRIBE, UNSUBSCRIBE
82 
83 	enum string ID = "id";
84 
85 	enum string ACK = "ack";
86 
87 	// MESSAGE
88 
89 	enum string SUBSCRIPTION = "subscription";
90 
91 	enum string MESSAGE_ID = "message-id";
92 
93 	// RECEIPT
94 
95 	enum string RECEIPT_ID = "receipt-id";
96 
97 
98 	private MultiStringsMap headers;
99 
100 
101 	/**
102 	 * Create a new instance to be populated with new header values.
103 	 */
104 	this() {
105 		this(new LinkedMultiValueMap!(string, string)(4), false);
106 	}
107 
108 	private this(MultiStringsMap headers, bool readOnly) {
109 		assert(headers, "'headers' must not be null");
110 		if (readOnly) {
111 			MultiStringsMap map = new LinkedMultiValueMap!(string, string)(headers.size());
112 			foreach(string key, List!(string) value; map)
113 				map.put(key, value);
114 				// map.put(key, Collections.unmodifiableList(value));
115 			this.headers = map; // Collections.unmodifiableMap(map);
116 		}
117 		else {
118 			this.headers = headers;
119 		}
120 	}
121 
122 
123 	/**
124 	 * Set the content-type header.
125 	 * Applies to the SEND, MESSAGE, and ERROR frames.
126 	 */
127 	void setContentType(MimeType mimeType) {
128 		if (mimeType !is null) {
129 			// assert(!mimeType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'");
130 			// assert(!mimeType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'");
131 			set(CONTENT_TYPE, mimeType.toString());
132 		}
133 		else {
134 			set(CONTENT_TYPE, null);
135 		}
136 	}
137 
138 	/**
139 	 * Return the content-type header value.
140 	 */
141 	
142 	AcceptMimeType[] getContentType() {
143 		string value = getFirst(CONTENT_TYPE);
144 		// AcceptMimeType[] list = MimeTypes.parseAcceptMIMETypes(value);
145 		// return value.empty ? null : MimeTypeUtils.parseMimeType(value);
146 		return value.empty ? null : MimeTypeUtils.parseAcceptMIMETypes(value);
147 	}
148 
149 	/**
150 	 * Set the content-length header.
151 	 * Applies to the SEND, MESSAGE, and ERROR frames.
152 	 */
153 	void setContentLength(long contentLength) {
154 		set(CONTENT_LENGTH, to!string(contentLength));
155 	}
156 
157 	/**
158 	 * Return the content-length header or -1 if unknown.
159 	 */
160 	long getContentLength() {
161 		string value = getFirst(CONTENT_LENGTH);
162 		return (value !is null ? Long.parseLong(value) : -1);
163 	}
164 
165 	/**
166 	 * Set the receipt header.
167 	 * Applies to any client frame other than CONNECT.
168 	 */
169 	void setReceipt(string receipt) {
170 		set(RECEIPT, receipt);
171 	}
172 
173 	/**
174 	 * Get the receipt header.
175 	 */
176 	
177 	string getReceipt() {
178 		return getFirst(RECEIPT);
179 	}
180 
181 	/**
182 	 * Set the host header.
183 	 * Applies to the CONNECT frame.
184 	 */
185 	void setHost(string host) {
186 		set(HOST, host);
187 	}
188 
189 	/**
190 	 * Get the host header.
191 	 */
192 	
193 	string getHost() {
194 		return getFirst(HOST);
195 	}
196 
197 	/**
198 	 * Set the accept-version header. Must be one of "1.1", "1.2", or both.
199 	 * Applies to the CONNECT frame.
200 	 * @since 5.0.7
201 	 */
202 	void setAcceptVersion(string[] acceptVersions...) {
203 		if (acceptVersions.length == 0) {
204 			set(ACCEPT_VERSION, null);
205 			return;
206 		}
207 		foreach(string ver; acceptVersions) {
208 			assert(ver !is null && (ver == ("1.1") || ver == ("1.2")),
209 						"Invalid version: " ~ ver);
210 		}
211 		set(ACCEPT_VERSION, StringUtils.toCommaDelimitedString(acceptVersions.dup));
212 	}
213 
214 	/**
215 	 * Get the accept-version header.
216 	 * @since 5.0.7
217 	 */
218 	
219 	string[] getAcceptVersion() {
220 		string value = getFirst(ACCEPT_VERSION);
221 		return value !is null ? StringUtils.commaDelimitedListToStringArray(value) : null;
222 	}
223 
224 	/**
225 	 * Set the login header.
226 	 * Applies to the CONNECT frame.
227 	 */
228 	void setLogin(string login) {
229 		set(LOGIN, login);
230 	}
231 
232 	/**
233 	 * Get the login header.
234 	 */
235 	
236 	string getLogin() {
237 		return getFirst(LOGIN);
238 	}
239 
240 	/**
241 	 * Set the passcode header.
242 	 * Applies to the CONNECT frame.
243 	 */
244 	void setPasscode(string passcode) {
245 		set(PASSCODE, passcode);
246 	}
247 
248 	/**
249 	 * Get the passcode header.
250 	 */
251 	
252 	string getPasscode() {
253 		return getFirst(PASSCODE);
254 	}
255 
256 	/**
257 	 * Set the heartbeat header.
258 	 * Applies to the CONNECT and CONNECTED frames.
259 	 */
260 	void setHeartbeat(long[] heartbeat) {
261 		if (heartbeat is null || heartbeat.length != 2) {
262 			throw new IllegalArgumentException("Heart-beat array must be of length 2, not " ~
263 					(heartbeat !is null ? to!string(heartbeat.length) : "null"));
264 		}
265 		string value = heartbeat[0].to!string() ~ "," ~ heartbeat[1].to!string();
266 		if (heartbeat[0] < 0 || heartbeat[1] < 0) {
267 			throw new IllegalArgumentException("Heart-beat values cannot be negative: " ~ value);
268 		}
269 		set(HEARTBEAT, value);
270 	}
271 
272 	/**
273 	 * Get the heartbeat header.
274 	 */
275 	
276 	long[] getHeartbeat() {
277 		string rawValue = getFirst(HEARTBEAT);
278 		string[] rawValues = StringUtils.split(rawValue, ",");
279 		if (rawValues is null) {
280 			return null;
281 		}
282 		// return [Long.valueOf(rawValues[0]), Long.valueOf(rawValues[1])];
283 		return [rawValues[0].to!long(), rawValues[1].to!long()];
284 	}
285 
286 	/**
287 	 * Whether heartbeats are enabled. Returns {@code false} if
288 	 * {@link #setHeartbeat} is set to "0,0", and {@code true} otherwise.
289 	 */
290 	bool isHeartbeatEnabled() {
291 		long[] heartbeat = getHeartbeat();
292 		return (heartbeat !is null && heartbeat[0] != 0 && heartbeat[1] != 0);
293 	}
294 
295 	/**
296 	 * Set the session header.
297 	 * Applies to the CONNECTED frame.
298 	 */
299 	void setSession(string session) {
300 		set(SESSION, session);
301 	}
302 
303 	/**
304 	 * Get the session header.
305 	 */
306 	
307 	string getSession() {
308 		return getFirst(SESSION);
309 	}
310 
311 	/**
312 	 * Set the server header.
313 	 * Applies to the CONNECTED frame.
314 	 */
315 	void setServer(string server) {
316 		set(SERVER, server);
317 	}
318 
319 	/**
320 	 * Get the server header.
321 	 * Applies to the CONNECTED frame.
322 	 */
323 	
324 	string getServer() {
325 		return getFirst(SERVER);
326 	}
327 
328 	/**
329 	 * Set the destination header.
330 	 */
331 	void setDestination(string destination) {
332 		set(DESTINATION, destination);
333 	}
334 
335 	/**
336 	 * Get the destination header.
337 	 * Applies to the SEND, SUBSCRIBE, and MESSAGE frames.
338 	 */
339 	
340 	string getDestination() {
341 		return getFirst(DESTINATION);
342 	}
343 
344 	/**
345 	 * Set the id header.
346 	 * Applies to the SUBSCR0BE, UNSUBSCRIBE, and ACK or NACK frames.
347 	 */
348 	void setId(string id) {
349 		set(ID, id);
350 	}
351 
352 	/**
353 	 * Get the id header.
354 	 */
355 	
356 	string getId() {
357 		return getFirst(ID);
358 	}
359 
360 	/**
361 	 * Set the ack header to one of "auto", "client", or "client-individual".
362 	 * Applies to the SUBSCRIBE and MESSAGE frames.
363 	 */
364 	void setAck(string ack) {
365 		set(ACK, ack);
366 	}
367 
368 	/**
369 	 * Get the ack header.
370 	 */
371 	
372 	string getAck() {
373 		return getFirst(ACK);
374 	}
375 
376 	/**
377 	 * Set the login header.
378 	 * Applies to the MESSAGE frame.
379 	 */
380 	void setSubscription(string subscription) {
381 		set(SUBSCRIPTION, subscription);
382 	}
383 
384 	/**
385 	 * Get the subscription header.
386 	 */
387 	
388 	string getSubscription() {
389 		return getFirst(SUBSCRIPTION);
390 	}
391 
392 	/**
393 	 * Set the message-id header.
394 	 * Applies to the MESSAGE frame.
395 	 */
396 	void setMessageId(string messageId) {
397 		set(MESSAGE_ID, messageId);
398 	}
399 
400 	/**
401 	 * Get the message-id header.
402 	 */
403 	
404 	string getMessageId() {
405 		return getFirst(MESSAGE_ID);
406 	}
407 
408 	/**
409 	 * Set the receipt-id header.
410 	 * Applies to the RECEIPT frame.
411 	 */
412 	void setReceiptId(string receiptId) {
413 		set(RECEIPT_ID, receiptId);
414 	}
415 
416 	/**
417 	 * Get the receipt header.
418 	 */
419 	
420 	string getReceiptId() {
421 		return getFirst(RECEIPT_ID);
422 	}
423 
424 	/**
425 	 * Return the first header value for the given header name, if any.
426 	 * @param headerName the header name
427 	 * @return the first header value, or {@code null} if none
428 	 */
429 	override
430 	
431 	string getFirst(string headerName) {
432 		List!(string) headerValues = this.headers.get(headerName);
433 		return headerValues !is null ? headerValues.get(0) : null;
434 	}
435 
436 	/**
437 	 * Add the given, single header value under the given name.
438 	 * @param headerName the header name
439 	 * @param headerValue the header value
440 	 * @throws UnsupportedOperationException if adding headers is not supported
441 	 * @see #put(string, List)
442 	 * @see #set(string, string)
443 	 */
444 	override
445 	void add(string headerName, string headerValue) {
446 		List!(string) headerValues = this.headers.computeIfAbsent(headerName, k => new LinkedList!string());
447 		headerValues.add(headerValue);
448 	}
449 
450 	override
451 	void addAll(string headerName, List!(string) headerValues) {
452 		List!(string) currentValues = this.headers.computeIfAbsent(headerName, k => new LinkedList!string());
453 		currentValues.addAll(headerValues);
454 	}
455 
456 	// override
457 	void addAll(MultiStringsMap values) {
458 		foreach(string k, List!(string) v; values) 
459 			this.addAll(k, v);
460 	}
461 
462 	/**
463 	 * Set the given, single header value under the given name.
464 	 * @param headerName the header name
465 	 * @param headerValue the header value
466 	 * @throws UnsupportedOperationException if adding headers is not supported
467 	 * @see #put(string, List)
468 	 * @see #add(string, string)
469 	 */
470 	override
471 	void set(string headerName, string headerValue) {
472 		List!(string) headerValues = new LinkedList!(string)();
473 		headerValues.add(headerValue);
474 		this.headers.put(headerName, headerValues);
475 	}
476 
477 	override
478 	void setAll(Map!(string, string) values) {
479 		foreach(string k, string v; values) 
480 			this.set(k, v);
481 	}
482 
483 	override
484 	Map!(string, string) toSingleValueMap() {
485 		LinkedHashMap!(string, string) singleValueMap = new LinkedHashMap!(string, string)(this.headers.size());
486 		foreach(string key, List!(string) value; this.headers) {
487 			singleValueMap.put(key, value.get(0));
488 		}
489 		return singleValueMap;
490 	}
491 
492 
493 	// Map implementation
494 
495 	override
496 	int size() {
497 		return this.headers.size();
498 	}
499 
500 	override
501 	bool isEmpty() {
502 		return this.headers.isEmpty();
503 	}
504 
505 	override
506 	bool containsKey(string key) {
507 		return this.headers.containsKey(key);
508 	}
509 
510 	override
511 	bool containsValue(List!(string) value) {
512 		return this.headers.containsValue(value);
513 	}
514 
515     List!(string) opIndex(string key) {
516         return get(key);
517     }
518 
519 	override
520 	List!(string) get(string key) {
521 		return this.headers.get(key);
522 	}
523 
524 	override
525 	List!(string) put(string key, List!(string) value) {
526 		return this.headers.put(key, value);
527 	}
528 
529 	override
530 	void putAll(MultiStringsMap map) {
531 		this.headers.putAll(map);
532 	}
533 
534     List!(string) putIfAbsent(string key, List!(string) value) {
535         List!(string) v;
536 
537         if(!containsKey(key))
538             v = put(key, value);
539 
540         return v;
541     }
542 
543 	override
544 	List!(string) remove(string key) {
545 		return this.headers.remove(key);
546 	}
547 	
548     bool remove(string key, List!(string) value){
549         List!(string) curValue = get(key);
550         if(curValue != value || !containsKey(key))
551             return false;
552         remove(key);
553         return true;
554     }
555 
556 	override
557 	void clear() {
558 		this.headers.clear();
559 	}
560 
561 	
562     bool replace(string key, List!(string) oldValue, List!(string) newValue) {
563         List!(string) curValue = get(key);
564          if(curValue != oldValue || !containsKey(key)){
565             return false;
566         }
567         put(key, newValue);
568         return true;
569     }
570 
571     List!(string) replace(string key, List!(string) value) {
572         List!(string) curValue;
573         if (containsKey(key)) {
574             curValue = put(key, value);
575         }
576         return curValue;
577     }
578 
579 	// override
580 	// Set!(string) keySet() {
581 	// 	return this.headers.keySet();
582 	// }
583 
584 
585 	List!(string)[] values() {
586         List!(string)[] arr;
587         foreach(List!(string) value; byValue()) {
588             arr ~= value;
589         }
590         return arr;
591     }
592 
593 	// override
594 	// Set!(Entry!(string, List!(string))) entrySet() {
595 	// 	return this.headers.entrySet();
596 	// }
597 
598 
599     int opApply(scope int delegate(ref string, ref List!(string)) dg)  {
600         return this.headers.opApply(dg);
601     }
602     
603     int opApply(scope int delegate(MapEntry!(string, List!(string)) entry) dg) {
604         return this.headers.opApply(dg);
605     }
606     
607     InputRange!string byKey() {
608         return this.headers.byKey();
609     }
610 
611     InputRange!(List!(string)) byValue() {
612         return this.headers.byValue();
613     }
614 
615     bool opEquals(IObject o) {
616         return opEquals(cast(Object)o);
617     }
618 
619 	override
620 	bool opEquals(Object other) {
621 		if(this is other)
622 			return true;
623 		StompHeaders sh = cast(StompHeaders) other;
624 		if(sh is null)	return false;
625 		return this.headers == sh.headers;
626 	}
627 
628 	override
629 	size_t toHash() @trusted nothrow {
630 		return this.headers.toHash();
631 	}
632 
633 	override
634 	string toString() {
635 		return this.headers.toString();
636 	}
637 
638 
639 	/**
640 	 * Return a {@code StompHeaders} object that can only be read, not written to.
641 	 */
642 	static StompHeaders readOnlyStompHeaders(MultiStringsMap headers) {
643 		return new StompHeaders((headers !is null ? headers : Collections.emptyMap!(string, List!(string))()), true);
644 	}
645 
646     mixin CloneMemberTemplate!(typeof(this));	
647 
648 }