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.http.HttpMethod.*;
20  import static org.junit.jupiter.api.Assertions.*;
21  
22  import java.util.*;
23  
24  import org.apache.http.client.config.*;
25  import org.apache.juneau.*;
26  import org.apache.juneau.annotation.*;
27  import org.apache.juneau.collections.*;
28  import org.apache.juneau.commons.utils.*;
29  import org.apache.juneau.http.annotation.*;
30  import org.apache.juneau.json.*;
31  import org.apache.juneau.rest.*;
32  import org.apache.juneau.rest.httppart.*;
33  import org.apache.juneau.rest.mock.*;
34  import org.apache.juneau.testutils.pojos.*;
35  import org.junit.jupiter.api.*;
36  
37  class Path_Test extends TestBase {
38  
39  	//------------------------------------------------------------------------------------------------------------------
40  	// Basic tests
41  	//------------------------------------------------------------------------------------------------------------------
42  
43  	@Rest
44  	public static class A  {
45  		@RestGet(path="/")
46  		public void a(RestResponse res) {
47  			res.setContent(GET);
48  		}
49  		@RestGet(path="/a")
50  		public String b() {
51  			return "GET /a";
52  		}
53  		@RestGet(path="/a/{foo}")
54  		public String c(RestResponse res, @Path("foo") String foo) {
55  			return "GET /a " + foo;
56  		}
57  		@RestGet(path="/a/{foo}/{bar}")
58  		public String d(RestResponse res, @Path("foo") String foo, @Path("bar") String bar) {
59  			return "GET /a " + foo + "," + bar;
60  		}
61  		@RestGet(path="/a/{foo}/{bar}/*")
62  		public String e(@Path("foo") String foo, @Path("bar") int bar, @Path("/*") String remainder) {
63  			return "GET /a "+foo+","+bar+",r="+remainder;
64  		}
65  	}
66  
67  	@Test void a01_basic() throws Exception {
68  		var a = MockRestClient.buildLax(A.class);
69  
70  		a.get("/bad?noTrace=true")
71  			.run()
72  			.assertStatus(404);
73  
74  		a.get(null)
75  			.run()
76  			.assertContent("GET");
77  		a.get()
78  			.run()
79  			.assertContent("GET");
80  
81  		a.get("/a")
82  			.run()
83  			.assertContent("GET /a");
84  
85  		a.get("/a/foo")
86  			.run()
87  			.assertContent("GET /a foo");
88  
89  		a.get("/a/foo/bar")
90  			.run()
91  			.assertContent("GET /a foo,bar");
92  
93  		a.get("/a/foo/123/baz")
94  			.run()
95  			.assertContent("GET /a foo,123,r=baz");
96  
97  		// URL-encoded part should not get decoded before finding method to invoke.
98  		// This should match /get1/{foo} and not /get1/{foo}/{bar} - NOSONAR
99  		// NOTE:  When testing on Tomcat, must specify the following system property:
100 		// -Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true
101 		a.get("/a/x%2Fy")
102 			.run()
103 			.assertContent("GET /a x/y");
104 		a.get("/a/x%2Fy/x%2Fy")
105 			.run()
106 			.assertContent("GET /a x/y,x/y");
107 	}
108 
109 	//------------------------------------------------------------------------------------------------------------------
110 	// Primitives
111 	//------------------------------------------------------------------------------------------------------------------
112 
113 	@Rest
114 	public static class B  {
115 		@RestGet(path="/a/{x}/foo")
116 		public String a(@Path("x") int x) {
117 			return String.valueOf(x);
118 		}
119 		@RestGet(path="/b/{x}/foo")
120 		public String b(@Path("x") short x) {
121 			return String.valueOf(x);
122 		}
123 		@RestGet(path="/c/{x}/foo")
124 		public String c(@Path("x") long x) {
125 			return String.valueOf(x);
126 		}
127 		@RestGet(path="/d/{x}/foo")
128 		public String d(@Path("x") char x) {
129 			return String.valueOf(x);
130 		}
131 		@RestGet(path="/e/{x}/foo")
132 		public String e(@Path("x") float x) {
133 			return String.valueOf(x);
134 		}
135 		@RestGet(path="/f/{x}/foo")
136 		public String f(@Path("x") double x) {
137 			return String.valueOf(x);
138 		}
139 		@RestGet(path="/g/{x}/foo")
140 		public String g(@Path("x") byte x) {
141 			return String.valueOf(x);
142 		}
143 		@RestGet(path="/h/{x}/foo")
144 		public String h(@Path("x") boolean x) {
145 			return String.valueOf(x);
146 		}
147 	}
148 
149 	@Test void b01_primitives() throws Exception {
150 		var b = MockRestClient.buildLax(B.class);
151 
152 		b.get("/a/123/foo")
153 			.run()
154 			.assertContent("123");
155 		b.get("/a/bad/foo?noTrace=true")
156 			.run()
157 			.assertStatus(400);
158 
159 		b.get("/b/123/foo")
160 			.run()
161 			.assertContent("123");
162 		b.get("/b/bad/foo?noTrace=true")
163 			.run()
164 			.assertStatus(400);
165 
166 		b.get("/c/123/foo")
167 			.run()
168 			.assertContent("123");
169 		b.get("/c/bad/foo?noTrace=true")
170 			.run()
171 			.assertStatus(400);
172 
173 		b.get("/d/c/foo")
174 			.run()
175 			.assertContent("c");
176 		b.get("/d/bad/foo?noTrace=true")
177 			.run()
178 			.assertStatus(400);
179 
180 		b.get("/e/1.23/foo")
181 			.run()
182 			.assertContent("1.23");
183 		b.get("/e/bad/foo?noTrace=true")
184 			.run()
185 			.assertStatus(400);
186 
187 		b.get("/f/1.23/foo")
188 			.run()
189 			.assertContent("1.23");
190 		b.get("/f/bad/foo?noTrace=true")
191 			.run()
192 			.assertStatus(400);
193 
194 		b.get("/g/123/foo")
195 			.run()
196 			.assertContent("123");
197 		b.get("/g/bad/foo?noTrace=true")
198 			.run()
199 			.assertStatus(400);
200 
201 		b.get("/h/true/foo")
202 			.run()
203 			.assertContent("true");
204 		b.get("/h/bad/foo?noTrace=true")
205 			.run()
206 			.assertStatus(400);
207 	}
208 
209 	//------------------------------------------------------------------------------------------------------------------
210 	// Primitive objects
211 	//------------------------------------------------------------------------------------------------------------------
212 
213 	@Rest
214 	public static class C  {
215 		@RestGet(path="/a/{x}/foo")
216 		public String a(@Path("x") Integer x) {
217 			return String.valueOf(x);
218 		}
219 		@RestGet(path="/b/{x}/foo")
220 		public String b(@Path("x") Short x) {
221 			return String.valueOf(x);
222 		}
223 		@RestGet(path="/c/{x}/foo")
224 		public String c(@Path("x") Long x) {
225 			return String.valueOf(x);
226 		}
227 		@RestGet(path="/d/{x}/foo")
228 		public String d(@Path("x") Character x) {
229 			return String.valueOf(x);
230 		}
231 		@RestGet(path="/e/{x}/foo")
232 		public String e(@Path("x") Float x) {
233 			return String.valueOf(x);
234 		}
235 		@RestGet(path="/f/{x}/foo")
236 		public String f(@Path("x") Double x) {
237 			return String.valueOf(x);
238 		}
239 		@RestGet(path="/g/{x}/foo")
240 		public String g(@Path("x") Byte x) {
241 			return String.valueOf(x);
242 		}
243 		@RestGet(path="/h/{x}/foo")
244 		public String h(@Path("x") Boolean x) {
245 			return String.valueOf(x);
246 		}
247 	}
248 
249 	@Test void c01_primitiveObjects() throws Exception {
250 		var c = MockRestClient.buildLax(C.class);
251 
252 		c.get("/a/123/foo")
253 			.run()
254 			.assertContent("123");
255 		c.get("/a/bad/foo?noTrace=true")
256 			.run()
257 			.assertStatus(400);
258 
259 		c.get("/b/123/foo")
260 			.run()
261 			.assertContent("123");
262 		c.get("/b/bad/foo?noTrace=true")
263 			.run()
264 			.assertStatus(400);
265 
266 		c.get("/c/123/foo")
267 			.run()
268 			.assertContent("123");
269 		c.get("/c/bad/foo?noTrace=true")
270 			.run()
271 			.assertStatus(400);
272 
273 		c.get("/d/c/foo")
274 			.run()
275 			.assertContent("c");
276 		c.get("/d/bad/foo?noTrace=true")
277 			.run()
278 			.assertStatus(400);
279 
280 		c.get("/e/1.23/foo")
281 			.run()
282 			.assertContent("1.23");
283 		c.get("/e/bad/foo?noTrace=true")
284 			.run()
285 			.assertStatus(400);
286 
287 		c.get("/f/1.23/foo")
288 			.run()
289 			.assertContent("1.23");
290 		c.get("/f/bad/foo?noTrace=true")
291 			.run()
292 			.assertStatus(400);
293 
294 		c.get("/g/123/foo")
295 			.run()
296 			.assertContent("123");
297 		c.get("/g/bad/foo?noTrace=true")
298 			.run()
299 			.assertStatus(400);
300 
301 		c.get("/h/true/foo")
302 			.run()
303 			.assertContent("true");
304 		c.get("/h/bad/foo?noTrace=true")
305 			.run()
306 			.assertStatus(400);
307 	}
308 
309 	//------------------------------------------------------------------------------------------------------------------
310 	// POJOs convertible from strings
311 	//------------------------------------------------------------------------------------------------------------------
312 
313 	@Rest
314 	public static class D {
315 		// Object with forString(String) method
316 		@RestGet(path="/a/{uuid}")
317 		public UUID a(RestResponse res, @Path("uuid") UUID uuid) {
318 			return uuid;
319 		}
320 	}
321 
322 	@Test void d01_pojosConvertibleFromStrings() throws Exception {
323 		var d = MockRestClient.build(D.class);
324 
325 		var uuid = UUID.randomUUID();
326 		d.get("/a/" + uuid)
327 			.run()
328 			.assertContent(uuid.toString());
329 	}
330 
331 	//------------------------------------------------------------------------------------------------------------------
332 	// @Path annotation without name.
333 	//------------------------------------------------------------------------------------------------------------------
334 
335 	@Rest
336 	public static class E  {
337 		@RestGet(path="/x/{foo}/{bar}")
338 		public Object a(@Path String foo, @Path String bar) {
339 			return JsonMap.of("m", "normal1", "foo", foo, "bar", bar);
340 		}
341 		@RestGet(path="/x/{foo}/x/{bar}/x")
342 		public Object b(@Path String foo, @Path String bar) {
343 			return JsonMap.of("m", "normal2", "foo", foo, "bar", bar);
344 		}
345 		@RestGet(path="/y/{0}/{1}")
346 		public Object c(@Path String foo, @Path String bar) {
347 			return JsonMap.of("m", "numbers1", "0", foo, "1", bar);
348 		}
349 		@RestGet(path="/y/{0}/y/{1}/y")
350 		public Object d(@Path String foo, @Path String bar) {
351 			return JsonMap.of("m", "numbers2", "0", foo, "1", bar);
352 		}
353 		@RestGet(path="/z/{1}/z/{0}/z")
354 		public Object e(@Path String foo, @Path String bar) {
355 			return JsonMap.of("m", "numbers3", "0", foo, "1", bar);
356 		}
357 	}
358 
359 	@Test void e01_withoutName() throws Exception {
360 		var e = MockRestClient.build(E.class);
361 		e.get("/x/x1/x2")
362 			.run()
363 			.assertContent("{m:'normal1',foo:'x1',bar:'x2'}");
364 		e.get("/x/x1/x/x2/x")
365 			.run()
366 			.assertContent("{m:'normal2',foo:'x1',bar:'x2'}");
367 		e.get("/y/y1/y2")
368 			.run()
369 			.assertContent("{m:'numbers1','0':'y1','1':'y2'}");
370 		e.get("/y/y1/y/y2/y")
371 			.run()
372 			.assertContent("{m:'numbers2','0':'y1','1':'y2'}");
373 		e.get("/z/z1/z/z2/z")
374 			.run()
375 			.assertContent("{m:'numbers3','0':'z2','1':'z1'}");
376 	}
377 
378 	//------------------------------------------------------------------------------------------------------------------
379 	// Path variables on class.
380 	//------------------------------------------------------------------------------------------------------------------
381 
382 	@Rest(path="/f/{a}/{b}")
383 	public static class F  {
384 		@RestGet(path="/")
385 		public String a(RequestPathParams path) {
386 			return Utils.f("a: {0}", path.toString());
387 		}
388 		@RestGet(path="/*")
389 		public String b(RequestPathParams path) {
390 			return Utils.f("b: {0}", path.toString());
391 		}
392 		@RestGet(path="/fc")
393 		public String c(RequestPathParams path) {
394 			return Utils.f("c: {0}", path.toString());
395 		}
396 		@RestGet(path="/fd/{c}/{d}")
397 		public String d(RequestPathParams path) {
398 			return Utils.f("d: {0}", path.toString());
399 		}
400 		@RestGet(path="/fe/{a}/{b}")
401 		public String e(RequestPathParams path) {
402 			return Utils.f("e: {0}", path.toString());
403 		}
404 		@RestGet(path="/ff/{c}/{d}/*")
405 		public String f(RequestPathParams path) {
406 			return Utils.f("f: {0}", path.toString());
407 		}
408 	}
409 
410 	@Test void f01_pathVariablesOnClass() throws Exception {
411 		var f = MockRestClient.createLax(F.class).servletPath("/f").defaultRequestConfig(RequestConfig.custom().setNormalizeUri(false).build()).build();
412 		f.get("http://localhost/f/x1/x2")
413 			.run()
414 			.assertContent("a: {a=x1,b=x2}");
415 		f.get("http://localhost/f/x1")
416 			.run()
417 			.assertStatus(404);
418 		f.get("http://localhost/f")
419 			.run()
420 			.assertStatus(404);
421 		f.get("http://localhost/f//")
422 			.run()
423 			.assertStatus(404);
424 		f.get("http://localhost/f/x/")
425 			.run()
426 			.assertStatus(404);
427 		f.get("http://localhost/f//x")
428 			.run()
429 			.assertStatus(404);
430 		f.get("http://localhost/f/x1/x2/foo")
431 			.run()
432 			.assertContent("b: {/*=foo,/**=foo,a=x1,b=x2}");
433 		f.get("http://localhost/f///foo")
434 			.run()
435 			.assertStatus(404);
436 		f.get("http://localhost/f/x1//foo")
437 			.run()
438 			.assertStatus(404);
439 		f.get("http://localhost/f//x2/foo")
440 			.run()
441 			.assertStatus(404);
442 		f.get("http://localhost/f/x1/x2/fc")
443 			.run()
444 			.assertContent("c: {a=x1,b=x2}");
445 		f.get("http://localhost/f///a")
446 			.run()
447 			.assertStatus(404);
448 		f.get("http://localhost/f/x1//a")
449 			.run()
450 			.assertStatus(404);
451 		f.get("http://localhost/f//x2/a")
452 			.run()
453 			.assertStatus(404);
454 		f.get("http://localhost/f/x1/x2/fd/x3/x4")
455 			.run()
456 			.assertContent("d: {a=x1,b=x2,c=x3,d=x4}");
457 		f.get("http://localhost/f//x2/b/x3/x4")
458 			.run()
459 			.assertStatus(404);
460 		f.get("http://localhost/f/x1//b/x3/x4")
461 			.run()
462 			.assertStatus(404);
463 		f.get("http://localhost/f/x1/x2/b//x4")
464 			.run()
465 			.assertStatus(200);
466 		f.get("http://localhost/f/x1/x2/b/x3/")
467 			.run()
468 			.assertStatus(200);
469 		f.get("http://localhost/f///b//")
470 			.run()
471 			.assertStatus(404);
472 		f.get("http://localhost/f/x1/x2/fe/x3/x4")
473 			.run()
474 			.assertContent("e: {a=x1, x3,b=x2, x4}");
475 		f.get("http://localhost/f/x1/x2/ff/x3/x4")
476 			.run()
477 			.assertContent("f: {a=x1,b=x2,c=x3,d=x4}");
478 		f.get("http://localhost/f/x1/x2/ff/x3/x4/")
479 			.run()
480 			.assertContent("f: {/*=,/**=,a=x1,b=x2,c=x3,d=x4}");
481 		f.get("http://localhost/f/x1/x2/ff/x3/x4/foo/bar")
482 			.run()
483 			.assertContent("f: {/*=foo/bar,/**=foo/bar,a=x1,b=x2,c=x3,d=x4}");
484 	}
485 
486 	//------------------------------------------------------------------------------------------------------------------
487 	// Path variables on child class.
488 	//------------------------------------------------------------------------------------------------------------------
489 
490 	@Rest(children={F.class})
491 	public static class G {}
492 
493 	@Test void g01_pathVariablesOnChildClass() throws Exception {
494 		var g = MockRestClient.createLax(G.class).defaultRequestConfig(RequestConfig.custom().setNormalizeUri(false).build()).build();
495 		g.get("http://localhost/f/x1/x2")
496 			.run()
497 			.assertContent("a: {a=x1,b=x2}");
498 		g.get("http://localhost/f/x1")
499 			.run()
500 			.assertStatus(404);
501 		g.get("http://localhost/f")
502 			.run()
503 			.assertStatus(404);
504 		g.get("http://localhost/f//")
505 			.run()
506 			.assertStatus(404);
507 		g.get("http://localhost/f/x1/")
508 			.run()
509 			.assertStatus(404);
510 		g.get("http://localhost/f//x2")
511 			.run()
512 			.assertStatus(404);
513 		g.get("http://localhost/f/x1/x2/fc")
514 			.run()
515 			.assertContent("c: {a=x1,b=x2}");
516 		g.get("http://localhost/f///a")
517 			.run()
518 			.assertStatus(404);
519 		g.get("http://localhost/f/x1//a")
520 			.run()
521 			.assertStatus(404);
522 		g.get("http://localhost/f//x2/a")
523 			.run()
524 			.assertStatus(404);
525 		g.get("http://localhost/f/x1/x2/fd/x3/x4")
526 			.run()
527 			.assertContent("d: {a=x1,b=x2,c=x3,d=x4}");
528 		g.get("http://localhost/f//x2/b/x3/x4")
529 			.run()
530 			.assertStatus(404);
531 		g.get("http://localhost/f/x1//b/x3/x4")
532 			.run()
533 			.assertStatus(404);
534 		g.get("http://localhost/f/x1/x2/b//x4")
535 			.run()
536 			.assertStatus(200);
537 		g.get("http://localhost/f/x1/x2/b/x3/")
538 			.run()
539 			.assertStatus(200);
540 		g.get("http://localhost/f///b//")
541 			.run()
542 			.assertStatus(404);
543 		g.get("http://localhost/f/x1/x2/fe/x3/x4")
544 			.run()
545 			.assertContent("e: {a=x1, x3,b=x2, x4}");
546 		g.get("http://localhost/f/x1/x2/ff/x3/x4")
547 			.run()
548 			.assertContent("f: {a=x1,b=x2,c=x3,d=x4}");
549 		g.get("http://localhost/f/x1/x2/ff/x3/x4/")
550 			.run()
551 			.assertContent("f: {/*=,/**=,a=x1,b=x2,c=x3,d=x4}");
552 		g.get("http://localhost/f/x1/x2/ff/x3/x4/foo/bar")
553 			.run()
554 			.assertContent("f: {/*=foo/bar,/**=foo/bar,a=x1,b=x2,c=x3,d=x4}");
555 	}
556 
557 	//------------------------------------------------------------------------------------------------------------------
558 	// Path variables on parent and child class.
559 	//------------------------------------------------------------------------------------------------------------------
560 
561 	@Rest(path="/h/{ha}/{hb}", children={F.class})
562 	public static class H {}
563 
564 	@Test void h01_pathVariablesOnParentAndChildClass() throws Exception {
565 		var h = MockRestClient.createLax(H.class).servletPath("/h").defaultRequestConfig(RequestConfig.custom().setNormalizeUri(false).build()).build();
566 		h.get("http://localhost/h/ha1/hb1/f/x1/x2")
567 			.run()
568 			.assertContent("a: {a=x1,b=x2,ha=ha1,hb=hb1}");
569 		// These are 405 instead of 404 because when children don't match, we try to find a matching Java method.
570 		h.get("http://localhost/h/ha1/hb1/f/x1")
571 			.run()
572 			.assertStatus(404);
573 		h.get("http://localhost/h/ha1/hb1/f")
574 			.run()
575 			.assertStatus(404);
576 		h.get("http://localhost/h/ha1/hb1")
577 			.run()
578 			.assertStatus(404);
579 		h.get("http://localhost/h/ha1")
580 			.run()
581 			.assertStatus(404);
582 		h.get("http://localhost/h")
583 			.run()
584 			.assertStatus(404);
585 		h.get("http://localhost/h//hb1/f/x1/x2")
586 			.run()
587 			.assertStatus(404);
588 		h.get("http://localhost/h/ha1//f/x1/x2")
589 			.run()
590 			.assertStatus(404);
591 		h.get("http://localhost/h/ha1/hb1/f//x2")
592 			.run()
593 			.assertStatus(404);
594 		h.get("http://localhost/h/ha1/hb1/f/x1/")
595 			.run()
596 			.assertStatus(404);
597 		h.get("http://localhost/h///f//")
598 			.run()
599 			.assertStatus(404);
600 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/foo")
601 			.run()
602 			.assertContent("b: {/*=foo,/**=foo,a=x1,b=x2,ha=ha1,hb=hb1}");
603 		h.get("http://localhost/h//hb1/f/x1/x2/foo")
604 			.run()
605 			.assertStatus(404);
606 		h.get("http://localhost/h/ha1//f/x1/x2/foo")
607 			.run()
608 			.assertStatus(404);
609 		h.get("http://localhost/h/ha1/hb1/f//x2/foo")
610 			.run()
611 			.assertStatus(404);
612 		h.get("http://localhost/h/ha1/hb1/f/x1//foo")
613 			.run()
614 			.assertStatus(404);
615 		h.get("http://localhost/h///f///foo")
616 			.run()
617 			.assertStatus(404);
618 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/fc")
619 			.run()
620 			.assertContent("c: {a=x1,b=x2,ha=ha1,hb=hb1}");
621 		h.get("http://localhost/h//hb1/f/x1/x2/a")
622 			.run()
623 			.assertStatus(404);
624 		h.get("http://localhost/h/ha1//f/x1/x2/a")
625 			.run()
626 			.assertStatus(404);
627 		h.get("http://localhost/h/ha1/hb1/f//x2/a")
628 			.run()
629 			.assertStatus(404);
630 		h.get("http://localhost/h/ha1/hb1/f/x1//a")
631 			.run()
632 			.assertStatus(404);
633 		h.get("http://localhost/h///f///a")
634 			.run()
635 			.assertStatus(404);
636 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/fd/x3/x4")
637 			.run()
638 			.assertContent("d: {a=x1,b=x2,c=x3,d=x4,ha=ha1,hb=hb1}");
639 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/fe/x3/x4")
640 			.run()
641 			.assertContent("e: {a=x1, x3,b=x2, x4,ha=ha1,hb=hb1}");
642 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/ff/x3/x4")
643 			.run()
644 			.assertContent("f: {a=x1,b=x2,c=x3,d=x4,ha=ha1,hb=hb1}");
645 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/ff/x3/x4/")
646 			.run()
647 			.assertContent("f: {/*=,/**=,a=x1,b=x2,c=x3,d=x4,ha=ha1,hb=hb1}");
648 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/ff/x3/x4/foo/bar")
649 			.run()
650 			.assertContent("f: {/*=foo/bar,/**=foo/bar,a=x1,b=x2,c=x3,d=x4,ha=ha1,hb=hb1}");
651 	}
652 
653 	//------------------------------------------------------------------------------------------------------------------
654 	// Path variables on parents and child class.
655 	//------------------------------------------------------------------------------------------------------------------
656 
657 	@Rest(path="/i/{ia}/{ib}", children={H.class})
658 	public static class I {}
659 
660 	@Test void i01_pathVariablesOnParentAndChildClass() throws Exception {
661 		var i = MockRestClient.createLax(I.class).servletPath("/i").build();
662 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2")
663 			.run()
664 			.assertContent("a: {a=x1,b=x2,ha=ha1,hb=hb1,ia=ia1,ib=ib1}");
665 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1")
666 			.run()
667 			.assertStatus(404);
668 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f")
669 			.run()
670 			.assertStatus(404);
671 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1")
672 			.run()
673 			.assertStatus(404);
674 		i.get("http://localhost/i/ia1/ib1/h/ha1")
675 			.run()
676 			.assertStatus(404);
677 		i.get("http://localhost/i/ia1/ib1/h")
678 			.run()
679 			.assertStatus(404);
680 		i.get("http://localhost/i/ia1/ib1")
681 			.run()
682 			.assertStatus(404);
683 		i.get("http://localhost/i/ia1")
684 			.run()
685 			.assertStatus(404);
686 		i.get("http://localhost/i")
687 			.run()
688 			.assertStatus(404);
689 		i.get("http://localhost/i//ib1/h/ha1/hb1/f/x1/x2")
690 			.run()
691 			.assertStatus(404);
692 		i.get("http://localhost/i/ia1//h/ha1/hb1/f/x1/x2")
693 			.run()
694 			.assertStatus(404);
695 		i.get("http://localhost/i/ia1/ib1/h//hb1/f/x1/x2")
696 			.run()
697 			.assertStatus(404);
698 		i.get("http://localhost/i/ia1/ib1/h/ha1//f/x1/x2")
699 			.run()
700 			.assertStatus(404);
701 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f//x2")
702 			.run()
703 			.assertStatus(404);
704 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/")
705 			.run()
706 			.assertStatus(404);
707 		i.get("http://localhost/i///h///f//")
708 			.run()
709 			.assertStatus(404);
710 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/foo")
711 			.run()
712 			.assertContent("b: {/*=foo,/**=foo,a=x1,b=x2,ha=ha1,hb=hb1,ia=ia1,ib=ib1}");
713 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/fc")
714 			.run()
715 			.assertContent("c: {a=x1,b=x2,ha=ha1,hb=hb1,ia=ia1,ib=ib1}");
716 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/fd/x3/x4")
717 			.run()
718 			.assertContent("d: {a=x1,b=x2,c=x3,d=x4,ha=ha1,hb=hb1,ia=ia1,ib=ib1}");
719 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/fe/x3/x4")
720 			.run()
721 			.assertContent("e: {a=x1, x3,b=x2, x4,ha=ha1,hb=hb1,ia=ia1,ib=ib1}");
722 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/ff/x3/x4")
723 			.run()
724 			.assertContent("f: {a=x1,b=x2,c=x3,d=x4,ha=ha1,hb=hb1,ia=ia1,ib=ib1}");
725 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/ff/x3/x4/")
726 			.run()
727 			.assertContent("f: {/*=,/**=,a=x1,b=x2,c=x3,d=x4,ha=ha1,hb=hb1,ia=ia1,ib=ib1}");
728 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/ff/x3/x4/foo/bar")
729 			.run()
730 			.assertContent("f: {/*=foo/bar,/**=foo/bar,a=x1,b=x2,c=x3,d=x4,ha=ha1,hb=hb1,ia=ia1,ib=ib1}");
731 	}
732 
733 	//------------------------------------------------------------------------------------------------------------------
734 	// Optional path parameter.
735 	//------------------------------------------------------------------------------------------------------------------
736 
737 	@Rest(serializers=Json5Serializer.class)
738 	public static class J {
739 		@RestGet(path="/a/{f1}")
740 		public Object a(@Path("f1") Optional<Integer> f1) {
741 			assertNotNull(f1);
742 			return f1;
743 		}
744 		@RestGet(path="/b/{f1}")
745 		public Object b(@Path("f1") Optional<ABean> f1) {
746 			assertNotNull(f1);
747 			return f1;
748 		}
749 		@RestGet(path="/c/{f1}")
750 		public Object c(@Path("f1") Optional<List<ABean>> f1) {
751 			assertNotNull(f1);
752 			return f1;
753 		}
754 		@RestGet(path="/d/{f1}")
755 		public Object d(@Path("f1") List<Optional<ABean>> f1) {
756 			return f1;
757 		}
758 	}
759 
760 	@Test void j01_optionalParam() throws Exception {
761 		var j = MockRestClient.buildJson(J.class);
762 		j.get("/a/123")
763 			.run()
764 			.assertStatus(200)
765 			.assertContent("123");
766 		j.get("/b/a=1,b=foo")
767 			.run()
768 			.assertStatus(200)
769 			.assertContent("{a:1,b:'foo'}");
770 		j.get("/c/@((a=1,b=foo))")
771 			.run()
772 			.assertStatus(200)
773 			.assertContent("[{a:1,b:'foo'}]");
774 		j.get("/d/@((a=1,b=foo))")
775 			.run()
776 			.assertStatus(200)
777 			.assertContent("[{a:1,b:'foo'}]");
778 	}
779 
780 	//------------------------------------------------------------------------------------------------------------------
781 	// Basic tests
782 	//------------------------------------------------------------------------------------------------------------------
783 
784 	@Rest(path="/k1/{k1}",children=K2.class)
785 	public static class K1 {}
786 
787 	@Rest(path="/k2")
788 	public static class K2 {
789 		@RestGet(path="/")
790 		public String a(@Path("k1") @Schema(r=false) String k1) {
791 			return k1 == null ? "nil" : k1;
792 		}
793 		@RestGet(path="/foo/{bar}")
794 		public String b(@Path("k1") @Schema(r=false) String k1, @Path("bar") @Schema(r=false) String bar) {
795 			return (k1 == null ? "nil" : k1) + "," + (bar == null ? "nil" : bar);
796 		}
797 	}
798 
799 	@Test void k01_basic() throws Exception {
800 		var k1 = MockRestClient.build(K1.class);
801 		var k2 = MockRestClient.build(K2.class);
802 
803 		k1.get("http://localhost/k1/foo/k2")
804 			.run()
805 			.assertStatus(200)
806 			.assertContent("foo");
807 		k1.get("http://localhost/k1/foo/k2/foo/xxx")
808 			.run()
809 			.assertStatus(200)
810 			.assertContent("foo,xxx");
811 		k2.get("/")
812 			.run()
813 			.assertStatus(200)
814 			.assertContent("nil");
815 		k2.get("/foo/xxx")
816 			.run()
817 			.assertStatus(200)
818 			.assertContent("nil,xxx");
819 	}
820 
821 	//------------------------------------------------------------------------------------------------------------------
822 	// Multiple paths
823 	//------------------------------------------------------------------------------------------------------------------
824 
825 	@Rest(path="/l1/{l1}",children=L2.class)
826 	public static class L1 {}
827 
828 	@Rest(path="/l2")
829 	public static class L2 {
830 		@RestGet(path={"/","/{foo}"})
831 		public String a(@Path("l1") @Schema(r=false) String l1, @Path("foo") @Schema(r=false) String foo) {
832 			return "1," + (l1 == null ? "nil" : l1) + "," + (foo == null ? "nil" : foo);
833 		}
834 		@RestGet(path={"/foo","/foo/{foo}"})
835 		public String b(@Path("l1") @Schema(r=false) String l1, @Path("foo") @Schema(r=false) String foo) {
836 			return "2," + (l1 == null ? "nil" : l1) + "," + (foo == null ? "nil" : foo);
837 		}
838 	}
839 
840 	@Test void l01_multiplePaths() throws Exception {
841 		var l1 = MockRestClient.build(L1.class);
842 		var l2 = MockRestClient.build(L2.class);
843 
844 		l1.get("http://localhost/l1/l1foo/l2")
845 			.run()
846 			.assertStatus(200)
847 			.assertContent("1,l1foo,nil");
848 		l1.get("http://localhost/l1/l1foo/l2/l2foo")
849 			.run()
850 			.assertStatus(200)
851 			.assertContent("1,l1foo,l2foo");
852 		l1.get("http://localhost/l1/l1foo/l2/foo")
853 			.run()
854 			.assertStatus(200)
855 			.assertContent("2,l1foo,nil");
856 		l1.get("http://localhost/l1/l1foo/l2/foo/l2foo")
857 			.run()
858 			.assertStatus(200)
859 			.assertContent("2,l1foo,l2foo");
860 		l2.get("http://localhost/l2")
861 			.run()
862 			.assertStatus(200)
863 			.assertContent("1,nil,nil");
864 		l2.get("http://localhost/l2/l2foo")
865 			.run()
866 			.assertStatus(200)
867 			.assertContent("1,nil,l2foo");
868 		l2.get("http://localhost/l2/foo")
869 			.run()
870 			.assertStatus(200)
871 			.assertContent("2,nil,nil");
872 		l2.get("http://localhost/l2/foo/l2foo")
873 			.run()
874 			.assertStatus(200)
875 			.assertContent("2,nil,l2foo");
876 	}
877 }