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.rest.annotation;
18  
19  import static org.apache.juneau.TestUtils.*;
20  import static org.apache.juneau.common.utils.IOUtils.*;
21  import static org.apache.juneau.http.header.ContentType.*;
22  import static org.junit.jupiter.api.Assertions.*;
23  
24  import java.io.*;
25  import java.util.*;
26  
27  import org.apache.juneau.*;
28  import org.apache.juneau.annotation.*;
29  import org.apache.juneau.http.annotation.*;
30  import org.apache.juneau.json.*;
31  import org.apache.juneau.marshaller.*;
32  import org.apache.juneau.rest.mock.*;
33  import org.apache.juneau.testutils.pojos.*;
34  import org.apache.juneau.uon.*;
35  import org.apache.juneau.urlencoding.*;
36  import org.apache.juneau.urlencoding.annotation.*;
37  import org.apache.juneau.urlencoding.annotation.UrlEncoding;
38  import org.junit.jupiter.api.*;
39  
40  class Content_Test extends TestBase {
41  
42  	//------------------------------------------------------------------------------------------------------------------
43  	// @Body on parameter
44  	//------------------------------------------------------------------------------------------------------------------
45  
46  	@Rest(serializers=Json5Serializer.class, parsers=JsonParser.class, defaultAccept="text/json")
47  	public static class A {
48  		@RestPut(path="/String")
49  		public String a(@Content String b) {
50  			return b;
51  		}
52  		@RestPut(path="/Integer")
53  		public Integer b(@Content Integer b) {
54  			return b;
55  		}
56  		@RestPut(path="/int")
57  		public Integer c(@Content int b) {
58  			return b;
59  		}
60  		@RestPut(path="/Boolean")
61  		public Boolean d(@Content Boolean b) {
62  			return b;
63  		}
64  		@RestPut(path="/boolean")
65  		public Boolean e(@Content boolean b) {
66  			return b;
67  		}
68  		@RestPut(path="/float")
69  		public float f(@Content float f) {
70  			return f;
71  		}
72  		@RestPut(path="/Float")
73  		public Float g(@Content Float f) {
74  			return f;
75  		}
76  		@RestPut(path="/Map")
77  		public TreeMap<String,Integer> h(@Content TreeMap<String,Integer> m) {
78  			return m;
79  		}
80  		@RestPut(path="/enum")
81  		public TestEnum i(@Content TestEnum e) {
82  			return e;
83  		}
84  		public static class A11 {
85  			public String f1;
86  		}
87  		@RestPut(path="/Bean")
88  		public A11 j(@Content A11 b) {
89  			return b;
90  		}
91  		@RestPut(path="/InputStream")
92  		public String k(@Content InputStream b) throws Exception {
93  			return read(b);
94  		}
95  		@RestPut(path="/Reader")
96  		public String l(@Content Reader b) throws Exception {
97  			return read(b);
98  		}
99  		@RestPut(path="/InputStreamTransform")
100 		public A14 m(@Content A14 b) {
101 			return b;
102 		}
103 		public static class A14 {
104 			String s;
105 			public A14(InputStream in) throws Exception { this.s = read(in); }
106 			@Override public String toString() { return s; }
107 		}
108 		@RestPut(path="/ReaderTransform")
109 		public A15 n(@Content A15 b) {
110 			return b;
111 		}
112 		public static class A15 {
113 			private String s;
114 			public A15(Reader in) throws Exception { this.s = read(in); }
115 			@Override public String toString() { return s; }
116 		}
117 		@RestPut(path="/StringTransform")
118 		public A16 o(@Content A16 b) { return b; }
119 		public static class A16 {
120 			private String s;
121 			public A16(String s) { this.s = s; }
122 			@Override public String toString() { return s; }
123 		}
124 	}
125 
126 	@Test void a01_onParameters() throws Exception {
127 		var a = MockRestClient.buildLax(A.class);
128 
129 		a.put("/String", "foo")
130 			.json()
131 			.run()
132 			.assertContent("'foo'");
133 		// If no Content-Type specified, should be treated as plain-text.
134 		a.put("/String", "'foo'")
135 			.run()
136 			.assertContent("'\\'foo\\''");
137 		// If Content-Type not matched, should be treated as plain-text.
138 		a.put("/String", "'foo'").contentType("")
139 			.run()
140 			.assertContent("'\\'foo\\''");
141 		a.put("/String", "'foo'").contentType("text/plain")
142 			.run()
143 			.assertContent("'\\'foo\\''");
144 		a.put("/String?content=foo", null)
145 			.run()
146 			.assertContent("'foo'");
147 		a.put("/String?content=null", null)
148 			.run()
149 			.assertContent("null");
150 		a.put("/String?content=", null)
151 			.run()
152 			.assertContent("''");
153 
154 		a.put("/Integer", "123").json()
155 			.run()
156 			.assertContent("123");
157 		// Integer takes in a String arg, so it can be parsed without Content-Type.
158 		a.put("/Integer", "123")
159 			.run()
160 			.assertContent("123");
161 		a.put("/Integer?content=123", null)
162 			.run()
163 			.assertContent("123");
164 		a.put("/Integer?content=-123", null)
165 			.run()
166 			.assertContent("-123");
167 		a.put("/Integer?content=null", null)
168 			.run()
169 			.assertContent("null");
170 		a.put("/Integer?content=", null)
171 			.run()
172 			.assertContent("null");
173 		a.put("/Integer?content=bad&noTrace=true", null)
174 			.run()
175 			.assertStatus(400);
176 
177 		a.put("/int", "123").json()
178 			.run()
179 			.assertContent("123");
180 		a.put("/int", "123")
181 			.run()
182 			.assertContent("123"); // Uses part parser.
183 		a.put("/int?content=123", null)
184 			.run()
185 			.assertContent("123");
186 		a.put("/int?content=-123", null)
187 			.run()
188 			.assertContent("-123");
189 		a.put("/int?content=null", null)
190 			.run()
191 			.assertContent("0");
192 		a.put("/int?content=", null)
193 			.run()
194 			.assertContent("0");
195 		a.put("/int?content=bad&noTrace=true", null)
196 			.run()
197 			.assertStatus(400);
198 
199 		a.put("/Boolean", "true").json()
200 			.run()
201 			.assertContent("true");
202 		// Boolean takes in a String arg, so it can be parsed without Content-Type.
203 		a.put("/Boolean", "true")
204 			.run()
205 			.assertContent("true");
206 		a.put("/Boolean?content=true", null)
207 			.run()
208 			.assertContent("true");
209 		a.put("/Boolean?content=false", null)
210 			.run()
211 			.assertContent("false");
212 		a.put("/Boolean?content=null", null)
213 			.run()
214 			.assertContent("null");
215 		a.put("/Boolean?content=", null)
216 			.run()
217 			.assertContent("null");
218 		a.put("/Boolean?content=bad&noTrace=true", null)
219 			.run()
220 			.assertStatus(400);
221 
222 		a.put("/boolean", "true").json()
223 			.run()
224 			.assertContent("true");
225 		a.put("/boolean", "true")
226 			.run()
227 			.assertContent("true"); // Uses part parser.
228 		a.put("/boolean?content=true", null)
229 			.run()
230 			.assertContent("true");
231 		a.put("/boolean?content=false", null)
232 			.run()
233 			.assertContent("false");
234 		a.put("/boolean?content=null", null)
235 			.run()
236 			.assertContent("false");
237 		a.put("/boolean?content=", null)
238 			.run()
239 			.assertContent("false");
240 		a.put("/boolean?content=bad&noTrace=true", null)
241 			.run()
242 			.assertStatus(400);
243 
244 		a.put("/float", "1.23").json()
245 			.run()
246 			.assertContent("1.23");
247 		a.put("/float", "1.23")
248 			.run()
249 			.assertContent("1.23");  // Uses part parser.
250 		a.put("/float?content=1.23", null)
251 			.run()
252 			.assertContent("1.23");
253 		a.put("/float?content=-1.23", null)
254 			.run()
255 			.assertContent("-1.23");
256 		a.put("/float?content=null", null)
257 			.run()
258 			.assertContent("0.0");
259 		a.put("/float?content=", null)
260 			.run()
261 			.assertContent("0.0");
262 		a.put("/float?content=bad&noTrace=true", null)
263 			.run()
264 			.assertStatus(400);
265 
266 		a.put("/Float", "1.23").json()
267 			.run()
268 			.assertContent("1.23");
269 		// Float takes in a String arg, so it can be parsed without Content-Type.
270 		a.put("/Float", "1.23")
271 			.run()
272 			.assertContent("1.23");
273 		a.put("/Float?content=1.23", null)
274 			.run()
275 			.assertContent("1.23");
276 		a.put("/Float?content=-1.23", null)
277 			.run()
278 			.assertContent("-1.23");
279 		a.put("/Float?content=null", null)
280 			.run()
281 			.assertContent("null");
282 		a.put("/Float?content=", null)
283 			.run()
284 			.assertContent("null");
285 		a.put("/Float?content=bad&noTrace=true", null)
286 			.run()
287 			.assertStatus(400);
288 
289 		a.put("/Map", "{foo:123}", APPLICATION_JSON)
290 			.run()
291 			.assertContent("{foo:123}");
292 		a.put("/Map", "(foo=123)", TEXT_OPENAPI)
293 			.run()
294 			.assertStatus(415);
295 		a.put("/Map?content=(foo=123)", null)
296 			.run()
297 			.assertContent("{foo:123}");
298 		a.put("/Map?content=()", null)
299 			.run()
300 			.assertContent("{}");
301 		a.put("/Map?content=null", null)
302 			.run()
303 			.assertContent("null");
304 		a.put("/Map?content=", null)
305 			.run()
306 			.assertContent("null");
307 		a.put("/Map?content=bad&noTrace=true", null)
308 			.run()
309 			.assertStatus(400);
310 
311 		a.put("/enum", "'ONE'", APPLICATION_JSON)
312 			.run()
313 			.assertContent("'ONE'");
314 		a.put("/enum", "ONE")
315 			.run()
316 			.assertContent("'ONE'");
317 		a.put("/enum?content=ONE", null)
318 			.run()
319 			.assertContent("'ONE'");
320 		a.put("/enum?content=TWO", null)
321 			.run()
322 			.assertContent("'TWO'");
323 		a.put("/enum?content=null", null)
324 			.run()
325 			.assertContent("null");
326 		a.put("/enum?content=", null)
327 			.run()
328 			.assertContent("null");
329 		a.put("/enum?content=bad&noTrace=true", null)
330 			.run()
331 			.assertStatus(400);
332 
333 		a.put("/Bean", "{f1:'a'}", APPLICATION_JSON)
334 			.run()
335 			.assertContent("{f1:'a'}");
336 		a.put("/Bean", "(f1=a)", TEXT_OPENAPI)
337 			.run()
338 			.assertStatus(415);
339 		a.put("/Bean?content=(f1=a)", null)
340 			.run()
341 			.assertContent("{f1:'a'}");
342 		a.put("/Bean?content=()", null)
343 			.run()
344 			.assertContent("{}");
345 		a.put("/Bean?content=null", null)
346 			.run()
347 			.assertContent("null");
348 		a.put("/Bean?content=", null)
349 			.run()
350 			.assertContent("null");
351 		a.put("/Bean?content=bad&noTrace=true", null)
352 			.run()
353 			.assertStatus(400);
354 
355 		// Content-Type should always be ignored.
356 		a.put("/InputStream", "'a'", APPLICATION_JSON)
357 			.run()
358 			.assertContent("'\\'a\\''");
359 		a.put("/InputStream", "'a'")
360 			.run()
361 			.assertContent("'\\'a\\''");
362 		a.put("/InputStream?content=a", null)
363 			.run()
364 			.assertContent("'a'");
365 		a.put("/InputStream?content=null", null)
366 			.run()
367 			.assertContent("'null'");
368 		a.put("/InputStream?content=", null)
369 			.run()
370 			.assertContent("''");
371 
372 		// Content-Type should always be ignored.
373 		a.put("/Reader", "'a'", APPLICATION_JSON)
374 			.run()
375 			.assertContent("'\\'a\\''");
376 		a.put("/Reader", "'a'")
377 			.run()
378 			.assertContent("'\\'a\\''");
379 		a.put("/Reader?content=a", null)
380 			.run()
381 			.assertContent("'a'");
382 		a.put("/Reader?content=null", null)
383 			.run()
384 			.assertContent("'null'");
385 		a.put("/Reader?content=", null)
386 			.run()
387 			.assertContent("''");
388 
389 		// It's not currently possible to pass in a &body parameter for InputStream/Reader transforms.
390 
391 		// Input stream transform requests must not specify Content-Type or else gets resolved as POJO.
392 		a.put("/InputStreamTransform?noTrace=true", "'a'", APPLICATION_JSON)
393 			.run()
394 			.assertContent().isContains("Bad Request");
395 		a.put("/InputStreamTransform", "'a'")
396 			.run()
397 			.assertContent("'\\'a\\''");
398 
399 		// Reader transform requests must not specify Content-Type or else gets resolved as POJO.
400 		a.put("/ReaderTransform?noTrace=true", "'a'", APPLICATION_JSON)
401 			.run()
402 			.assertContent().isContains("Bad Request");
403 		a.put("/ReaderTransform", "'a'")
404 			.run()
405 			.assertContent("'\\'a\\''");
406 
407 		// When Content-Type specified and matched, treated as a parsed POJO.
408 		a.put("/StringTransform", "'a'", APPLICATION_JSON)
409 			.run()
410 			.assertContent("'a'");
411 		// When Content-Type not matched, treated as plain text.
412 		a.put("/StringTransform", "'a'")
413 			.run()
414 			.assertContent("'\\'a\\''");
415 	}
416 
417 	//------------------------------------------------------------------------------------------------------------------
418 	// @Body on POJO
419 	//------------------------------------------------------------------------------------------------------------------
420 
421 	@Rest(serializers=Json5Serializer.class, parsers=JsonParser.class, defaultAccept="application/json")
422 	public static class B {
423 		@RestPut(path="/StringTransform")
424 		public B1 a(B1 b) {
425 			return b;
426 		}
427 		@Content
428 		public static class B1 {
429 			private String val;
430 			public B1(String val) { this.val = val; }
431 			@Override public String toString() { return val; }
432 		}
433 		@RestPut(path="/Bean")
434 		public B2 b(B2 b) {
435 			return b;
436 		}
437 		@Content
438 		public static class B2 {
439 			public String f1;
440 		}
441 		@RestPut(path="/BeanList")
442 		public B3 c(B3 b) {
443 			return b;
444 		}
445 		@SuppressWarnings("serial")
446 		@Content
447 		public static class B3 extends LinkedList<B2> {}
448 		@RestPut(path="/InputStreamTransform")
449 		public B4 d(B4 b) {
450 			return b;
451 		}
452 		@Content
453 		public static class B4 {
454 			String s;
455 			public B4(InputStream in) throws Exception { this.s = read(in); }
456 			@Override public String toString() { return s; }
457 		}
458 		@RestPut(path="/ReaderTransform")
459 		public B5 e(B5 b) {
460 			return b;
461 		}
462 		@Content
463 		public static class B5 {
464 			private String s;
465 			public B5(Reader in) throws Exception { this.s = read(in); }
466 			@Override public String toString() { return s; }
467 		}
468 	}
469 
470 	@Test void b01_onPojos() throws Exception {
471 		var b = MockRestClient.buildLax(B.class);
472 		b.put("/StringTransform", "'foo'", APPLICATION_JSON)
473 			.run()
474 			.assertContent("'foo'");
475 		// When Content-Type not matched, treated as plain text.
476 		b.put("/StringTransform", "'foo'")
477 			.run()
478 			.assertContent("'\\'foo\\''");
479 		b.put("/Bean", "{f1:'a'}", APPLICATION_JSON)
480 			.run()
481 			.assertContent("{f1:'a'}");
482 		b.put("/Bean", "(f1=a)", TEXT_OPENAPI)
483 			.run()
484 			.assertStatus(415);
485 		b.put("/BeanList", "[{f1:'a'}]", APPLICATION_JSON)
486 			.run()
487 			.assertContent("[{f1:'a'}]");
488 		b.put("/BeanList", "(f1=a)", TEXT_OPENAPI)
489 			.run()
490 			.assertStatus(415);
491 		b.put("/InputStreamTransform", "a")
492 			.run()
493 			.assertContent("'a'");
494 		// When Content-Type matched, treated as parsed POJO.
495 		b.put("/InputStreamTransform?noTrace=true", "a", APPLICATION_JSON)
496 			.run()
497 			.assertContent().isContains("Bad Request");
498 		b.put("/ReaderTransform", "a")
499 			.run()
500 			.assertContent("'a'");
501 		// When Content-Type matched, treated as parsed POJO.
502 		b.put("/ReaderTransform?noTrace=true", "a", APPLICATION_JSON)
503 			.run()
504 			.assertContent().isContains("Bad Request");
505 	}
506 
507 	//------------------------------------------------------------------------------------------------------------------
508 	// No serializers or parsers needed when using only streams and readers.
509 	//------------------------------------------------------------------------------------------------------------------
510 
511 	@Rest
512 	public static class D {
513 		@RestPut(path="/String")
514 		public Reader a(@Content Reader b) {
515 			return b;
516 		}
517 		@RestPut(path="/InputStream")
518 		public InputStream b(@Content InputStream b) {
519 			return b;
520 		}
521 		@RestPut(path="/Reader")
522 		public Reader c(@Content Reader b) {
523 			return b;
524 		}
525 		@RestPut(path="/StringTransform")
526 		public Reader d(@Content D1 b) {
527 			return reader(b.toString());
528 		}
529 		public static class D1 {
530 			private String s;
531 			public D1(String in) { this.s = in; }
532 			@Override public String toString() { return s; }
533 		}
534 		@RestPut(path="/InputStreamTransform")
535 		public Reader e(@Content D2 b) {
536 			return reader(b.toString());
537 		}
538 		public static class D2 {
539 			String s;
540 			public D2(InputStream in) throws Exception { this.s = read(in); }
541 			@Override public String toString() { return s; }
542 		}
543 		@RestPut(path="/ReaderTransform")
544 		public Reader f(@Content D3 b) {
545 			return reader(b.toString());
546 		}
547 		public static class D3 {
548 			private String s;
549 			public D3(Reader in) throws Exception{ this.s = read(in); }
550 			@Override public String toString() { return s; }
551 		}
552 		@RestPut(path="/StringTransformBodyOnPojo")
553 		public Reader g(D4 b) {
554 			return reader(b.toString());
555 		}
556 		@Content
557 		public static class D4 {
558 			private String s;
559 			public D4(String in) { this.s = in; }
560 			@Override public String toString() { return s; }
561 		}
562 		@RestPut(path="/InputStreamTransformBodyOnPojo")
563 		public Reader h(D5 b) {
564 			return reader(b.toString());
565 		}
566 		@Content
567 		public static class D5 {
568 			String s;
569 			public D5(InputStream in) throws Exception { this.s = read(in); }
570 			@Override public String toString() { return s; }
571 		}
572 
573 		@RestPut(path="/ReaderTransformBodyOnPojo")
574 		public Reader i(D6 b) {
575 			return reader(b.toString());
576 		}
577 		@Content
578 		public static class D6 {
579 			private String s;
580 			public D6(Reader in) throws Exception{ this.s = read(in); }
581 			@Override public String toString() { return s; }
582 		}
583 	}
584 
585 	@Test void d01_noMediaTypesOnStreams() throws Exception {
586 		var d = MockRestClient.buildLax(D.class);
587 		d.put("/String", "a")
588 			.run()
589 			.assertContent("a");
590 		d.put("/String", "a", APPLICATION_JSON)
591 			.run()
592 			.assertContent("a");
593 		d.put("/InputStream", "a")
594 			.run()
595 			.assertContent("a");
596 		d.put("/InputStream", "a", APPLICATION_JSON)
597 			.run()
598 			.assertContent("a");
599 		d.put("/Reader", "a")
600 			.run()
601 			.assertContent("a");
602 		d.put("/Reader", "a", APPLICATION_JSON)
603 			.run()
604 			.assertContent("a");
605 		d.put("/StringTransform", "a")
606 			.run()
607 			.assertContent("a");
608 		d.put("/StringTransform?noTrace=true", "a", APPLICATION_JSON)
609 			.run()
610 			.assertStatus(415);
611 		d.put("/InputStreamTransform", "a")
612 			.run()
613 			.assertContent("a");
614 		d.put("/InputStreamTransform", "a", APPLICATION_JSON)
615 			.run()
616 			.assertContent("a");
617 		d.put("/ReaderTransform", "a")
618 			.run()
619 			.assertContent("a");
620 		d.put("/ReaderTransform", "a", APPLICATION_JSON)
621 			.run()
622 			.assertContent("a");
623 		d.put("/StringTransformBodyOnPojo", "a")
624 			.run()
625 			.assertContent("a");
626 		d.put("/StringTransformBodyOnPojo?noTrace=true", "a", APPLICATION_JSON)
627 			.run()
628 			.assertStatus(415);
629 		d.put("/InputStreamTransformBodyOnPojo", "a")
630 			.run()
631 			.assertContent("a");
632 		d.put("/InputStreamTransformBodyOnPojo", "a", APPLICATION_JSON)
633 			.run()
634 			.assertContent("a");
635 		d.put("/ReaderTransformBodyOnPojo", "a")
636 			.run()
637 			.assertContent("a");
638 		d.put("/ReaderTransformBodyOnPojo", "a", APPLICATION_JSON)
639 			.run()
640 			.assertContent("a");
641 	}
642 
643 	//------------------------------------------------------------------------------------------------------------------
644 	// Complex POJOs
645 	//------------------------------------------------------------------------------------------------------------------
646 
647 	@Rest(serializers=Json5Serializer.class, parsers=JsonParser.class, defaultAccept="application/json")
648 	public static class E {
649 		@RestPut(path="/B")
650 		public XBeans.XB a(@Content XBeans.XB b) {
651 			return b;
652 		}
653 		@RestPut(path="/C")
654 		public XBeans.XC b(@Content XBeans.XC c) {
655 			return c;
656 		}
657 	}
658 
659 	@Test void e01_complexPojos() throws Exception {
660 		var e = MockRestClient.build(E.class);
661 		var expected = "{f01:['a','b'],f02:['c','d'],f03:[1,2],f04:[3,4],f05:[['e','f'],['g','h']],f06:[['i','j'],['k','l']],f07:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f08:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f09:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f10:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f11:['a','b'],f12:['c','d'],f13:[1,2],f14:[3,4],f15:[['e','f'],['g','h']],f16:[['i','j'],['k','l']],f17:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f18:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f19:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f20:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]]}";
662 
663 		e.put("/B", Json5Serializer.DEFAULT.toString(XBeans.XB.INSTANCE), APPLICATION_JSON)
664 			.run()
665 			.assertContent(expected);
666 
667 		expected = "{f01:['a','b'],f02:['c','d'],f03:[1,2],f04:[3,4],f05:[['e','f'],['g','h']],f06:[['i','j'],['k','l']],f07:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f08:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f09:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f10:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f11:['a','b'],f12:['c','d'],f13:[1,2],f14:[3,4],f15:[['e','f'],['g','h']],f16:[['i','j'],['k','l']],f17:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f18:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f19:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f20:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]]}";
668 		e.put("/B?content=" + UonSerializer.DEFAULT.serialize(XBeans.XB.INSTANCE), "a")
669 			.run()
670 			.assertContent(expected);
671 
672 		expected = "{f01:['a','b'],f02:['c','d'],f03:[1,2],f04:[3,4],f05:[['e','f'],['g','h']],f06:[['i','j'],['k','l']],f07:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f08:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f09:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f10:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f11:['a','b'],f12:['c','d'],f13:[1,2],f14:[3,4],f15:[['e','f'],['g','h']],f16:[['i','j'],['k','l']],f17:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f18:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f19:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f20:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]]}";
673 		e.put("/C", Json5Serializer.DEFAULT.toString(XBeans.XB.INSTANCE), APPLICATION_JSON)
674 			.run()
675 			.assertContent(expected);
676 
677 		expected = "{f01:['a','b'],f02:['c','d'],f03:[1,2],f04:[3,4],f05:[['e','f'],['g','h']],f06:[['i','j'],['k','l']],f07:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f08:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f09:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f10:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f11:['a','b'],f12:['c','d'],f13:[1,2],f14:[3,4],f15:[['e','f'],['g','h']],f16:[['i','j'],['k','l']],f17:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f18:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f19:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f20:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]]}";
678 		e.put("/C?content=" + UonSerializer.DEFAULT.serialize(XBeans.XB.INSTANCE), "a")
679 			.run()
680 			.assertContent(expected);
681 	}
682 
683 	@Rest(serializers=Json5Serializer.class, parsers=JsonParser.class, defaultAccept="application/json")
684 	@Bean(on="A,B,C",sort=true)
685 	@UrlEncoding(on="C",expandedParams=true)
686 	public static class E2 {
687 		@RestPut(path="/B")
688 		public XBeans.XE a(@Content XBeans.XE b) {
689 			return b;
690 		}
691 		@RestPut(path="/C")
692 		public XBeans.XF b(@Content XBeans.XF c) {
693 			return c;
694 		}
695 	}
696 
697 	@Test void e02_complexPojos() throws Exception {
698 		var e2 = MockRestClient.build(E2.class);
699 		var expected = "{f01:['a','b'],f02:['c','d'],f03:[1,2],f04:[3,4],f05:[['e','f'],['g','h']],f06:[['i','j'],['k','l']],f07:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f08:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f09:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f10:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f11:['a','b'],f12:['c','d'],f13:[1,2],f14:[3,4],f15:[['e','f'],['g','h']],f16:[['i','j'],['k','l']],f17:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f18:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f19:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f20:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]]}";
700 
701 		e2.put("/B", Json5Serializer.DEFAULT.copy().applyAnnotations(XBeans.Annotations.class).build().toString(XBeans.XE.INSTANCE), APPLICATION_JSON)
702 			.run()
703 			.assertContent(expected);
704 
705 		expected = "{f01:['a','b'],f02:['c','d'],f03:[1,2],f04:[3,4],f05:[['e','f'],['g','h']],f06:[['i','j'],['k','l']],f07:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f08:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f09:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f10:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f11:['a','b'],f12:['c','d'],f13:[1,2],f14:[3,4],f15:[['e','f'],['g','h']],f16:[['i','j'],['k','l']],f17:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f18:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f19:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f20:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]]}";
706 		e2.put("/B?content=" + UonSerializer.DEFAULT.copy().applyAnnotations(XBeans.Annotations.class).build().serialize(XBeans.XE.INSTANCE), "a")
707 			.run()
708 			.assertContent(expected);
709 
710 		expected = "{f01:['a','b'],f02:['c','d'],f03:[1,2],f04:[3,4],f05:[['e','f'],['g','h']],f06:[['i','j'],['k','l']],f07:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f08:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f09:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f10:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f11:['a','b'],f12:['c','d'],f13:[1,2],f14:[3,4],f15:[['e','f'],['g','h']],f16:[['i','j'],['k','l']],f17:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f18:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f19:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f20:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]]}";
711 		e2.put("/C", Json5Serializer.DEFAULT.copy().applyAnnotations(XBeans.Annotations.class).build().toString(XBeans.XE.INSTANCE), APPLICATION_JSON)
712 			.run()
713 			.assertContent(expected);
714 
715 		expected = "{f01:['a','b'],f02:['c','d'],f03:[1,2],f04:[3,4],f05:[['e','f'],['g','h']],f06:[['i','j'],['k','l']],f07:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f08:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f09:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f10:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f11:['a','b'],f12:['c','d'],f13:[1,2],f14:[3,4],f15:[['e','f'],['g','h']],f16:[['i','j'],['k','l']],f17:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f18:[{a:'a',b:1,c:true},{a:'a',b:1,c:true}],f19:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]],f20:[[{a:'a',b:1,c:true}],[{a:'a',b:1,c:true}]]}";
716 		e2.put("/C?content=" + UonSerializer.DEFAULT.copy().applyAnnotations(XBeans.Annotations.class).build().serialize(XBeans.XE.INSTANCE), "a")
717 			.run()
718 			.assertContent(expected);
719 	}
720 
721 	//------------------------------------------------------------------------------------------------------------------
722 	// Form POSTS with @Body parameter
723 	//------------------------------------------------------------------------------------------------------------------
724 
725 	@Rest(serializers=JsonSerializer.class,parsers=JsonParser.class)
726 	public static class F {
727 		@RestPost(path="/*")
728 		public Reader a(
729 				@Content F1 bean,
730 				@HasQuery("p1") boolean hqp1, @HasQuery("p2") boolean hqp2,
731 				@Query("p1") String qp1, @Query("p2") int qp2) {
732 			return reader("bean=["+Json5Serializer.DEFAULT.toString(bean)+"],qp1=["+qp1+"],qp2=["+qp2+"],hqp1=["+hqp1+"],hqp2=["+hqp2+"]");
733 		}
734 		public static class F1 {
735 			public String p1;
736 			public int p2;
737 		}
738 	}
739 
740 	@Test void f01_formPostAsContent() throws Exception {
741 		var f = MockRestClient.build(F.class);
742 		f.post("/", "{p1:'p1',p2:2}", APPLICATION_JSON)
743 			.run()
744 			.assertContent("bean=[{p1:'p1',p2:2}],qp1=[null],qp2=[0],hqp1=[false],hqp2=[false]");
745 		f.post("/", "{}", APPLICATION_JSON)
746 			.run()
747 			.assertContent("bean=[{p2:0}],qp1=[null],qp2=[0],hqp1=[false],hqp2=[false]");
748 		f.post("?p1=p3&p2=4", "{p1:'p1',p2:2}", APPLICATION_JSON)
749 			.run()
750 			.assertContent("bean=[{p1:'p1',p2:2}],qp1=[p3],qp2=[4],hqp1=[true],hqp2=[true]");
751 		f.post("?p1=p3&p2=4", "{}", APPLICATION_JSON)
752 			.run()
753 			.assertContent("bean=[{p2:0}],qp1=[p3],qp2=[4],hqp1=[true],hqp2=[true]");
754 	}
755 
756 	//------------------------------------------------------------------------------------------------------------------
757 	// Test multi-part parameter keys on bean properties of type array/Collection (i.e. &key=val1,&key=val2)
758 	// using @UrlEncoding(expandedParams=true) annotation on bean.
759 	// A simple round-trip test to verify that both serializing and parsing works.
760 	//------------------------------------------------------------------------------------------------------------------
761 
762 	@Rest(serializers=UrlEncodingSerializer.class,parsers=UrlEncodingParser.class)
763 	public static class G {
764 		@RestPost(path="/")
765 		public XBeans.XC a(@Content XBeans.XC content) {
766 			return content;
767 		}
768 	}
769 
770 	@Test void g01_multiPartParameterKeysOnCollections() throws Exception {
771 		var g = MockRestClient.build(G.class);
772 		var in = ""
773 			+ "f01=a&f01=b"
774 			+ "&f02=c&f02=d"
775 			+ "&f03=1&f03=2"
776 			+ "&f04=3&f04=4"
777 			+ "&f05=@(e,f)&f05=@(g,h)"
778 			+ "&f06=@(i,j)&f06=@(k,l)"
779 			+ "&f07=(a=a,b=1,c=true)&f07=(a=b,b=2,c=false)"
780 			+ "&f08=(a=a,b=1,c=true)&f08=(a=b,b=2,c=false)"
781 			+ "&f09=@((a=a,b=1,c=true))&f09=@((a=b,b=2,c=false))"
782 			+ "&f10=@((a=a,b=1,c=true))&f10=@((a=b,b=2,c=false))"
783 			+ "&f11=a&f11=b"
784 			+ "&f12=c&f12=d"
785 			+ "&f13=1&f13=2"
786 			+ "&f14=3&f14=4"
787 			+ "&f15=@(e,f)&f15=@(g,h)"
788 			+ "&f16=@(i,j)&f16=@(k,l)"
789 			+ "&f17=(a=a,b=1,c=true)&f17=(a=b,b=2,c=false)"
790 			+ "&f18=(a=a,b=1,c=true)&f18=(a=b,b=2,c=false)"
791 			+ "&f19=@((a=a,b=1,c=true))&f19=@((a=b,b=2,c=false))"
792 			+ "&f20=@((a=a,b=1,c=true))&f20=@((a=b,b=2,c=false))";
793 		g.post("/", in, APPLICATION_FORM_URLENCODED)
794 			.run()
795 			.assertContent(in);
796 	}
797 
798 	//------------------------------------------------------------------------------------------------------------------
799 	// Test multi-part parameter keys on bean properties of type array/Collection (i.e. &key=val1,&key=val2)
800 	// using URLENC_expandedParams property.
801 	// A simple round-trip test to verify that both serializing and parsing works.
802 	//------------------------------------------------------------------------------------------------------------------
803 
804 	@Rest(serializers=UrlEncodingSerializer.class,parsers=UrlEncodingParser.class)
805 	public static class H {
806 		@RestPost(path="/")
807 		@UrlEncodingConfig(expandedParams="true")
808 		public XBeans.XB a(@Content XBeans.XB content) {
809 			return content;
810 		}
811 	}
812 
813 	@Test void h01_multiPartParameterKeysOnCollections_usingExpandedParams() throws Exception {
814 		var h = MockRestClient.build(H.class);
815 		var in = ""
816 			+ "f01=a&f01=b"
817 			+ "&f02=c&f02=d"
818 			+ "&f03=1&f03=2"
819 			+ "&f04=3&f04=4"
820 			+ "&f05=@(e,f)&f05=@(g,h)"
821 			+ "&f06=@(i,j)&f06=@(k,l)"
822 			+ "&f07=(a=a,b=1,c=true)&f07=(a=b,b=2,c=false)"
823 			+ "&f08=(a=a,b=1,c=true)&f08=(a=b,b=2,c=false)"
824 			+ "&f09=@((a=a,b=1,c=true))&f09=@((a=b,b=2,c=false))"
825 			+ "&f10=@((a=a,b=1,c=true))&f10=@((a=b,b=2,c=false))"
826 			+ "&f11=a&f11=b"
827 			+ "&f12=c&f12=d"
828 			+ "&f13=1&f13=2"
829 			+ "&f14=3&f14=4"
830 			+ "&f15=@(e,f)&f15=@(g,h)"
831 			+ "&f16=@(i,j)&f16=@(k,l)"
832 			+ "&f17=(a=a,b=1,c=true)&f17=(a=b,b=2,c=false)"
833 			+ "&f18=(a=a,b=1,c=true)&f18=(a=b,b=2,c=false)"
834 			+ "&f19=@((a=a,b=1,c=true))&f19=@((a=b,b=2,c=false))"
835 			+ "&f20=@((a=a,b=1,c=true))&f20=@((a=b,b=2,c=false))";
836 		h.post("/", in, APPLICATION_FORM_URLENCODED)
837 			.run()
838 			.assertContent(in);
839 	}
840 
841 	@Rest(serializers=UrlEncodingSerializer.class,parsers=UrlEncodingParser.class)
842 	@Bean(on="A,B,C",sort=true)
843 	@UrlEncoding(on="C",expandedParams=true)
844 	public static class H2 {
845 		@RestPost(path="/")
846 		@UrlEncodingConfig(expandedParams="true")
847 		public XBeans.XE a(@Content XBeans.XE content) {
848 			return content;
849 		}
850 	}
851 
852 	@Test void h02_multiPartParameterKeysOnCollections_usingExpandedParams() throws Exception {
853 		var h2 = MockRestClient.build(H2.class);
854 		var in = ""
855 			+ "f01=a&f01=b"
856 			+ "&f02=c&f02=d"
857 			+ "&f03=1&f03=2"
858 			+ "&f04=3&f04=4"
859 			+ "&f05=@(e,f)&f05=@(g,h)"
860 			+ "&f06=@(i,j)&f06=@(k,l)"
861 			+ "&f07=(a=a,b=1,c=true)&f07=(a=b,b=2,c=false)"
862 			+ "&f08=(a=a,b=1,c=true)&f08=(a=b,b=2,c=false)"
863 			+ "&f09=@((a=a,b=1,c=true))&f09=@((a=b,b=2,c=false))"
864 			+ "&f10=@((a=a,b=1,c=true))&f10=@((a=b,b=2,c=false))"
865 			+ "&f11=a&f11=b"
866 			+ "&f12=c&f12=d"
867 			+ "&f13=1&f13=2"
868 			+ "&f14=3&f14=4"
869 			+ "&f15=@(e,f)&f15=@(g,h)"
870 			+ "&f16=@(i,j)&f16=@(k,l)"
871 			+ "&f17=(a=a,b=1,c=true)&f17=(a=b,b=2,c=false)"
872 			+ "&f18=(a=a,b=1,c=true)&f18=(a=b,b=2,c=false)"
873 			+ "&f19=@((a=a,b=1,c=true))&f19=@((a=b,b=2,c=false))"
874 			+ "&f20=@((a=a,b=1,c=true))&f20=@((a=b,b=2,c=false))";
875 		h2.post("/", in, APPLICATION_FORM_URLENCODED)
876 			.run()
877 			.assertContent(in);
878 	}
879 
880 	//------------------------------------------------------------------------------------------------------------------
881 	// Test behavior of @Body(required=true).
882 	//------------------------------------------------------------------------------------------------------------------
883 
884 	@Rest(serializers=JsonSerializer.class,parsers=JsonParser.class)
885 	public static class I {
886 		@RestPost
887 		public XBeans.XB a(@Content @Schema(r=true) XBeans.XB content) {
888 			return content;
889 		}
890 		@RestPost
891 		@Bean(on="A,B,C",sort=true)
892 		@UrlEncoding(on="C",expandedParams=true)
893 		public XBeans.XE b(@Content @Schema(r=true) XBeans.XE content) {
894 			return content;
895 		}
896 	}
897 
898 	@Test void i01_required() throws Exception {
899 		var i = MockRestClient.buildLax(I.class);
900 
901 		i.post("/a", "", APPLICATION_JSON)
902 			.run()
903 			.assertStatus(400)
904 			.assertContent().isContains("Required value not provided.");
905 		i.post("/a", "{}", APPLICATION_JSON)
906 			.run()
907 			.assertStatus(200);
908 
909 		i.post("/b", "", APPLICATION_JSON)
910 			.run()
911 			.assertStatus(400)
912 			.assertContent().isContains("Required value not provided.");
913 		i.post("/b", "{}", APPLICATION_JSON)
914 			.run()
915 			.assertStatus(200);
916 	}
917 
918 	//------------------------------------------------------------------------------------------------------------------
919 	// Optional body parameter.
920 	//------------------------------------------------------------------------------------------------------------------
921 
922 	@Rest(serializers=Json5Serializer.class,parsers=JsonParser.class)
923 	public static class J {
924 		@RestPost
925 		public Object a(@Content Optional<Integer> body) {
926 			assertNotNull(body);
927 			return body;
928 		}
929 		@RestPost
930 		public Object b(@Content Optional<ABean> body) {
931 			assertNotNull(body);
932 			return body;
933 		}
934 		@RestPost
935 		public Object c(@Content Optional<List<ABean>> body) {
936 			assertNotNull(body);
937 			return body;
938 		}
939 		@RestPost
940 		public Object d(@Content List<Optional<ABean>> body) {
941 			return body;
942 		}
943 	}
944 
945 	@Test void j01_optionalParams() throws Exception {
946 		var j = MockRestClient.buildJson(J.class);
947 		j.post("/a", 123)
948 			.run()
949 			.assertStatus(200)
950 			.assertContent("123");
951 		j.post("/a", null)
952 			.run()
953 			.assertStatus(200)
954 			.assertContent("null");
955 
956 		j.post("/b", ABean.get())
957 			.run()
958 			.assertStatus(200)
959 			.assertContent("{a:1,b:'foo'}");
960 		j.post("/b", null)
961 			.run()
962 			.assertStatus(200)
963 			.assertContent("null");
964 
965 		var body1 = Json5.of(list(ABean.get()));
966 		j.post("/c", body1, APPLICATION_JSON)
967 			.run()
968 			.assertStatus(200)
969 			.assertContent("[{a:1,b:'foo'}]");
970 		j.post("/c", null)
971 			.run()
972 			.assertStatus(200)
973 			.assertContent("null");
974 
975 		var body2 = Json5.of(list(opt(ABean.get())));
976 		j.post("/d", body2, APPLICATION_JSON)
977 			.run()
978 			.assertStatus(200)
979 			.assertContent("[{a:1,b:'foo'}]");
980 		j.post("/d", null)
981 			.run()
982 			.assertStatus(200)
983 			.assertContent("null");
984 	}
985 }