View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau.urlencoding;
18  
19  import static org.apache.juneau.TestUtils.*;
20  import static org.junit.jupiter.api.Assertions.*;
21  
22  import java.util.*;
23  
24  import org.apache.juneau.*;
25  import org.apache.juneau.collections.*;
26  import org.apache.juneau.parser.*;
27  import org.junit.jupiter.api.*;
28  
29  @SuppressWarnings("rawtypes")
30  class UrlEncodingParser_Test extends TestBase {
31  
32  	static UrlEncodingParser p = UrlEncodingParser.DEFAULT;
33  	static BeanSession bs = p.getBeanContext().getSession();
34  
35  	//====================================================================================================
36  	// Basic test
37  	//====================================================================================================
38  	@Test void a01_basic() throws Exception {
39  
40  		// Simple string
41  		// Top level
42  		var t = "_value=a";
43  		assertEquals("a", p.parse(t, Object.class));
44  		assertEquals("a", p.parse(t, String.class));
45  		t = "_value='a'";
46  		assertEquals("a", p.parse(t, String.class));
47  		assertEquals("a", p.parse(t, Object.class));
48  		t = "_value= 'a' ";
49  		assertEquals("a", p.parse(t, String.class));
50  
51  		// 2nd level
52  		t = "?a=a";
53  		assertEquals("a", p.parse(t, Map.class).get("a"));
54  
55  		// Simple map
56  		// Top level
57  		t = "?a=b&c=123&d=false&e=true&f=null";
58  		var m = p.parse(t, Map.class);
59  		assertEquals("b", m.get("a"));
60  		assertTrue(m.get("c") instanceof Number);
61  		assertEquals(123, m.get("c"));
62  		assertTrue(m.get("d") instanceof Boolean);
63  		assertEquals(Boolean.FALSE, m.get("d"));
64  		assertTrue(m.get("e") instanceof Boolean);
65  		assertEquals(Boolean.TRUE, m.get("e"));
66  		assertNull(m.get("f"));
67  
68  		t = "?a=true";
69  		m = p.parse(t, HashMap.class, String.class, Boolean.class);
70  		assertTrue(m.get("a") instanceof Boolean);
71  		assertEquals("true", m.get("a").toString());
72  
73  		// null
74  		// Top level
75  		t = "_value=null";
76  		assertNull(p.parse(t, Object.class));
77  
78  		// 2nd level
79  		t = "?null=null";
80  		m = p.parse(t, Map.class);
81  		assertTrue(m.containsKey(null));
82  		assertNull(m.get(null));
83  
84  		t = "?null=null";
85  		m = p.parse(t, Map.class);
86  		assertTrue(m.containsKey(null));
87  		assertNull(m.get(null));
88  
89  		// 3rd level
90  		t = "?null=(null=null)";
91  		m = p.parse(t, Map.class);
92  		assertTrue(((Map)m.get(null)).containsKey(null));
93  		assertNull(((Map)m.get(null)).get(null));
94  
95  		// Empty array
96  
97  		// 2nd level in map
98  		t = "?x=@()";
99  		m = p.parse(t, HashMap.class, String.class, List.class);
100 		assertTrue(m.containsKey("x"));
101 		assertTrue(((List)m.get("x")).isEmpty());
102 		m = (Map)p.parse(t, Object.class);
103 		assertTrue(m.containsKey("x"));
104 		assertTrue(((List)m.get("x")).isEmpty());
105 		t = "?x=@()";
106 		m = p.parse(t, HashMap.class, String.class, List.class);
107 		assertTrue(m.containsKey("x"));
108 		assertTrue(((List)m.get("x")).isEmpty());
109 
110 		// Empty 2 dimensional array
111 		t = "_value=@(@())";
112 		var l = (List)p.parse(t, Object.class);
113 		assertEquals(1, l.size());
114 		l = (List)l.get(0);
115 		assertTrue(l.isEmpty());
116 		t = "0=@()";
117 		l = p.parse(t, LinkedList.class, List.class);
118 		assertEquals(1, l.size());
119 		l = (List)l.get(0);
120 		assertTrue(l.isEmpty());
121 
122 		// Array containing empty string
123 		// Top level
124 		t = "_value=@('')";
125 		l = (List)p.parse(t, Object.class);
126 		assertEquals(1, l.size());
127 		assertEquals("", l.get(0));
128 		t = "0=''";
129 		l = p.parse(t, List.class, String.class);
130 		assertEquals(1, l.size());
131 		assertEquals("", l.get(0));
132 
133 		// 2nd level
134 		t = "?''=@('')";
135 		m = (Map)p.parse(t, Object.class);
136 		assertEquals("", ((List)m.get("")).get(0));
137 		t = "?''=@('')";
138 		m = p.parse(t, HashMap.class, String.class, List.class);
139 		assertEquals("", ((List)m.get("")).get(0));
140 
141 		// Array containing 3 empty strings
142 		t = "_value=@('','','')";
143 		l = (List)p.parse(t, Object.class);
144 		assertEquals(3, l.size());
145 		assertEquals("", l.get(0));
146 		assertEquals("", l.get(1));
147 		assertEquals("", l.get(2));
148 		t = "0=''&1=''&2=''";
149 		l = p.parse(t, List.class, Object.class);
150 		assertEquals(3, l.size());
151 		assertEquals("", l.get(0));
152 		assertEquals("", l.get(1));
153 		assertEquals("", l.get(2));
154 
155 		// String containing \u0000
156 		// Top level
157 		t = "_value='\u0000'";
158 		assertEquals("\u0000", p.parse(t, Object.class));
159 		t = "_value='\u0000'";
160 		assertEquals("\u0000", p.parse(t, String.class));
161 		assertEquals("\u0000", p.parse(t, Object.class));
162 
163 		// 2nd level
164 		t = "?'\u0000'='\u0000'";
165 		m = (Map)p.parse(t, Object.class);
166 		assertEquals(1, m.size());
167 		assertEquals("\u0000", m.get("\u0000"));
168 		m = p.parse(t, HashMap.class, String.class, Object.class);
169 		assertEquals(1, m.size());
170 		assertEquals("\u0000", m.get("\u0000"));
171 
172 		// Boolean
173 		// Top level
174 		t = "_value=false";
175 		var b = (Boolean)p.parse(t, Object.class);
176 		assertEquals(Boolean.FALSE, b);
177 		b = p.parse(t, Boolean.class);
178 		assertEquals(Boolean.FALSE, b);
179 		t = "_value=false";
180 		b = p.parse(t, Boolean.class);
181 		assertEquals(Boolean.FALSE, b);
182 
183 		// 2nd level
184 		t = "?x=false";
185 		m = (Map)p.parse(t, Object.class);
186 		assertEquals(Boolean.FALSE, m.get("x"));
187 		t = "?x=false";
188 		m = p.parse(t, HashMap.class, String.class, Boolean.class);
189 		assertEquals(Boolean.FALSE, m.get("x"));
190 
191 		// Number
192 		// Top level
193 		t = "_value=123";
194 		var i = (Integer)p.parse(t, Object.class);
195 		assertEquals(123, i.intValue());
196 		i = p.parse(t, Integer.class);
197 		assertEquals(123, i.intValue());
198 		var d = p.parse(t, Double.class);
199 		assertEquals(123, d.intValue());
200 		var f = p.parse(t, Float.class);
201 		assertEquals(123, f.intValue());
202 		t = "_value=123";
203 		i = p.parse(t, Integer.class);
204 		assertEquals(123, i.intValue());
205 
206 		// 2nd level
207 		t = "?x=123";
208 		m = (Map)p.parse(t, Object.class);
209 		assertEquals(123, ((Integer)m.get("x")).intValue());
210 		m = p.parse(t, HashMap.class, String.class, Double.class);
211 		assertEquals(123, ((Double)m.get("x")).intValue());
212 
213 		// Unencoded chars
214 		// Top level
215 		t = "_value=x;/?:@-_.!*'";
216 		assertEquals("x;/?:@-_.!*'", p.parse(t, Object.class));
217 
218 		// 2nd level
219 		t = "?x;/?:@-_.!*'=x;/?:@-_.!*'";
220 		m = (Map)p.parse(t, Object.class);
221 		assertEquals("x;/?:@-_.!*'", m.get("x;/?:@-_.!*'"));
222 		m = p.parse(t, HashMap.class, String.class, Object.class);
223 		assertEquals("x;/?:@-_.!*'", m.get("x;/?:@-_.!*'"));
224 		m = p.parse(t, HashMap.class, String.class, String.class);
225 		assertEquals("x;/?:@-_.!*'", m.get("x;/?:@-_.!*'"));
226 
227 		// Encoded chars
228 		// Top level
229 		assertThrows(ParseException.class, ()->p.parse("_value=x{}|\\^[]`<>#%\"&+", Object.class));
230 		t = "_value=x%7B%7D%7C%5C%5E%5B%5D%60%3C%3E%23%25%22%26%2B";
231 		assertEquals("x{}|\\^[]`<>#%\"&+", p.parse(t, Object.class));
232 		assertEquals("x{}|\\^[]`<>#%\"&+", p.parse(t, String.class));
233 
234 		// 2nd level
235 		assertThrows(ParseException.class, ()->p.parse("?x{}|\\^[]`<>#%\"&+=x{}|\\^[]`<>#%\"&+", Object.class));
236 		t = "?x%7B%7D%7C%5C%5E%5B%5D%60%3C%3E%23%25%22%26%2B=x%7B%7D%7C%5C%5E%5B%5D%60%3C%3E%23%25%22%26%2B";
237 		m = (Map)p.parse(t, Object.class);
238 		assertEquals("x{}|\\^[]`<>#%\"&+", m.get("x{}|\\^[]`<>#%\"&+"));
239 
240 		// Special chars
241 		// These characters are escaped and not encoded.
242 		// Top level
243 		t = "_value='x$,()'";
244 		assertEquals("x$,()", p.parse(t, Object.class));
245 		t = "_value='x~~$~~,~~(~~)'";
246 		assertEquals("x~$~,~(~)", p.parse(t, Object.class));
247 
248 		// At secondary levels, these characters are escaped and not encoded.
249 		// 2nd level
250 		t = "?'x$,()'='x$,()'";
251 		m = (Map)p.parse(t, Object.class);
252 		assertEquals("x$,()", m.get("x$,()"));
253 		t = "?'x~~$~~,~~(~~)'='x~~$~~,~~(~~)'";
254 		m = (Map)p.parse(t, Object.class);
255 		assertEquals("x~$~,~(~)", m.get("x~$~,~(~)"));
256 
257 		// Equals sign
258 		// Gets encoded at top level, and encoded+escaped at 2nd level.
259 		// Top level
260 		t = "_value='x='";
261 		assertEquals("x=", p.parse(t, Object.class));
262 		t = "_value='x%3D'";
263 		assertEquals("x=", p.parse(t, Object.class));
264 
265 		// 2nd level
266 		t = "?'x%3D'='x%3D'";
267 		m = (Map)p.parse(t, Object.class);
268 		assertEquals("x=", m.get("x="));
269 		t = "?'x~~%3D'='x~~%3D'";
270 		m = (Map)p.parse(t, Object.class);
271 		assertEquals("x~=", m.get("x~="));
272 
273 		// String starting with parenthesis
274 		// Top level
275 		t = "_value='()'";
276 		assertEquals("()", p.parse(t, Object.class));
277 		assertEquals("()", p.parse(t, String.class));
278 		t = "_value='()'";
279 		assertEquals("()", p.parse(t, Object.class));
280 		assertEquals("()", p.parse(t, String.class));
281 
282 		// 2nd level
283 		t = "?'()'='()'";
284 		m = (Map)p.parse(t, Object.class);
285 		assertEquals("()", m.get("()"));
286 		t = "?'()'='()'";
287 		m = p.parse(t, HashMap.class, String.class, Object.class);
288 		assertEquals("()", m.get("()"));
289 
290 		// String starting with $
291 		// Top level
292 		t = "_value=$a";
293 		assertEquals("$a", p.parse(t, Object.class));
294 		t = "_value=$a";
295 		assertEquals("$a", p.parse(t, Object.class));
296 
297 		// 2nd level
298 		t = "?$a=$a";
299 		m = (Map)p.parse(t, Object.class);
300 		assertEquals("$a", m.get("$a"));
301 		m = p.parse(t, HashMap.class, String.class, Object.class);
302 		assertEquals("$a", m.get("$a"));
303 
304 		// Blank string
305 		// Top level
306 		t = "_value=";
307 		assertEquals("", p.parse(t, Object.class));
308 
309 		// 2nd level
310 		t = "?=";
311 		m = (Map)p.parse(t, Object.class);
312 		assertEquals("", m.get(""));
313 		m = p.parse(t, HashMap.class, String.class, Object.class);
314 		assertEquals("", m.get(""));
315 
316 		// 3rd level
317 		t = "?=(=)";
318 		m = (Map)p.parse(t, Object.class);
319 		assertEquals("", ((Map)m.get("")).get(""));
320 		t = "?=(=)";
321 		m = p.parse(t, HashMap.class, String.class, HashMap.class);
322 		assertEquals("", ((Map)m.get("")).get(""));
323 
324 		// Newline character
325 		// Top level
326 		t = "_value='%0A'";
327 		assertEquals("\n", p.parse(t, Object.class));
328 
329 		// 2nd level
330 		t = "?'%0A'='%0A'";
331 		m = (Map)p.parse(t, Object.class);
332 		assertEquals("\n", m.get("\n"));
333 
334 		// 3rd level
335 		t = "?'%0A'=('%0A'='%0A')";
336 		m = (Map)p.parse(t, Object.class);
337 		assertEquals("\n", ((Map)m.get("\n")).get("\n"));
338 	}
339 
340 	//====================================================================================================
341 	// Unicode character test
342 	//====================================================================================================
343 	@Test void a02_unicodeChars() throws Exception {
344 		// 2-byte UTF-8 character
345 		// Top level
346 		var t = "_value=¢";
347 		assertEquals("¢", p.parse(t, Object.class));
348 		assertEquals("¢", p.parse(t, String.class));
349 		t = "_value=%C2%A2";
350 		assertEquals("¢", p.parse(t, Object.class));
351 		assertEquals("¢", p.parse(t, String.class));
352 
353 		// 2nd level
354 		t = "?%C2%A2=%C2%A2";
355 		var m = (Map)p.parse(t, Object.class);
356 		assertEquals("¢", m.get("¢"));
357 
358 		// 3rd level
359 		t = "?%C2%A2=(%C2%A2=%C2%A2)";
360 		m = (Map)p.parse(t, Object.class);
361 		assertEquals("¢", ((Map)m.get("¢")).get("¢"));
362 
363 		// 3-byte UTF-8 character
364 		// Top level
365 		t = "_value=€";
366 		assertEquals("€", p.parse(t, Object.class));
367 		assertEquals("€", p.parse(t, String.class));
368 		t = "_value=%E2%82%AC";
369 		assertEquals("€", p.parse(t, Object.class));
370 		assertEquals("€", p.parse(t, String.class));
371 
372 		// 2nd level
373 		t = "?%E2%82%AC=%E2%82%AC";
374 		m = (Map)p.parse(t, Object.class);
375 		assertEquals("€", m.get("€"));
376 
377 		// 3rd level
378 		t = "?%E2%82%AC=(%E2%82%AC=%E2%82%AC)";
379 		m = (Map)p.parse(t, Object.class);
380 		assertEquals("€", ((Map)m.get("€")).get("€"));
381 
382 		// 4-byte UTF-8 character
383 		// Top level
384 		t = "_value=𤭢";
385 		assertEquals("𤭢", p.parse(t, Object.class));
386 		assertEquals("𤭢", p.parse(t, String.class));
387 		t = "_value=%F0%A4%AD%A2";
388 		assertEquals("𤭢", p.parse(t, Object.class));
389 		assertEquals("𤭢", p.parse(t, String.class));
390 
391 		// 2nd level
392 		t = "?%F0%A4%AD%A2=%F0%A4%AD%A2";
393 		m = (Map)p.parse(t, Object.class);
394 		assertEquals("𤭢", m.get("𤭢"));
395 
396 		// 3rd level
397 		t = "?%F0%A4%AD%A2=(%F0%A4%AD%A2=%F0%A4%AD%A2)";
398 		m = (Map)p.parse(t, Object.class);
399 		assertEquals("𤭢", ((Map)m.get("𤭢")).get("𤭢"));
400 	}
401 
402 	//====================================================================================================
403 	// Test simple bean
404 	//====================================================================================================
405 	@Test void a03_simpleBean() throws Exception {
406 		var p2 = UrlEncodingParser.DEFAULT;
407 		var s = "?f1=foo&f2=123";
408 		var t = p2.parse(s, A.class);
409 		assertEquals("foo", t.f1);
410 		assertEquals(123, t.f2);
411 	}
412 
413 	public static class A {
414 		public String f1;
415 		public int f2;
416 	}
417 
418 	//====================================================================================================
419 	// Test URL-encoded strings with no-value parameters.
420 	//====================================================================================================
421 	@Test void a04_noValues() throws Exception {
422 		var p2 = UrlEncodingParser.DEFAULT;
423 		var s = "?f1";
424 		var m = p2.parse(s, JsonMap.class);
425 		assertTrue(m.containsKey("f1"));
426 		assertNull(m.get("f1"));
427 		s = "?f1=f2&f3";
428 		m = p2.parse(s, JsonMap.class);
429 		assertEquals("f2", m.get("f1"));
430 		assertTrue(m.containsKey("f3"));
431 		assertNull(m.get("f3"));
432 	}
433 
434 	//====================================================================================================
435 	// Test comma-delimited list parameters.
436 	//====================================================================================================
437 	@Test void a05_commaDelimitedLists() throws Exception {
438 		var p2 = UrlEncodingParser.DEFAULT;
439 		var s = "?f1=1,2,3&f2=a,b,c&f3=true,false&f4=&f5";
440 		var c = p2.parse(s, C.class);
441 		assertBean(c, "f1,f2,f3,f4", "[1,2,3],[a,b,c],[true,false],[]");
442 	}
443 
444 	public static class C {
445 		public int[] f1;
446 		public String[] f2;
447 		public boolean[] f3;
448 		public String[] f4;
449 		public String[] f5;
450 	}
451 
452 	//====================================================================================================
453 	// Test comma-delimited list parameters with special characters.
454 	//====================================================================================================
455 	@Test void a06_commaDelimitedListsWithSpecialChars() throws Exception {
456 		var p2 = UrlEncodingParser.DEFAULT;
457 
458 		// In the string below, the ~ character should not be interpreted as an escape.
459 		var s = "?f1=a~b,a~b";
460 		var c = p2.parse(s, C1.class);
461 		assertBean(c, "f1", "[a~b,a~b]");
462 
463 		s = "?f1=@(a~b,a~b)";
464 		c = p2.parse(s, C1.class);
465 		assertBean(c, "f1", "[a~b,a~b]");
466 
467 		s = "?f1=@('a~b','a~b')";
468 		c = p2.parse(s, C1.class);
469 		assertBean(c, "f1", "[a~b,a~b]");
470 
471 		s = "?f1=@('a~b','a~b')";
472 		c = p2.parse(s, C1.class);
473 		assertBean(c, "f1", "[a~b,a~b]");
474 
475 		s = "?f1=@('a~b','a~b')";
476 		c = p2.parse(s, C1.class);
477 		assertBean(c, "f1", "[a~b,a~b]");
478 
479 		s = "?f1=~~,~~";
480 		c = p2.parse(s, C1.class);
481 		assertJson("{f1:['~','~']}", c);
482 
483 		s = "?f1=@(~~,~~)";
484 		c = p2.parse(s, C1.class);
485 		assertJson("{f1:['~','~']}", c);
486 
487 		s = "?f1=@(~~~~~~,~~~~~~)";
488 		c = p2.parse(s, C1.class);
489 		assertJson("{f1:['~~~','~~~']}", c);
490 
491 		s = "?f1=@('~~~~~~','~~~~~~')";
492 		c = p2.parse(s, C1.class);
493 		assertJson("{f1:['~~~','~~~']}", c);
494 
495 		// The ~ should be treated as an escape if followed by any of the following characters:  '~
496 		s = "?f1=~'~~,~'~~";
497 		c = p2.parse(s, C1.class);
498 		assertJson("{f1:['\\'~','\\'~']}", c);
499 
500 		s = "?f1=@(~'~~,~'~~)";
501 		c = p2.parse(s, C1.class);
502 		assertJson("{f1:['\\'~','\\'~']}", c);
503 
504 		s = "?f1=@('~'~~','~'~~')";
505 		c = p2.parse(s, C1.class);
506 		assertJson("{f1:['\\'~','\\'~']}", c);
507 
508 		s = "?a~b=a~b";
509 		var m = p2.parse(s, JsonMap.class);
510 		assertEquals("{'a~b':'a~b'}", m.toString());
511 
512 		s = "?'a~b'='a~b'";
513 		m = p2.parse(s, JsonMap.class);
514 		assertEquals("{'a~b':'a~b'}", m.toString());
515 
516 		s = "?~~=~~";
517 		m = p2.parse(s, JsonMap.class);
518 		assertEquals("{'~':'~'}", m.toString());
519 
520 		s = "?'~~'='~~'";
521 		m = p2.parse(s, JsonMap.class);
522 		assertEquals("{'~':'~'}", m.toString());
523 
524 		s = "?~~~~~~=~~~~~~";
525 		m = p2.parse(s, JsonMap.class);
526 		assertEquals("{'~~~':'~~~'}", m.toString());
527 
528 		s = "?'~~~~~~'='~~~~~~'";
529 		m = p2.parse(s, JsonMap.class);
530 		assertEquals("{'~~~':'~~~'}", m.toString());
531 	}
532 
533 	public static class C1 {
534 		public String[] f1;
535 	}
536 
537 	//====================================================================================================
538 	// Test comma-delimited list parameters.
539 	//====================================================================================================
540 	@Test void a07_whitespace() throws Exception {
541 		var p2 = UrlEncodingParser.DEFAULT;
542 		var s = "?f1=foo\n\t&f2=bar\n\t";
543 		var m = p2.parse(s, JsonMap.class);
544 		assertEquals("{f1:'foo',f2:'bar'}", m.toString());
545 
546 		s = "?f1='\n\t'&f2='\n\t'";
547 		m = p2.parse(s, JsonMap.class);
548 		assertEquals("\n\t", m.getString("f1"));
549 		assertEquals("\n\t", m.getString("f2"));
550 
551 		s = "?f1='\n\t'\n\t&f2='\n\t'\n\t";
552 		m = p2.parse(s, JsonMap.class);
553 		assertEquals("\n\t", m.getString("f1"));
554 		assertEquals("\n\t", m.getString("f2"));
555 		assertEquals("{f1:'\\n\\t',f2:'\\n\\t'}", m.toString());  // Note that JsonSerializer escapes newlines and tabs.
556 
557 		s = "?f1='\n\t'\n\t&f2='\n\t'\n\t";
558 		m = p2.parse(s, JsonMap.class);
559 		assertEquals("\n\t", m.getString("f1"));
560 		assertEquals("\n\t", m.getString("f2"));
561 		assertEquals("{f1:'\\n\\t',f2:'\\n\\t'}", m.toString());  // Note that JsonSerializer escapes newlines and tabs.
562 
563 		s = "?f1=(\n\tf1a=a,\n\tf1b=b\n\t)\n\t&f2=(\n\tf2a=a,\n\tf2b=b\n\t)\n\t";
564 		m = p2.parse(s, JsonMap.class);
565 		assertEquals("{f1:{f1a:'a',f1b:'b'},f2:{f2a:'a',f2b:'b'}}", m.toString());  // Note that JsonSerializer escapes newlines and tabs.
566 		var d = p2.parse(s, D.class);
567 		assertBean(d, "f1{f1a,f1b},f2{f2a,f2b}", "{a,b},{a,b}");
568 
569 		s = "?f1=(\n\tf1a='\n\t',\n\tf1b='\n\t'\n\t)\n\t&f2=(\n\tf2a='\n\t',\n\tf2b='\n\t'\n\t)\n\t";
570 		m = p2.parse(s, JsonMap.class);
571 		assertEquals("{f1:{f1a:'\\n\\t',f1b:'\\n\\t'},f2:{f2a:'\\n\\t',f2b:'\\n\\t'}}", m.toString());  // Note that JsonSerializer escapes newlines and tabs.
572 		d = p2.parse(s, D.class);
573 		assertBean(d, "f1{f1a,f1b},f2{f2a,f2b}", "{\n\t,\n\t},{\n\t,\n\t}");
574 
575 		s = "?f1=@(\n\tfoo,\n\tbar\n\t)\n\t&f2=@(\n\tfoo,\n\tbar\n\t)\n\t";
576 		m = p2.parse(s, JsonMap.class);
577 		assertEquals("{f1:['foo','bar'],f2:['foo','bar']}", m.toString());  // Note that JsonSerializer escapes newlines and tabs.
578 
579 		s = "f1=a,\n\tb,\n\tc\n\t&f2=1,\n\t2,\n\t3\n\t&f3=true,\n\tfalse\n\t";
580 		var e = p2.parse(s, E.class);
581 		assertBean(e, "f1,f2,f3", "[a,b,c],[1,2,3],[true,false]");
582 
583 		s = "f1=a%2C%0D%0Ab%2C%0D%0Ac%0D%0A&f2=1%2C%0D%0A2%2C%0D%0A3%0D%0A&f3=true%2C%0D%0Afalse%0D%0A";
584 		e = p2.parse(s, E.class);
585 		assertBean(e, "f1,f2,f3", "[a,b,c],[1,2,3],[true,false]");
586 	}
587 
588 	public static class D {
589 		public D1 f1;
590 		public D2 f2;
591 	}
592 
593 	public static class D1 {
594 		public String f1a, f1b;
595 	}
596 
597 	public static class D2 {
598 		public String f2a, f2b;
599 	}
600 
601 	public static class E {
602 		public String[] f1;
603 		public int[] f2;
604 		public boolean[] f3;
605 	}
606 
607 	//====================================================================================================
608 	// Multi-part parameters on beans via URLENC_expandedParams
609 	//====================================================================================================
610 	@Test void a08_multiPartParametersOnBeansViaProperty() throws Exception {
611 		var p2 = UrlEncodingParser.create().expandedParams().build();
612 		var in = ""
613 			+ "f01=a&f01=b"
614 			+ "&f02=c&f02=d"
615 			+ "&f03=1&f03=2"
616 			+ "&f04=3&f04=4"
617 			+ "&f05=@(e,f)&f05=@(g,h)"
618 			+ "&f06=@(i,j)&f06=@(k,l)"
619 			+ "&f07=(a=a,b=1,c=true)&f07=(a=b,b=2,c=false)"
620 			+ "&f08=(a=a,b=1,c=true)&f08=(a=b,b=2,c=false)"
621 			+ "&f09=@((a=a,b=1,c=true))&f09=@((a=b,b=2,c=false))"
622 			+ "&f10=@((a=a,b=1,c=true))&f10=@((a=b,b=2,c=false))"
623 			+ "&f11=a&f11=b"
624 			+ "&f12=c&f12=d"
625 			+ "&f13=1&f13=2"
626 			+ "&f14=3&f14=4"
627 			+ "&f15=@(e,f)&f15=@(g,h)"
628 			+ "&f16=@(i,j)&f16=@(k,l)"
629 			+ "&f17=(a=a,b=1,c=true)&f17=(a=b,b=2,c=false)"
630 			+ "&f18=(a=a,b=1,c=true)&f18=(a=b,b=2,c=false)"
631 			+ "&f19=@((a=a,b=1,c=true))&f19=@((a=b,b=2,c=false))"
632 			+ "&f20=@((a=a,b=1,c=true))&f20=@((a=b,b=2,c=false))";
633 
634 		var t = p2.parse(in, DTOs.B.class);
635 		assertBean(t,
636 			"f01,f02,f03,f04,f05,f06,f07{#{a,b,c}},f08{#{a,b,c}},f09{#{#{a,b,c}}},f10{#{#{a,b,c}}},f11,f12,f13,f14,f15,f16,f17{#{a,b,c}},f18{#{a,b,c}},f19{#{#{a,b,c}}},f20{#{#{a,b,c}}}",
637 			"[a,b],[c,d],[1,2],[3,4],[[e,f],[g,h]],[[i,j],[k,l]],{[{a,1,true},{b,2,false}]},{[{a,1,true},{b,2,false}]},{[{[{a,1,true}]},{[{b,2,false}]}]},{[{[{a,1,true}]},{[{b,2,false}]}]},[a,b],[c,d],[1,2],[3,4],[[e,f],[g,h]],[[i,j],[k,l]],{[{a,1,true},{b,2,false}]},{[{a,1,true},{b,2,false}]},{[{[{a,1,true}]},{[{b,2,false}]}]},{[{[{a,1,true}]},{[{b,2,false}]}]}");
638 	}
639 
640 	@Test void a09_multiPartParametersOnBeansViaProperty_usingConfig() throws Exception {
641 		var p2 = UrlEncodingParser.create().expandedParams().applyAnnotations(DTOs2.Annotations.class).build();
642 		var in = ""
643 			+ "f01=a&f01=b"
644 			+ "&f02=c&f02=d"
645 			+ "&f03=1&f03=2"
646 			+ "&f04=3&f04=4"
647 			+ "&f05=@(e,f)&f05=@(g,h)"
648 			+ "&f06=@(i,j)&f06=@(k,l)"
649 			+ "&f07=(a=a,b=1,c=true)&f07=(a=b,b=2,c=false)"
650 			+ "&f08=(a=a,b=1,c=true)&f08=(a=b,b=2,c=false)"
651 			+ "&f09=@((a=a,b=1,c=true))&f09=@((a=b,b=2,c=false))"
652 			+ "&f10=@((a=a,b=1,c=true))&f10=@((a=b,b=2,c=false))"
653 			+ "&f11=a&f11=b"
654 			+ "&f12=c&f12=d"
655 			+ "&f13=1&f13=2"
656 			+ "&f14=3&f14=4"
657 			+ "&f15=@(e,f)&f15=@(g,h)"
658 			+ "&f16=@(i,j)&f16=@(k,l)"
659 			+ "&f17=(a=a,b=1,c=true)&f17=(a=b,b=2,c=false)"
660 			+ "&f18=(a=a,b=1,c=true)&f18=(a=b,b=2,c=false)"
661 			+ "&f19=@((a=a,b=1,c=true))&f19=@((a=b,b=2,c=false))"
662 			+ "&f20=@((a=a,b=1,c=true))&f20=@((a=b,b=2,c=false))";
663 
664 		assertBean(p2.parse(in, DTOs2.B.class),
665 			"f01,f02,f03,f04,f05,f06,f07{#{a,b,c}},f08{#{a,b,c}},f09{#{#{a,b,c}}},f10{#{#{a,b,c}}},f11,f12,f13,f14,f15,f16,f17{#{a,b,c}},f18{#{a,b,c}},f19{#{#{a,b,c}}},f20{#{#{a,b,c}}}",
666 			"[a,b],[c,d],[1,2],[3,4],[[e,f],[g,h]],[[i,j],[k,l]],{[{a,1,true},{b,2,false}]},{[{a,1,true},{b,2,false}]},{[{[{a,1,true}]},{[{b,2,false}]}]},{[{[{a,1,true}]},{[{b,2,false}]}]},[a,b],[c,d],[1,2],[3,4],[[e,f],[g,h]],[[i,j],[k,l]],{[{a,1,true},{b,2,false}]},{[{a,1,true},{b,2,false}]},{[{[{a,1,true}]},{[{b,2,false}]}]},{[{[{a,1,true}]},{[{b,2,false}]}]}");
667 	}
668 
669 	//====================================================================================================
670 	// Multi-part parameters on beans via @UrlEncoding.expandedParams on class
671 	//====================================================================================================
672 	@Test void a10_multiPartParametersOnBeansViaAnnotationOnClass() throws Exception {
673 		var p2 = UrlEncodingParser.DEFAULT;
674 		var in = ""
675 			+ "f01=a&f01=b"
676 			+ "&f02=c&f02=d"
677 			+ "&f03=1&f03=2"
678 			+ "&f04=3&f04=4"
679 			+ "&f05=@(e,f)&f05=@(g,h)"
680 			+ "&f06=@(i,j)&f06=@(k,l)"
681 			+ "&f07=(a=a,b=1,c=true)&f07=(a=b,b=2,c=false)"
682 			+ "&f08=(a=a,b=1,c=true)&f08=(a=b,b=2,c=false)"
683 			+ "&f09=@((a=a,b=1,c=true))&f09=@((a=b,b=2,c=false))"
684 			+ "&f10=@((a=a,b=1,c=true))&f10=@((a=b,b=2,c=false))"
685 			+ "&f11=a&f11=b"
686 			+ "&f12=c&f12=d"
687 			+ "&f13=1&f13=2"
688 			+ "&f14=3&f14=4"
689 			+ "&f15=@(e,f)&f15=@(g,h)"
690 			+ "&f16=@(i,j)&f16=@(k,l)"
691 			+ "&f17=(a=a,b=1,c=true)&f17=(a=b,b=2,c=false)"
692 			+ "&f18=(a=a,b=1,c=true)&f18=(a=b,b=2,c=false)"
693 			+ "&f19=@((a=a,b=1,c=true))&f19=@((a=b,b=2,c=false))"
694 			+ "&f20=@((a=a,b=1,c=true))&f20=@((a=b,b=2,c=false))";
695 
696 		assertBean(p2.parse(in, DTOs.C.class),
697 			"f01,f02,f03,f04,f05,f06,f07{#{a,b,c}},f08{#{a,b,c}},f09{#{#{a,b,c}}},f10{#{#{a,b,c}}},f11,f12,f13,f14,f15,f16,f17{#{a,b,c}},f18{#{a,b,c}},f19{#{#{a,b,c}}},f20{#{#{a,b,c}}}",
698 			"[a,b],[c,d],[1,2],[3,4],[[e,f],[g,h]],[[i,j],[k,l]],{[{a,1,true},{b,2,false}]},{[{a,1,true},{b,2,false}]},{[{[{a,1,true}]},{[{b,2,false}]}]},{[{[{a,1,true}]},{[{b,2,false}]}]},[a,b],[c,d],[1,2],[3,4],[[e,f],[g,h]],[[i,j],[k,l]],{[{a,1,true},{b,2,false}]},{[{a,1,true},{b,2,false}]},{[{[{a,1,true}]},{[{b,2,false}]}]},{[{[{a,1,true}]},{[{b,2,false}]}]}");
699 	}
700 
701 	@Test void a11_multiPartParametersOnBeansViaAnnotationOnClass_usingConfig() throws Exception {
702 		var p2 = UrlEncodingParser.DEFAULT.copy().applyAnnotations(DTOs2.Annotations.class).build();
703 		var in = ""
704 			+ "f01=a&f01=b"
705 			+ "&f02=c&f02=d"
706 			+ "&f03=1&f03=2"
707 			+ "&f04=3&f04=4"
708 			+ "&f05=@(e,f)&f05=@(g,h)"
709 			+ "&f06=@(i,j)&f06=@(k,l)"
710 			+ "&f07=(a=a,b=1,c=true)&f07=(a=b,b=2,c=false)"
711 			+ "&f08=(a=a,b=1,c=true)&f08=(a=b,b=2,c=false)"
712 			+ "&f09=@((a=a,b=1,c=true))&f09=@((a=b,b=2,c=false))"
713 			+ "&f10=@((a=a,b=1,c=true))&f10=@((a=b,b=2,c=false))"
714 			+ "&f11=a&f11=b"
715 			+ "&f12=c&f12=d"
716 			+ "&f13=1&f13=2"
717 			+ "&f14=3&f14=4"
718 			+ "&f15=@(e,f)&f15=@(g,h)"
719 			+ "&f16=@(i,j)&f16=@(k,l)"
720 			+ "&f17=(a=a,b=1,c=true)&f17=(a=b,b=2,c=false)"
721 			+ "&f18=(a=a,b=1,c=true)&f18=(a=b,b=2,c=false)"
722 			+ "&f19=@((a=a,b=1,c=true))&f19=@((a=b,b=2,c=false))"
723 			+ "&f20=@((a=a,b=1,c=true))&f20=@((a=b,b=2,c=false))";
724 
725 		assertBean(p2.parse(in, DTOs2.C.class),
726 			"f01,f02,f03,f04,f05,f06,f07{#{a,b,c}},f08{#{a,b,c}},f09{#{#{a,b,c}}},f10{#{#{a,b,c}}},f11,f12,f13,f14,f15,f16,f17{#{a,b,c}},f18{#{a,b,c}},f19{#{#{a,b,c}}},f20{#{#{a,b,c}}}",
727 			"[a,b],[c,d],[1,2],[3,4],[[e,f],[g,h]],[[i,j],[k,l]],{[{a,1,true},{b,2,false}]},{[{a,1,true},{b,2,false}]},{[{[{a,1,true}]},{[{b,2,false}]}]},{[{[{a,1,true}]},{[{b,2,false}]}]},[a,b],[c,d],[1,2],[3,4],[[e,f],[g,h]],[[i,j],[k,l]],{[{a,1,true},{b,2,false}]},{[{a,1,true},{b,2,false}]},{[{[{a,1,true}]},{[{b,2,false}]}]},{[{[{a,1,true}]},{[{b,2,false}]}]}");
728 	}
729 }