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