1 module hunt.http.HttpFields;
2 
3 import hunt.http.HttpField;
4 import hunt.http.HttpHeader;
5 import hunt.http.QuotedCSV;
6 
7 import hunt.collection;
8 import hunt.Exceptions;
9 import hunt.logging;
10 import hunt.text.Common;
11 import hunt.text.QuotedStringTokenizer;
12 import hunt.util.StringBuilder;
13 import hunt.text.StringTokenizer;
14 import hunt.text.StringUtils;
15 import hunt.util.Common;
16 
17 import std.array;
18 import std.algorithm;
19 import std.container.array;
20 import std.conv;
21 import std.datetime;
22 import std.string;
23 import std.range;
24 
25 
26 /**
27  * HTTP Fields. A collection of HTTP header and or Trailer fields.
28  *
29  * <p>
30  * This class is not synchronized as it is expected that modifications will only
31  * be performed by a single thread.
32  * 
33  * <p>
34  * The cookie handling provided by this class is guided by the Servlet
35  * specification and RFC6265.
36  *
37  */
38 class HttpFields : Iterable!HttpField {
39 	// static string __separators = ", \t";
40 
41 	private HttpField[] _fields;
42 	private int _size;
43 
44 	/**
45 	 * Initialize an empty HttpFields.
46 	 */
47 	this() {
48 		_fields = new HttpField[20];
49 	}
50 
51 	/**
52 	 * Initialize an empty HttpFields.
53 	 * 
54 	 * @param capacity
55 	 *            the capacity of the http fields
56 	 */
57 	this(int capacity) {
58 		_fields = new HttpField[capacity];
59 	}
60 
61 	/**
62 	 * Initialize HttpFields from copy.
63 	 * 
64 	 * @param fields
65 	 *            the fields to copy data from
66 	 */
67 	this(HttpFields fields) {
68 		_fields = fields._fields.dup ~ new HttpField[10];
69 		_size = fields.size();
70 	}
71 
72 	int size() {
73 		return _size;
74 	}
75 
76 	InputRange!HttpField iterator() {
77 		return inputRangeObject(_fields[0 .. _size]);
78 	}
79 
80 	int opApply(scope int delegate(ref HttpField) dg) {
81 		int result = 0;
82 		foreach (HttpField v; _fields[0 .. _size]) {
83 			result = dg(v);
84 			if (result != 0)
85 				return result;
86 		}
87 		return result;
88 	}
89 
90 	int opApply(scope int delegate(string name, string value) dg) {
91 		int result = 0;
92 		foreach (HttpField v; _fields[0 .. _size]) {
93 			result = dg(v.getName(), v.getValue());
94 			if (result != 0)
95 				return result;
96 		}
97 		return result;
98 	}
99 
100 	/**
101 	 * Get Collection of header names.
102 	 * 
103 	 * @return the unique set of field names.
104 	 */
105 	Set!string getFieldNamesCollection() {
106 		Set!string set = new HashSet!string(_size);
107 		foreach (HttpField f; _fields[0 .. _size]) {
108 			if (f !is null)
109 				set.add(f.getName());
110 		}
111 		return set;
112 	}
113 
114 	/**
115 	 * Get enumeration of header _names. Returns an enumeration of strings
116 	 * representing the header _names for this request.
117 	 * 
118 	 * @return an enumeration of field names
119 	 */
120 	InputRange!string getFieldNames() {
121 		bool[string] set;
122 		foreach (HttpField f; _fields[0 .. _size]) {
123 			if (f !is null)
124 				set[f.getName()] = true;
125 		}
126 		return inputRangeObject(set.keys);
127 	}
128 
129 	// InputRange!string getFieldNames() {
130 	// 	// return Collections.enumeration(getFieldNamesCollection());
131 	// 	// return getFieldNamesCollection().toArray();
132 	// 	Array!string set;
133 	// 	foreach (HttpField f ; _fields[0.._size]) {
134 	// 		if (f !is null)
135 	// 			set.insertBack(f.getName());
136 	// 	}
137 	// 	// Enumeration!string r = new RangeEnumeration!string(inputRangeObject(set[].array));
138 	// 	return inputRangeObject(set[].array);
139 	// }
140 
141 	HttpField[] allFields() {
142 		return _fields[0 .. _size];
143 	}
144 
145 	/**
146 	 * Get a Field by index.
147 	 * 
148 	 * @param index
149 	 *            the field index
150 	 * @return A Field value or null if the Field value has not been set
151 	 */
152 	HttpField getField(int index) {
153 		if (index >= _size)
154 			throw new NoSuchElementException("");
155 		return _fields[index];
156 	}
157 
158 	HttpField getField(HttpHeader header) {
159 		for (int i = 0; i < _size; i++) {
160 			HttpField f = _fields[i];
161 			if (f.getHeader() == header)
162 				return f;
163 		}
164 		return null;
165 	}
166 
167 	HttpField getField(string name) {
168 		for (int i = 0; i < _size; i++) {
169 			HttpField f = _fields[i];
170 			if (f.getName().equalsIgnoreCase(name))
171 				return f;
172 		}
173 		return null;
174 	}
175 
176 	bool contains(HttpField field) {
177 		for (int i = _size; i-- > 0;) {
178 			HttpField f = _fields[i];
179 			if (f.isSameName(field) && (f.opEquals(field) || f.contains(field.getValue())))
180 				return true;
181 		}
182 		return false;
183 	}
184 
185 	bool contains(HttpHeader header, string value) {
186 		for (int i = _size; i-- > 0;) {
187 			HttpField f = _fields[i];
188 			if (f.getHeader() == header && f.contains(value))
189 				return true;
190 		}
191 		return false;
192 	}
193 
194 	bool contains(string name, string value) {
195 		for (int i = _size; i-- > 0;) {
196 			HttpField f = _fields[i];
197 			if (f.getName().equalsIgnoreCase(name) && f.contains(value))
198 				return true;
199 		}
200 		return false;
201 	}
202 
203 	bool contains(HttpHeader header) {
204 		for (int i = _size; i-- > 0;) {
205 			HttpField f = _fields[i];
206 			if (f.getHeader() == header)
207 				return true;
208 		}
209 		return false;
210 	}
211 
212 	bool containsKey(string name) {
213 		for (int i = _size; i-- > 0;) {
214 			HttpField f = _fields[i];
215 			if (std..string.icmp(f.getName(), name) == 0)
216 				return true;
217 		}
218 		return false;
219 	}
220 
221 	string get(HttpHeader header) {
222 		for (int i = 0; i < _size; i++) {
223 			HttpField f = _fields[i];
224 			if (f.getHeader() == header)
225 				return f.getValue();
226 		}
227 		return null;
228 	}
229 
230 	string get(string header) {
231 		for (int i = 0; i < _size; i++) {
232 			HttpField f = _fields[i];
233 			if (f.getName().equalsIgnoreCase(header))
234 				return f.getValue();
235 		}
236 		return null;
237 	}
238 
239 	/**
240 	 * Get multiple header of the same name
241 	 *
242 	 * @return List the values
243 	 * @param header
244 	 *            the header
245 	 */
246 	string[] getValuesList(HttpHeader header) {
247 		Array!(string) list;
248 		foreach (HttpField f; this) {
249 			if (f.getHeader() == header)
250 				list.insertBack(f.getValue());
251 		}
252 		return list.array();
253 	}
254 
255 	/**
256 	 * Get multiple header of the same name
257 	 * 
258 	 * @return List the header values
259 	 * @param name
260 	 *            the case-insensitive field name
261 	 */
262 	string[] getValuesList(string name) {
263 		Array!(string) list;
264 		foreach (HttpField f; this) {
265 			if (f.getName().equalsIgnoreCase(name))
266 				list.insertBack(f.getValue());
267 		}
268 		return list.array();
269 	}
270 
271 	/**
272 	 * Add comma separated values, but only if not already present.
273 	 * 
274 	 * @param header
275 	 *            The header to add the value(s) to
276 	 * @param values
277 	 *            The value(s) to add
278 	 * @return True if headers were modified
279 	 */
280 	// bool addCSV(HttpHeader header, string... values) {
281 	// 	QuotedCSV existing = null;
282 	// 	for (HttpField f : this) {
283 	// 		if (f.getHeader() == header) {
284 	// 			if (existing == null)
285 	// 				existing = new QuotedCSV(false);
286 	// 			existing.addValue(f.getValue());
287 	// 		}
288 	// 	}
289 
290 	// 	string value = addCSV(existing, values);
291 	// 	if (value != null) {
292 	// 		add(header, value);
293 	// 		return true;
294 	// 	}
295 	// 	return false;
296 	// }
297 
298 	/**
299 	 * Add comma separated values, but only if not already present.
300 	 * 
301 	 * @param name
302 	 *            The header to add the value(s) to
303 	 * @param values
304 	 *            The value(s) to add
305 	 * @return True if headers were modified
306 	 */
307 	bool addCSV(string name, string[] values...) {
308 		QuotedCSV existing = null;
309 		foreach (HttpField f; this) {
310 			if (f.getName().equalsIgnoreCase(name)) {
311 				if (existing is null)
312 					existing = new QuotedCSV(false);
313 				existing.addValue(f.getValue());
314 			}
315 		}
316 		string value = addCSV(existing, values);
317 		if (value != null) {
318 			add(name, value);
319 			return true;
320 		}
321 		return false;
322 	}
323 
324 	protected string addCSV(QuotedCSV existing, string[] values...) {
325 		// remove any existing values from the new values
326 		bool add = true;
327 		if (existing !is null && !existing.isEmpty()) {
328 			add = false;
329 
330 			for (size_t i = values.length; i-- > 0;) {
331 				string unquoted = QuotedCSV.unquote(values[i]);
332 				if (existing.getValues().contains(unquoted))
333 					values[i] = null;
334 				else
335 					add = true;
336 			}
337 		}
338 
339 		if (add) {
340 			StringBuilder value = new StringBuilder();
341 			foreach (string v; values) {
342 				if (v == null)
343 					continue;
344 				if (value.length > 0)
345 					value.append(", ");
346 				value.append(v);
347 			}
348 			if (value.length > 0)
349 				return value.toString();
350 		}
351 
352 		return null;
353 	}
354 
355 	/**
356 	 * Get multiple field values of the same name, split as a {@link QuotedCSV}
357 	 *
358 	 * @return List the values with OWS stripped
359 	 * @param header
360 	 *            The header
361 	 * @param keepQuotes
362 	 *            True if the fields are kept quoted
363 	 */
364 	string[] getCSV(HttpHeader header, bool keepQuotes) {
365 		QuotedCSV values = null;
366 		foreach (HttpField f; _fields[0 .. _size]) {
367 			if (f.getHeader() == header) {
368 				if (values is null)
369 					values = new QuotedCSV(keepQuotes);
370 				values.addValue(f.getValue());
371 			}
372 		}
373 		// Array!string ar = values.getValues();
374 		// return inputRangeObject(values.getValues()[].array);
375 
376 		return values is null ? cast(string[]) null : values.getValues().array;
377 	}
378 
379 	/**
380 	 * Get multiple field values of the same name as a {@link QuotedCSV}
381 	 *
382 	 * @return List the values with OWS stripped
383 	 * @param name
384 	 *            the case-insensitive field name
385 	 * @param keepQuotes
386 	 *            True if the fields are kept quoted
387 	 */
388 	List!string getCSV(string name, bool keepQuotes) {
389 		QuotedCSV values = null;
390 		foreach (HttpField f; _fields[0 .. _size]) {
391 			if (f.getName().equalsIgnoreCase(name)) {
392 				if (values is null)
393 					values = new QuotedCSV(keepQuotes);
394 				values.addValue(f.getValue());
395 			}
396 		}
397 		return values is null ? null : new ArrayList!string(values.getValues().array);
398 		// return inputRangeObject(values.getValues()[].array);
399 	}
400 
401 	string[] getCsvAsArray(string name, bool keepQuotes) {
402 		QuotedCSV values = null;
403 		foreach (HttpField f; _fields[0 .. _size]) {
404 			if (f.getName().equalsIgnoreCase(name)) {
405 				if (values is null)
406 					values = new QuotedCSV(keepQuotes);
407 				values.addValue(f.getValue());
408 			}
409 		}
410 		return values is null ? null : values.getValues().array;
411 		// return inputRangeObject(values.getValues()[].array);
412 	}
413 
414 	/**
415 	 * Get multiple field values of the same name, split and sorted as a
416 	 * {@link QuotedQualityCSV}
417 	 *
418 	 * @return List the values in quality order with the q param and OWS
419 	 *         stripped
420 	 * @param header
421 	 *            The header
422 	 */
423 	// List!string getQualityCSV(HttpHeader header) {
424 	// 	QuotedQualityCSV values = null;
425 	// 	for (HttpField f : this) {
426 	// 		if (f.getHeader() == header) {
427 	// 			if (values == null)
428 	// 				values = new QuotedQualityCSV();
429 	// 			values.addValue(f.getValue());
430 	// 		}
431 	// 	}
432 
433 	// 	return values == null ? Collections.emptyList() : values.getValues();
434 	// }
435 
436 	/**
437 	 * Get multiple field values of the same name, split and sorted as a
438 	 * {@link QuotedQualityCSV}
439 	 *
440 	 * @return List the values in quality order with the q param and OWS
441 	 *         stripped
442 	 * @param name
443 	 *            the case-insensitive field name
444 	 */
445 	// List!string getQualityCSV(string name) {
446 	// 	QuotedQualityCSV values = null;
447 	// 	for (HttpField f : this) {
448 	// 		if (f.getName().equalsIgnoreCase(name)) {
449 	// 			if (values == null)
450 	// 				values = new QuotedQualityCSV();
451 	// 			values.addValue(f.getValue());
452 	// 		}
453 	// 	}
454 	// 	return values == null ? Collections.emptyList() : values.getValues();
455 	// }
456 
457 	/**
458 	 * Get multi headers
459 	 *
460 	 * @return Enumeration of the values
461 	 * @param name
462 	 *            the case-insensitive field name
463 	 */
464 	InputRange!string getValues(string name) {
465 		Array!string r;
466 
467 		for (int i = 0; i < _size; i++) {
468 			HttpField f = _fields[i];
469 			if (f.getName().equalsIgnoreCase(name)) {
470 				string v = f.getValue();
471 				if (!v.empty)
472 					r.insertBack(v);
473 			}
474 		}
475 
476 		return inputRangeObject(r[].array);
477 	}
478 
479 	void put(HttpField field) {
480 		bool put = false;
481 		for (int i = _size; i-- > 0;) {
482 			HttpField f = _fields[i];
483 			if (f.isSameName(field)) {
484 				if (put) {
485 					--_size;
486 					_fields[i + 1 .. _size + 1] = _fields[i .. _size];
487 				} else {
488 					_fields[i] = field;
489 					put = true;
490 				}
491 			}
492 		}
493 		if (!put)
494 			add(field);
495 	}
496 
497 	/**
498 	 * Set a field.
499 	 *
500 	 * @param name
501 	 *            the name of the field
502 	 * @param value
503 	 *            the value of the field. If null the field is cleared.
504 	 */
505 	void put(string name, string value) {
506 		if (value == null)
507 			remove(name);
508 		else
509 			put(new HttpField(name, value));
510 	}
511 
512 	void put(HttpHeader header, HttpHeaderValue value) {
513 		put(header, value.toString());
514 	}
515 
516 	/**
517 	 * Set a field.
518 	 *
519 	 * @param header
520 	 *            the header name of the field
521 	 * @param value
522 	 *            the value of the field. If null the field is cleared.
523 	 */
524 	void put(HttpHeader header, string value) {
525 		if (value == null)
526 			remove(header);
527 		else
528 			put(new HttpField(header, value));
529 	}
530 
531 	/**
532 	 * Set a field.
533 	 *
534 	 * @param name
535 	 *            the name of the field
536 	 * @param list
537 	 *            the List value of the field. If null the field is cleared.
538 	 */
539 	void put(string name, List!string list) {
540 		remove(name);
541 		foreach (string v; list)
542 			if (!v.empty)
543 				add(name, v);
544 	}
545 
546 	/**
547 	 * Add to or set a field. If the field is allowed to have multiple values,
548 	 * add will add multiple headers of the same name.
549 	 *
550 	 * @param name
551 	 *            the name of the field
552 	 * @param value
553 	 *            the value of the field.
554 	 */
555 	void add(string name, string value) {
556 		HttpField field = new HttpField(name, value);
557 		add(field);
558 	}
559 
560 	void add(HttpHeader header, HttpHeaderValue value) {
561 		add(header, value.toString());
562 	}
563 
564 	/**
565 	 * Add to or set a field. If the field is allowed to have multiple values,
566 	 * add will add multiple headers of the same name.
567 	 *
568 	 * @param header
569 	 *            the header
570 	 * @param value
571 	 *            the value of the field.
572 	 */
573 	void add(HttpHeader header, string value) {
574 		if (value.empty)
575 			throw new IllegalArgumentException("The value is null for header: " ~ header.toString());
576 
577 		HttpField field = new HttpField(header, value);
578 		add(field);
579 	}
580 
581 	/**
582 	 * Remove a field.
583 	 *
584 	 * @param name
585 	 *            the field to remove
586 	 * @return the header that was removed
587 	 */
588 	HttpField remove(HttpHeader name) {
589 
590 		HttpField removed = null;
591 		for (int i = _size; i-- > 0;) {
592 			HttpField f = _fields[i];
593 			if (f.getHeader() == name) {
594 				removed = f;
595 				--_size;
596 				for (int j = i; j < size; j++)
597 					_fields[j] = _fields[j + 1];
598 			}
599 		}
600 		return removed;
601 	}
602 
603 	/**
604 	 * Remove a field.
605 	 *
606 	 * @param name
607 	 *            the field to remove
608 	 * @return the header that was removed
609 	 */
610 	HttpField remove(string name) {
611 		HttpField removed = null;
612 		for (int i = _size; i-- > 0;) {
613 			HttpField f = _fields[i];
614 			if (icmp(f.getName(), name) == 0) {
615 				removed = f;
616 				--_size;
617 				for (int j = i; j < size; j++)
618 					_fields[j] = _fields[j + 1];
619 			}
620 		}
621 		return removed;
622 	}
623 
624 	/**
625 	 * Get a header as an long value. Returns the value of an integer field or
626 	 * -1 if not found. The case of the field name is ignored.
627 	 *
628 	 * @param name
629 	 *            the case-insensitive field name
630 	 * @return the value of the field as a long
631 	 * @exception NumberFormatException
632 	 *                If bad long found
633 	 */
634 	long getLongField(string name) {
635 		HttpField field = getField(name);
636 		return field is null ? -1L : field.getLongValue();
637 	}
638 
639 	/**
640 	 * Get a header as a date value. Returns the value of a date field, or -1 if
641 	 * not found. The case of the field name is ignored.
642 	 *
643 	 * @param name
644 	 *            the case-insensitive field name
645 	 * @return the value of the field as a number of milliseconds since unix
646 	 *         epoch
647 	 */
648 	long getDateField(string name) {
649 		HttpField field = getField(name);
650 		if (field is null)
651 			return -1;
652 
653 		string val = valueParameters(field.getValue(), null);
654 		if (val.empty)
655 			return -1;
656 
657 		// TODO: Tasks pending completion -@zxp at 6/21/2018, 10:59:24 AM
658 		// 
659 		long date = SysTime.fromISOExtString(val).stdTime(); // DateParser.parseDate(val);
660 		if (date == -1)
661 			throw new IllegalArgumentException("Cannot convert date: " ~ val);
662 		return date;
663 	}
664 
665 	/**
666 	 * Sets the value of an long field.
667 	 *
668 	 * @param name
669 	 *            the field name
670 	 * @param value
671 	 *            the field long value
672 	 */
673 	void putLongField(HttpHeader name, long value) {
674 		string v = to!string(value);
675 		put(name, v);
676 	}
677 
678 	/**
679 	 * Sets the value of an long field.
680 	 *
681 	 * @param name
682 	 *            the field name
683 	 * @param value
684 	 *            the field long value
685 	 */
686 	void putLongField(string name, long value) {
687 		string v = to!string(value);
688 		put(name, v);
689 	}
690 
691 	/**
692 	 * Sets the value of a date field.
693 	 *
694 	 * @param name
695 	 *            the field name
696 	 * @param date
697 	 *            the field date value
698 	 */
699 	void putDateField(HttpHeader name, long date) {
700 		// TODO: Tasks pending completion -@zxp at 6/21/2018, 10:42:44 AM
701 		// 
702 		// string d = DateGenerator.formatDate(date);
703 		string d = SysTime(date).toISOExtString();
704 		put(name, d);
705 	}
706 
707 	/**
708 	 * Sets the value of a date field.
709 	 *
710 	 * @param name
711 	 *            the field name
712 	 * @param date
713 	 *            the field date value
714 	 */
715 	void putDateField(string name, long date) {
716 		// TODO: Tasks pending completion -@zxp at 6/21/2018, 11:04:46 AM
717 		// 
718 		// string d = DateGenerator.formatDate(date);		
719 		string d = SysTime(date).toISOExtString();
720 		put(name, d);
721 	}
722 
723 	/**
724 	 * Sets the value of a date field.
725 	 *
726 	 * @param name
727 	 *            the field name
728 	 * @param date
729 	 *            the field date value
730 	 */
731 	void addDateField(string name, long date) {
732 		// string d = DateGenerator.formatDate(date);
733 		string d = SysTime(date).toISOExtString();
734 		add(name, d);
735 	}
736 
737 	override size_t toHash() @trusted nothrow {
738 		int hash = 0;
739 		foreach (HttpField field; _fields[0 .. _size])
740 			hash += field.toHash();
741 		return hash;
742 	}
743 
744 	override bool opEquals(Object o) {
745 		if (o is this)
746 			return true;
747 
748 		if (!object.opEquals(this, o))
749 			return false;
750 		HttpFields that = cast(HttpFields) o;
751 		if (that is null)
752 			return false;
753 
754 		// Order is not important, so we cannot rely on List.equals().
755 		if (size() != that.size())
756 			return false;
757 
758 		foreach (HttpField fi; this) {
759 			bool isContinue = false;
760 			foreach (HttpField fa; that) {
761 				if (fi == fa) {
762 					isContinue = true;
763 					break;
764 				}
765 			}
766 			if (!isContinue)
767 				return false;
768 		}
769 		return true;
770 	}
771 
772 	override string toString() {
773 		try {
774 			StringBuilder buffer = new StringBuilder();
775 			foreach (HttpField field; this) {
776 				if (field !is null) {
777 					string tmp = field.getName();
778 					if (tmp != null)
779 						buffer.append(tmp);
780 					buffer.append(": ");
781 					tmp = field.getValue();
782 					if (tmp != null)
783 						buffer.append(tmp);
784 					buffer.append("\r\n");
785 				}
786 			}
787 			buffer.append("\r\n");
788 			return buffer.toString();
789 		} catch (Exception e) {
790 			warningf("http fields toString exception", e);
791 			return e.toString();
792 		}
793 	}
794 
795 	void clear() {
796 		_size = 0;
797 	}
798 
799 	void add(HttpField field) {
800 		version(HUNT_HTTP_DEBUG) {
801 			// infof("header: %s", field.toString());
802 		}
803 
804 		if (field !is null) {
805 			if (_size == _fields.length)
806 				_fields = _fields.dup ~ new HttpField[_size];
807 			_fields[_size++] = field;
808 		}
809 	}
810 
811 	void addAll(HttpFields fields) {
812 		for (int i = 0; i < fields._size; i++)
813 			add(fields._fields[i]);
814 	}
815 
816 	/**
817 	 * Add fields from another HttpFields instance. Single valued fields are
818 	 * replaced, while all others are added.
819 	 *
820 	 * @param fields
821 	 *            the fields to add
822 	 */
823 	void add(HttpFields fields) {
824 		if (fields is null)
825 			return;
826 
827 		// Enumeration<string> e = fields.getFieldNames();
828 		// while (e.hasMoreElements()) {
829 		// 	string name = e.nextElement();
830 		// 	Enumeration<string> values = fields.getValues(name);
831 		// 	while (values.hasMoreElements())
832 		// 		add(name, values.nextElement());
833 		// }
834 
835 		auto fieldNames = fields.getFieldNames();
836 		foreach (string n; fieldNames) {
837 			auto values = fields.getValues(n);
838 			foreach (string v; values)
839 				add(n, v);
840 		}
841 	}
842 
843 	/**
844 	 * Get field value without parameters. Some field values can have
845 	 * parameters. This method separates the value from the parameters and
846 	 * optionally populates a map with the parameters. For example:
847 	 *
848 	 * <PRE>
849 	 *
850 	 * FieldName : Value ; param1=val1 ; param2=val2
851 	 *
852 	 * </PRE>
853 	 *
854 	 * @param value
855 	 *            The Field value, possibly with parameters.
856 	 * @return The value.
857 	 */
858 	static string stripParameters(string value) {
859 		if (value == null)
860 			return null;
861 
862 		int i = cast(int) value.indexOf(';');
863 		if (i < 0)
864 			return value;
865 		return value.substring(0, i).strip();
866 	}
867 
868 	/**
869 	 * Get field value parameters. Some field values can have parameters. This
870 	 * method separates the value from the parameters and optionally populates a
871 	 * map with the parameters. For example:
872 	 *
873 	 * <PRE>
874 	 *
875 	 * FieldName : Value ; param1=val1 ; param2=val2
876 	 *
877 	 * </PRE>
878 	 *
879 	 * @param value
880 	 *            The Field value, possibly with parameters.
881 	 * @param parameters
882 	 *            A map to populate with the parameters, or null
883 	 * @return The value.
884 	 */
885 	static string valueParameters(string value, Map!(string, string) parameters) {
886 		if (value is null)
887 			return null;
888 
889 		int i = cast(int) value.indexOf(';');
890 		if (i < 0)
891 			return value;
892 		if (parameters is null)
893 			return value.substring(0, i).strip();
894 
895 		StringTokenizer tok1 = new QuotedStringTokenizer(value.substring(i), ";", false, true);
896 		while (tok1.hasMoreTokens()) {
897 			string token = tok1.nextToken();
898 			StringTokenizer tok2 = new QuotedStringTokenizer(token, "= ");
899 			if (tok2.hasMoreTokens()) {
900 				string paramName = tok2.nextToken();
901 				string paramVal = null;
902 				if (tok2.hasMoreTokens())
903 					paramVal = tok2.nextToken();
904 				parameters.put(paramName, paramVal);
905 			}
906 		}
907 
908 		return value.substring(0, i).strip();
909 	}
910 }