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