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.commons.utils.CollectionUtils.*;
20  import static org.junit.jupiter.api.Assertions.*;
21  
22  import org.apache.juneau.*;
23  import org.apache.juneau.annotation.*;
24  import org.apache.juneau.collections.*;
25  import org.junit.jupiter.api.*;
26  
27  class UrlEncodingSerializer_Test extends TestBase {
28  
29  	static UrlEncodingSerializer s = UrlEncodingSerializer.DEFAULT.copy().addRootType().build();
30  	static UrlEncodingSerializer sr = UrlEncodingSerializer.DEFAULT_READABLE.copy().addRootType().build();
31  
32  	//====================================================================================================
33  	// Basic test
34  	//====================================================================================================
35  	@Test void a01_basic() throws Exception {
36  
37  		// Simple string
38  		// Top level
39  		var t = (Object)"a";
40  		assertEquals("_value=a", s.serialize(t));
41  
42  		// 2nd level
43  		t = JsonMap.ofJson("{a:'a'}");
44  		assertEquals("a=a", s.serialize(t));
45  		assertEquals("a=a", sr.serialize(t));
46  
47  		// Simple map
48  		// Top level
49  		t = JsonMap.ofJson("{a:'b',c:123,d:false,e:true,f:null}");
50  		assertEquals("a=b&c=123&d=false&e=true&f=null", s.serialize(t));
51  		assertEquals("a=b\n&c=123\n&d=false\n&e=true\n&f=null", sr.serialize(t));
52  
53  		// 2nd level
54  		t = JsonMap.ofJson("{a:{a:'b',c:123,d:false,e:true,f:null}}");
55  		assertEquals("a=(a=b,c=123,d=false,e=true,f=null)", s.serialize(t));
56  		assertEquals("a=(\n\ta=b,\n\tc=123,\n\td=false,\n\te=true,\n\tf=null\n)", sr.serialize(t));
57  
58  		// Simple map with primitives as literals
59  		t = JsonMap.ofJson("{a:'b',c:'123',d:'false',e:'true',f:'null'}");
60  		assertEquals("a=b&c='123'&d='false'&e='true'&f='null'", s.serialize(t));
61  		assertEquals("a=b\n&c='123'\n&d='false'\n&e='true'\n&f='null'", sr.serialize(t));
62  
63  		// null
64  		// Note that serializeParams is always encoded.
65  		// Top level
66  		t = null;
67  		assertEquals("_value=null", s.serialize(t));
68  		assertEquals("_value=null", sr.serialize(t));
69  
70  		// 2nd level
71  		t = JsonMap.ofJson("{null:null}");
72  		assertEquals("null=null", s.serialize(t));
73  		assertEquals("null=null", sr.serialize(t));
74  
75  		// 3rd level
76  		t = JsonMap.ofJson("{null:{null:null}}");
77  		assertEquals("null=(null=null)", s.serialize(t));
78  		assertEquals("null=(\n\tnull=null\n)", sr.serialize(t));
79  
80  		// Empty array
81  		// Top level
82  		t = new String[0];
83  		assertEquals("", s.serialize(t));
84  		assertEquals("", sr.serialize(t));
85  
86  		// 2nd level in map
87  		t = JsonMap.ofJson("{x:[]}");
88  		assertEquals("x=@()", s.serialize(t));
89  		assertEquals("x=@()", sr.serialize(t));
90  
91  		// Empty 2 dimensional array
92  		t = new String[1][0];
93  		assertEquals("0=@()", s.serialize(t));
94  		assertEquals("0=@()", sr.serialize(t));
95  
96  		// Array containing empty string
97  		// Top level
98  		t = a("");
99  		assertEquals("0=''", s.serialize(t));
100 		assertEquals("0=''", sr.serialize(t));
101 
102 		// 2nd level
103 		t = JsonMap.ofJson("{x:['']}");
104 		assertEquals("x=@('')", s.serialize(t));
105 		assertEquals("x=@(\n\t''\n)", sr.serialize(t));
106 
107 		// Array containing 3 empty strings
108 		t = a("","","");
109 		assertEquals("0=''&1=''&2=''", s.serialize(t));
110 		assertEquals("0=''\n&1=''\n&2=''", sr.serialize(t));
111 
112 		// String containing \u0000
113 		// Top level
114 		t = "\u0000";
115 		assertEquals("_value=%00", s.serialize(t));
116 		assertEquals("_value=%00", sr.serialize(t));
117 
118 		// 2nd level
119 		t = JsonMap.ofJson("{'\u0000':'\u0000'}");
120 		assertEquals("%00=%00", s.serialize(t));
121 		assertEquals("%00=%00", sr.serialize(t));
122 
123 		// Boolean
124 		// Top level
125 		t = false;
126 		assertEquals("_value=false", s.serialize(t));
127 		assertEquals("_value=false", sr.serialize(t));
128 
129 		// 2nd level
130 		t = JsonMap.ofJson("{x:false}");
131 		assertEquals("x=false", s.serialize(t));
132 		assertEquals("x=false", sr.serialize(t));
133 
134 		// Number
135 		// Top level
136 		t = 123;
137 		assertEquals("_value=123", s.serialize(t));
138 		assertEquals("_value=123", sr.serialize(t));
139 
140 		// 2nd level
141 		t = JsonMap.ofJson("{x:123}");
142 		assertEquals("x=123", s.serialize(t));
143 		assertEquals("x=123", sr.serialize(t));
144 
145 		// Unencoded chars
146 		// Top level
147 		t = "x;/?:@-_.!*'";
148 		assertEquals("_value=x;/?:@-_.!*~'", s.serialize(t));
149 		assertEquals("_value=x;/?:@-_.!*~'", sr.serialize(t));
150 
151 		// 2nd level
152 		t = JsonMap.ofJson("{x:'x;/?:@-_.!*\\''}");
153 		assertEquals("x=x;/?:@-_.!*~'", s.serialize(t));
154 		assertEquals("x=x;/?:@-_.!*~'", sr.serialize(t));
155 
156 		// Encoded chars
157 		// Top level
158 		t = "x{}|\\^[]`<>#%\"&+";
159 		assertEquals("_value=x%7B%7D%7C%5C%5E%5B%5D%60%3C%3E%23%25%22%26%2B", s.serialize(t));
160 		assertEquals("_value=x%7B%7D%7C%5C%5E%5B%5D%60%3C%3E%23%25%22%26%2B", sr.serialize(t));
161 
162 		// 2nd level
163 		t = JsonMap.ofJson("{'x{}|\\\\^[]`<>#%\"&+':'x{}|\\\\^[]`<>#%\"&+'}");
164 		assertEquals("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", s.serialize(t));
165 		assertEquals("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", sr.serialize(t));
166 
167 		// Escaped chars
168 		// Top level
169 		t = "x$,()~";
170 		assertEquals("_value='x$,()~~'", s.serialize(t));
171 		assertEquals("_value='x$,()~~'", sr.serialize(t));
172 
173 		// 2nd level
174 		t = JsonMap.ofJson("{'x$,()~':'x$,()~'}");
175 		assertEquals("'x$,()~~'='x$,()~~'", s.serialize(t));
176 		assertEquals("'x$,()~~'='x$,()~~'", sr.serialize(t));
177 
178 		// 3rd level
179 		t = JsonMap.ofJson("{'x$,()~':{'x$,()~':'x$,()~'}}");
180 		assertEquals("'x$,()~~'=('x$,()~~'='x$,()~~')", s.serialize(t));
181 		assertEquals("'x$,()~~'=(\n\t'x$,()~~'='x$,()~~'\n)", sr.serialize(t));
182 
183 		// Equals sign
184 		// Gets encoded at top level, and encoded+escaped at 2nd level.
185 		// Top level
186 		t = "x=";
187 		assertEquals("_value='x='", s.serialize(t));
188 		assertEquals("_value='x='", sr.serialize(t));
189 
190 		// 2nd level
191 		t = JsonMap.ofJson("{'x=':'x='}");
192 		assertEquals("'x%3D'='x='", s.serialize(t));
193 		assertEquals("'x%3D'='x='", sr.serialize(t));
194 
195 		// 3rd level
196 		t = JsonMap.ofJson("{'x=':{'x=':'x='}}");
197 		assertEquals("'x%3D'=('x='='x=')", s.serialize(t));
198 		assertEquals("'x%3D'=(\n\t'x='='x='\n)", sr.serialize(t));
199 
200 		// String starting with parenthesis
201 		// Top level
202 		t = "()";
203 		assertEquals("_value='()'", s.serialize(t));
204 		assertEquals("_value='()'", sr.serialize(t));
205 
206 		// 2nd level
207 		t = JsonMap.ofJson("{'()':'()'}");
208 		assertEquals("'()'='()'", s.serialize(t));
209 		assertEquals("'()'='()'", sr.serialize(t));
210 
211 		// String starting with $
212 		// Top level
213 		t = "$a";
214 		assertEquals("_value=$a", s.serialize(t));
215 		assertEquals("_value=$a", sr.serialize(t));
216 
217 		// 2nd level
218 		t = JsonMap.ofJson("{$a:'$a'}");
219 		assertEquals("$a=$a", s.serialize(t));
220 		assertEquals("$a=$a", sr.serialize(t));
221 
222 		// Blank string
223 		// Top level
224 		t = "";
225 		assertEquals("_value=''", s.serialize(t));
226 		assertEquals("_value=''", sr.serialize(t));
227 
228 		// 2nd level
229 		t = JsonMap.ofJson("{'':''}");
230 		assertEquals("''=''", s.serialize(t));
231 		assertEquals("''=''", sr.serialize(t));
232 
233 		// 3rd level
234 		t = JsonMap.ofJson("{'':{'':''}}");
235 		assertEquals("''=(''='')", s.serialize(t));
236 		assertEquals("''=(\n\t''=''\n)", sr.serialize(t));
237 
238 		// Newline character
239 		// Top level
240 		t = "\n";
241 		assertEquals("_value='%0A'", s.serialize(t));
242 		assertEquals("_value='%0A'", sr.serialize(t));
243 
244 		// 2nd level
245 		t = JsonMap.ofJson("{'\n':'\n'}");
246 		assertEquals("'%0A'='%0A'", s.serialize(t));
247 		assertEquals("'%0A'='%0A'", sr.serialize(t));
248 
249 		// 3rd level
250 		t = JsonMap.ofJson("{'\n':{'\n':'\n'}}");
251 		assertEquals("'%0A'=('%0A'='%0A')", s.serialize(t));
252 		assertEquals("'%0A'=(\n\t'%0A'='%0A'\n)", sr.serialize(t));
253 	}
254 
255 	//====================================================================================================
256 	// Unicode characters test
257 	//====================================================================================================
258 	@Test void a02_unicodeChars() throws Exception {
259 
260 		// 2-byte UTF-8 character
261 		// Top level
262 		var t = (Object)"¢";
263 		assertEquals("_value=%C2%A2", s.serialize(t));
264 		assertEquals("_value=%C2%A2", sr.serialize(t));
265 
266 		// 2nd level
267 		t = JsonMap.ofJson("{'¢':'¢'}");
268 		assertEquals("%C2%A2=%C2%A2", s.serialize(t));
269 		assertEquals("%C2%A2=%C2%A2", sr.serialize(t));
270 
271 		// 3rd level
272 		t = JsonMap.ofJson("{'¢':{'¢':'¢'}}");
273 		assertEquals("%C2%A2=(%C2%A2=%C2%A2)", s.serialize(t));
274 		assertEquals("%C2%A2=(\n\t%C2%A2=%C2%A2\n)", sr.serialize(t));
275 
276 		// 3-byte UTF-8 character
277 		// Top level
278 		t = "€";
279 		assertEquals("_value=%E2%82%AC", s.serialize(t));
280 		assertEquals("_value=%E2%82%AC", sr.serialize(t));
281 
282 		// 2nd level
283 		t = JsonMap.ofJson("{'€':'€'}");
284 		assertEquals("%E2%82%AC=%E2%82%AC", s.serialize(t));
285 		assertEquals("%E2%82%AC=%E2%82%AC", sr.serialize(t));
286 
287 		// 3rd level
288 		t = JsonMap.ofJson("{'€':{'€':'€'}}");
289 		assertEquals("%E2%82%AC=(%E2%82%AC=%E2%82%AC)", s.serialize(t));
290 		assertEquals("%E2%82%AC=(\n\t%E2%82%AC=%E2%82%AC\n)", sr.serialize(t));
291 
292 		// 4-byte UTF-8 character
293 		// Top level
294 		t = "𤭢";
295 		assertEquals("_value=%F0%A4%AD%A2", s.serialize(t));
296 		assertEquals("_value=%F0%A4%AD%A2", sr.serialize(t));
297 
298 		// 2nd level
299 		t = JsonMap.ofJson("{'𤭢':'𤭢'}");
300 		assertEquals("%F0%A4%AD%A2=%F0%A4%AD%A2", s.serialize(t));
301 		assertEquals("%F0%A4%AD%A2=%F0%A4%AD%A2", sr.serialize(t));
302 
303 		// 3rd level
304 		t = JsonMap.ofJson("{'𤭢':{'𤭢':'𤭢'}}");
305 		assertEquals("%F0%A4%AD%A2=(%F0%A4%AD%A2=%F0%A4%AD%A2)", s.serialize(t));
306 		assertEquals("%F0%A4%AD%A2=(\n\t%F0%A4%AD%A2=%F0%A4%AD%A2\n)", sr.serialize(t));
307 	}
308 
309 	//====================================================================================================
310 	// Multi-part parameters on beans via URLENC_expandedParams
311 	//====================================================================================================
312 	@Test void a03_multiPartParametersOnBeansViaProperty() throws Exception {
313 		var t = DTOs.B.create();
314 		var s2 = UrlEncodingSerializer.DEFAULT;
315 		var r = s2.serialize(t);
316 
317 		var e = """
318 			f01=@(a,b)\
319 			&f02=@(c,d)\
320 			&f03=@(1,2)\
321 			&f04=@(3,4)\
322 			&f05=@(@(e,f),@(g,h))\
323 			&f06=@(@(i,j),@(k,l))\
324 			&f07=@((a=a,b=1,c=true),(a=a,b=1,c=true))\
325 			&f08=@((a=a,b=1,c=true),(a=a,b=1,c=true))\
326 			&f09=@(@((a=a,b=1,c=true)),@((a=a,b=1,c=true)))\
327 			&f10=@(@((a=a,b=1,c=true)),@((a=a,b=1,c=true)))\
328 			&f11=@(a,b)\
329 			&f12=@(c,d)\
330 			&f13=@(1,2)\
331 			&f14=@(3,4)\
332 			&f15=@(@(e,f),@(g,h))\
333 			&f16=@(@(i,j),@(k,l))\
334 			&f17=@((a=a,b=1,c=true),(a=a,b=1,c=true))\
335 			&f18=@((a=a,b=1,c=true),(a=a,b=1,c=true))\
336 			&f19=@(@((a=a,b=1,c=true)),@((a=a,b=1,c=true)))\
337 			&f20=@(@((a=a,b=1,c=true)),@((a=a,b=1,c=true)))""";
338 		assertEquals(e, r);
339 
340 		s2 = UrlEncodingSerializer.create().expandedParams().build();
341 		r = s2.serialize(t);
342 		e = """
343 			f01=a&f01=b\
344 			&f02=c&f02=d\
345 			&f03=1&f03=2\
346 			&f04=3&f04=4\
347 			&f05=@(e,f)&f05=@(g,h)\
348 			&f06=@(i,j)&f06=@(k,l)\
349 			&f07=(a=a,b=1,c=true)&f07=(a=a,b=1,c=true)\
350 			&f08=(a=a,b=1,c=true)&f08=(a=a,b=1,c=true)\
351 			&f09=@((a=a,b=1,c=true))&f09=@((a=a,b=1,c=true))\
352 			&f10=@((a=a,b=1,c=true))&f10=@((a=a,b=1,c=true))\
353 			&f11=a&f11=b\
354 			&f12=c&f12=d\
355 			&f13=1&f13=2\
356 			&f14=3&f14=4\
357 			&f15=@(e,f)&f15=@(g,h)\
358 			&f16=@(i,j)&f16=@(k,l)\
359 			&f17=(a=a,b=1,c=true)&f17=(a=a,b=1,c=true)\
360 			&f18=(a=a,b=1,c=true)&f18=(a=a,b=1,c=true)\
361 			&f19=@((a=a,b=1,c=true))&f19=@((a=a,b=1,c=true))\
362 			&f20=@((a=a,b=1,c=true))&f20=@((a=a,b=1,c=true))""";
363 		assertEquals(e, r);
364 	}
365 
366 	@Test void a04_multiPartParametersOnBeansViaProperty_usingConfig() throws Exception {
367 		var t = DTOs2.B.create();
368 		var s2 = UrlEncodingSerializer.DEFAULT.copy().applyAnnotations(DTOs2.Annotations.class).build();
369 		var r = s2.serialize(t);
370 
371 		var e = """
372 			f01=@(a,b)\
373 			&f02=@(c,d)\
374 			&f03=@(1,2)\
375 			&f04=@(3,4)\
376 			&f05=@(@(e,f),@(g,h))\
377 			&f06=@(@(i,j),@(k,l))\
378 			&f07=@((a=a,b=1,c=true),(a=a,b=1,c=true))\
379 			&f08=@((a=a,b=1,c=true),(a=a,b=1,c=true))\
380 			&f09=@(@((a=a,b=1,c=true)),@((a=a,b=1,c=true)))\
381 			&f10=@(@((a=a,b=1,c=true)),@((a=a,b=1,c=true)))\
382 			&f11=@(a,b)\
383 			&f12=@(c,d)\
384 			&f13=@(1,2)\
385 			&f14=@(3,4)\
386 			&f15=@(@(e,f),@(g,h))\
387 			&f16=@(@(i,j),@(k,l))\
388 			&f17=@((a=a,b=1,c=true),(a=a,b=1,c=true))\
389 			&f18=@((a=a,b=1,c=true),(a=a,b=1,c=true))\
390 			&f19=@(@((a=a,b=1,c=true)),@((a=a,b=1,c=true)))\
391 			&f20=@(@((a=a,b=1,c=true)),@((a=a,b=1,c=true)))""";
392 		assertEquals(e, r);
393 
394 		s2 = UrlEncodingSerializer.create().expandedParams().applyAnnotations(DTOs2.Annotations.class).build();
395 		r = s2.serialize(t);
396 		e = """
397 			f01=a&f01=b\
398 			&f02=c&f02=d\
399 			&f03=1&f03=2\
400 			&f04=3&f04=4\
401 			&f05=@(e,f)&f05=@(g,h)\
402 			&f06=@(i,j)&f06=@(k,l)\
403 			&f07=(a=a,b=1,c=true)&f07=(a=a,b=1,c=true)\
404 			&f08=(a=a,b=1,c=true)&f08=(a=a,b=1,c=true)\
405 			&f09=@((a=a,b=1,c=true))&f09=@((a=a,b=1,c=true))\
406 			&f10=@((a=a,b=1,c=true))&f10=@((a=a,b=1,c=true))\
407 			&f11=a&f11=b\
408 			&f12=c&f12=d\
409 			&f13=1&f13=2\
410 			&f14=3&f14=4\
411 			&f15=@(e,f)&f15=@(g,h)\
412 			&f16=@(i,j)&f16=@(k,l)\
413 			&f17=(a=a,b=1,c=true)&f17=(a=a,b=1,c=true)\
414 			&f18=(a=a,b=1,c=true)&f18=(a=a,b=1,c=true)\
415 			&f19=@((a=a,b=1,c=true))&f19=@((a=a,b=1,c=true))\
416 			&f20=@((a=a,b=1,c=true))&f20=@((a=a,b=1,c=true))""";
417 		assertEquals(e, r);
418 	}
419 
420 	//====================================================================================================
421 	// Multi-part parameters on beans via @UrlEncoding.expandedParams on class
422 	//====================================================================================================
423 	@Test void a05_multiPartParametersOnBeansViaAnnotationOnClass() throws Exception {
424 		var t = DTOs.C.create();
425 		var s2 = UrlEncodingSerializer.DEFAULT;
426 		var r = s2.serialize(t);
427 
428 		var e = ""
429 			+ "f01=a&f01=b"
430 			+ "&f02=c&f02=d"
431 			+ "&f03=1&f03=2"
432 			+ "&f04=3&f04=4"
433 			+ "&f05=@(e,f)&f05=@(g,h)"
434 			+ "&f06=@(i,j)&f06=@(k,l)"
435 			+ "&f07=(a=a,b=1,c=true)&f07=(a=a,b=1,c=true)"
436 			+ "&f08=(a=a,b=1,c=true)&f08=(a=a,b=1,c=true)"
437 			+ "&f09=@((a=a,b=1,c=true))&f09=@((a=a,b=1,c=true))"
438 			+ "&f10=@((a=a,b=1,c=true))&f10=@((a=a,b=1,c=true))"
439 			+ "&f11=a&f11=b"
440 			+ "&f12=c&f12=d"
441 			+ "&f13=1&f13=2"
442 			+ "&f14=3&f14=4"
443 			+ "&f15=@(e,f)&f15=@(g,h)"
444 			+ "&f16=@(i,j)&f16=@(k,l)"
445 			+ "&f17=(a=a,b=1,c=true)&f17=(a=a,b=1,c=true)"
446 			+ "&f18=(a=a,b=1,c=true)&f18=(a=a,b=1,c=true)"
447 			+ "&f19=@((a=a,b=1,c=true))&f19=@((a=a,b=1,c=true))"
448 			+ "&f20=@((a=a,b=1,c=true))&f20=@((a=a,b=1,c=true))";
449 		assertEquals(e, r);
450 
451 		s2 = UrlEncodingSerializer.create().expandedParams().build();
452 		r = s2.serialize(t);
453 		e = """
454 			f01=a&f01=b\
455 			&f02=c&f02=d\
456 			&f03=1&f03=2\
457 			&f04=3&f04=4\
458 			&f05=@(e,f)&f05=@(g,h)\
459 			&f06=@(i,j)&f06=@(k,l)\
460 			&f07=(a=a,b=1,c=true)&f07=(a=a,b=1,c=true)\
461 			&f08=(a=a,b=1,c=true)&f08=(a=a,b=1,c=true)\
462 			&f09=@((a=a,b=1,c=true))&f09=@((a=a,b=1,c=true))\
463 			&f10=@((a=a,b=1,c=true))&f10=@((a=a,b=1,c=true))\
464 			&f11=a&f11=b\
465 			&f12=c&f12=d\
466 			&f13=1&f13=2\
467 			&f14=3&f14=4\
468 			&f15=@(e,f)&f15=@(g,h)\
469 			&f16=@(i,j)&f16=@(k,l)\
470 			&f17=(a=a,b=1,c=true)&f17=(a=a,b=1,c=true)\
471 			&f18=(a=a,b=1,c=true)&f18=(a=a,b=1,c=true)\
472 			&f19=@((a=a,b=1,c=true))&f19=@((a=a,b=1,c=true))\
473 			&f20=@((a=a,b=1,c=true))&f20=@((a=a,b=1,c=true))""";
474 		assertEquals(e, r);
475 	}
476 
477 	@Test void a06_multiPartParametersOnBeansViaAnnotationOnClass_usingConfig() throws Exception {
478 		var t = DTOs2.C.create();
479 		var s2 = UrlEncodingSerializer.DEFAULT.copy().applyAnnotations(DTOs2.Annotations.class).build();
480 		var r = s2.serialize(t);
481 
482 		var e = ""
483 			+ "f01=a&f01=b"
484 			+ "&f02=c&f02=d"
485 			+ "&f03=1&f03=2"
486 			+ "&f04=3&f04=4"
487 			+ "&f05=@(e,f)&f05=@(g,h)"
488 			+ "&f06=@(i,j)&f06=@(k,l)"
489 			+ "&f07=(a=a,b=1,c=true)&f07=(a=a,b=1,c=true)"
490 			+ "&f08=(a=a,b=1,c=true)&f08=(a=a,b=1,c=true)"
491 			+ "&f09=@((a=a,b=1,c=true))&f09=@((a=a,b=1,c=true))"
492 			+ "&f10=@((a=a,b=1,c=true))&f10=@((a=a,b=1,c=true))"
493 			+ "&f11=a&f11=b"
494 			+ "&f12=c&f12=d"
495 			+ "&f13=1&f13=2"
496 			+ "&f14=3&f14=4"
497 			+ "&f15=@(e,f)&f15=@(g,h)"
498 			+ "&f16=@(i,j)&f16=@(k,l)"
499 			+ "&f17=(a=a,b=1,c=true)&f17=(a=a,b=1,c=true)"
500 			+ "&f18=(a=a,b=1,c=true)&f18=(a=a,b=1,c=true)"
501 			+ "&f19=@((a=a,b=1,c=true))&f19=@((a=a,b=1,c=true))"
502 			+ "&f20=@((a=a,b=1,c=true))&f20=@((a=a,b=1,c=true))";
503 		assertEquals(e, r);
504 
505 		s2 = UrlEncodingSerializer.create().expandedParams().applyAnnotations(DTOs2.Annotations.class).build();
506 		r = s2.serialize(t);
507 		e = """
508 			f01=a&f01=b\
509 			&f02=c&f02=d\
510 			&f03=1&f03=2\
511 			&f04=3&f04=4\
512 			&f05=@(e,f)&f05=@(g,h)\
513 			&f06=@(i,j)&f06=@(k,l)\
514 			&f07=(a=a,b=1,c=true)&f07=(a=a,b=1,c=true)\
515 			&f08=(a=a,b=1,c=true)&f08=(a=a,b=1,c=true)\
516 			&f09=@((a=a,b=1,c=true))&f09=@((a=a,b=1,c=true))\
517 			&f10=@((a=a,b=1,c=true))&f10=@((a=a,b=1,c=true))\
518 			&f11=a&f11=b\
519 			&f12=c&f12=d\
520 			&f13=1&f13=2\
521 			&f14=3&f14=4\
522 			&f15=@(e,f)&f15=@(g,h)\
523 			&f16=@(i,j)&f16=@(k,l)\
524 			&f17=(a=a,b=1,c=true)&f17=(a=a,b=1,c=true)\
525 			&f18=(a=a,b=1,c=true)&f18=(a=a,b=1,c=true)\
526 			&f19=@((a=a,b=1,c=true))&f19=@((a=a,b=1,c=true))\
527 			&f20=@((a=a,b=1,c=true))&f20=@((a=a,b=1,c=true))""";
528 		assertEquals(e, r);
529 	}
530 
531 	@Test void a07_multiPartParametersOnMapOfStringArrays() throws Exception {
532 		var t = map();
533 		t.put("f1", a("bar"));
534 		t.put("f2", a("bar","baz"));
535 		t.put("f3", a());
536 		var s2 = UrlEncodingSerializer.DEFAULT_EXPANDED;
537 		var r = s2.serialize(t);
538 		var e = "f1=bar&f2=bar&f2=baz";
539 		assertEquals(e, r);
540 	}
541 
542 	//====================================================================================================
543 	// Test URLENC_paramFormat == PLAINTEXT.
544 	//====================================================================================================
545 	@Test void a08_plainTextParams() throws Exception {
546 		var s2 = UrlEncodingSerializer.DEFAULT.copy().paramFormatPlain().build();
547 
548 		assertEquals("_value=foo", s2.serialize("foo"));
549 		assertEquals("_value='foo'", s2.serialize("'foo'"));
550 		assertEquals("_value=(foo)", s2.serialize("(foo)"));
551 		assertEquals("_value=@(foo)", s2.serialize("@(foo)"));
552 
553 		var m = mapb(String.class,Object.class).add("foo","foo").add("'foo'","'foo'").add("(foo)","(foo)").add("@(foo)","@(foo)").build();
554 		assertEquals("foo=foo&'foo'='foo'&(foo)=(foo)&@(foo)=@(foo)", s2.serialize(m));
555 
556 		var l = l("foo", "'foo'", "(foo)", "@(foo)");
557 		assertEquals("0=foo&1='foo'&2=(foo)&3=@(foo)", s2.serialize(l));
558 
559 		var a = new A();
560 		assertEquals("'foo'='foo'&(foo)=(foo)&@(foo)=@(foo)&foo=foo", s2.serialize(a));
561 	}
562 
563 	@Bean(sort=true)
564 	public static class A {
565 
566 		@Beanp(name="foo")
567 		public String f1 = "foo";
568 
569 		@Beanp(name="'foo'")
570 		public String f2 = "'foo'";
571 
572 		@Beanp(name="(foo)")
573 		public String f3 = "(foo)";
574 
575 		@Beanp(name="@(foo)")
576 		public String f4 = "@(foo)";
577 	}
578 }