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