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.common.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 format("a: {0}", path.toString());
387 		}
388 		@RestGet(path="/*")
389 		public String b(RequestPathParams path) {
390 			return format("b: {0}", path.toString());
391 		}
392 		@RestGet(path="/fc")
393 		public String c(RequestPathParams path) {
394 			return format("c: {0}", path.toString());
395 		}
396 		@RestGet(path="/fd/{c}/{d}")
397 		public String d(RequestPathParams path) {
398 			return format("d: {0}", path.toString());
399 		}
400 		@RestGet(path="/fe/{a}/{b}")
401 		public String e(RequestPathParams path) {
402 			return format("e: {0}", path.toString());
403 		}
404 		@RestGet(path="/ff/{c}/{d}/*")
405 		public String f(RequestPathParams path) {
406 			return format("f: {0}", path.toString());
407 		}
408 		private String format(String msg, Object...args) {
409 			return StringUtils.format(msg, args);
410 		}
411 	}
412 
413 	@Test void f01_pathVariablesOnClass() throws Exception {
414 		var f = MockRestClient.createLax(F.class).servletPath("/f").defaultRequestConfig(RequestConfig.custom().setNormalizeUri(false).build()).build();
415 		f.get("http://localhost/f/x1/x2")
416 			.run()
417 			.assertContent("a: {a:'x1',b:'x2'}");
418 		f.get("http://localhost/f/x1")
419 			.run()
420 			.assertStatus(404);
421 		f.get("http://localhost/f")
422 			.run()
423 			.assertStatus(404);
424 		f.get("http://localhost/f//")
425 			.run()
426 			.assertStatus(404);
427 		f.get("http://localhost/f/x/")
428 			.run()
429 			.assertStatus(404);
430 		f.get("http://localhost/f//x")
431 			.run()
432 			.assertStatus(404);
433 		f.get("http://localhost/f/x1/x2/foo")
434 			.run()
435 			.assertContent("b: {a:'x1',b:'x2','/**':'foo','/*':'foo'}");
436 		f.get("http://localhost/f///foo")
437 			.run()
438 			.assertStatus(404);
439 		f.get("http://localhost/f/x1//foo")
440 			.run()
441 			.assertStatus(404);
442 		f.get("http://localhost/f//x2/foo")
443 			.run()
444 			.assertStatus(404);
445 		f.get("http://localhost/f/x1/x2/fc")
446 			.run()
447 			.assertContent("c: {a:'x1',b:'x2'}");
448 		f.get("http://localhost/f///a")
449 			.run()
450 			.assertStatus(404);
451 		f.get("http://localhost/f/x1//a")
452 			.run()
453 			.assertStatus(404);
454 		f.get("http://localhost/f//x2/a")
455 			.run()
456 			.assertStatus(404);
457 		f.get("http://localhost/f/x1/x2/fd/x3/x4")
458 			.run()
459 			.assertContent("d: {a:'x1',b:'x2',c:'x3',d:'x4'}");
460 		f.get("http://localhost/f//x2/b/x3/x4")
461 			.run()
462 			.assertStatus(404);
463 		f.get("http://localhost/f/x1//b/x3/x4")
464 			.run()
465 			.assertStatus(404);
466 		f.get("http://localhost/f/x1/x2/b//x4")
467 			.run()
468 			.assertStatus(200);
469 		f.get("http://localhost/f/x1/x2/b/x3/")
470 			.run()
471 			.assertStatus(200);
472 		f.get("http://localhost/f///b//")
473 			.run()
474 			.assertStatus(404);
475 		f.get("http://localhost/f/x1/x2/fe/x3/x4")
476 			.run()
477 			.assertContent("e: {a:'x1, x3',b:'x2, 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/")
482 			.run()
483 			.assertContent("f: {a:'x1',b:'x2',c:'x3',d:'x4','/**':'','/*':''}");
484 		f.get("http://localhost/f/x1/x2/ff/x3/x4/foo/bar")
485 			.run()
486 			.assertContent("f: {a:'x1',b:'x2',c:'x3',d:'x4','/**':'foo/bar','/*':'foo/bar'}");
487 	}
488 
489 	//------------------------------------------------------------------------------------------------------------------
490 	// Path variables on child class.
491 	//------------------------------------------------------------------------------------------------------------------
492 
493 	@Rest(children={F.class})
494 	public static class G {}
495 
496 
497 	@Test void g01_pathVariablesOnChildClass() throws Exception {
498 		var g = MockRestClient.createLax(G.class).defaultRequestConfig(RequestConfig.custom().setNormalizeUri(false).build()).build();
499 		g.get("http://localhost/f/x1/x2")
500 			.run()
501 			.assertContent("a: {a:'x1',b:'x2'}");
502 		g.get("http://localhost/f/x1")
503 			.run()
504 			.assertStatus(404);
505 		g.get("http://localhost/f")
506 			.run()
507 			.assertStatus(404);
508 		g.get("http://localhost/f//")
509 			.run()
510 			.assertStatus(404);
511 		g.get("http://localhost/f/x1/")
512 			.run()
513 			.assertStatus(404);
514 		g.get("http://localhost/f//x2")
515 			.run()
516 			.assertStatus(404);
517 		g.get("http://localhost/f/x1/x2/fc")
518 			.run()
519 			.assertContent("c: {a:'x1',b:'x2'}");
520 		g.get("http://localhost/f///a")
521 			.run()
522 			.assertStatus(404);
523 		g.get("http://localhost/f/x1//a")
524 			.run()
525 			.assertStatus(404);
526 		g.get("http://localhost/f//x2/a")
527 			.run()
528 			.assertStatus(404);
529 		g.get("http://localhost/f/x1/x2/fd/x3/x4")
530 			.run()
531 			.assertContent("d: {a:'x1',b:'x2',c:'x3',d:'x4'}");
532 		g.get("http://localhost/f//x2/b/x3/x4")
533 			.run()
534 			.assertStatus(404);
535 		g.get("http://localhost/f/x1//b/x3/x4")
536 			.run()
537 			.assertStatus(404);
538 		g.get("http://localhost/f/x1/x2/b//x4")
539 			.run()
540 			.assertStatus(200);
541 		g.get("http://localhost/f/x1/x2/b/x3/")
542 			.run()
543 			.assertStatus(200);
544 		g.get("http://localhost/f///b//")
545 			.run()
546 			.assertStatus(404);
547 		g.get("http://localhost/f/x1/x2/fe/x3/x4")
548 			.run()
549 			.assertContent("e: {a:'x1, x3',b:'x2, x4'}");
550 		g.get("http://localhost/f/x1/x2/ff/x3/x4")
551 			.run()
552 			.assertContent("f: {a:'x1',b:'x2',c:'x3',d:'x4'}");
553 		g.get("http://localhost/f/x1/x2/ff/x3/x4/")
554 			.run()
555 			.assertContent("f: {a:'x1',b:'x2',c:'x3',d:'x4','/**':'','/*':''}");
556 		g.get("http://localhost/f/x1/x2/ff/x3/x4/foo/bar")
557 			.run()
558 			.assertContent("f: {a:'x1',b:'x2',c:'x3',d:'x4','/**':'foo/bar','/*':'foo/bar'}");
559 	}
560 
561 	//------------------------------------------------------------------------------------------------------------------
562 	// Path variables on parent and child class.
563 	//------------------------------------------------------------------------------------------------------------------
564 
565 	@Rest(path="/h/{ha}/{hb}", children={F.class})
566 	public static class H {}
567 
568 	@Test void h01_pathVariablesOnParentAndChildClass() throws Exception {
569 		var h = MockRestClient.createLax(H.class).servletPath("/h").defaultRequestConfig(RequestConfig.custom().setNormalizeUri(false).build()).build();
570 		h.get("http://localhost/h/ha1/hb1/f/x1/x2")
571 			.run()
572 			.assertContent("a: {a:'x1',b:'x2',ha:'ha1',hb:'hb1'}");
573 		// These are 405 instead of 404 because when children don't match, we try to find a matching Java method.
574 		h.get("http://localhost/h/ha1/hb1/f/x1")
575 			.run()
576 			.assertStatus(404);
577 		h.get("http://localhost/h/ha1/hb1/f")
578 			.run()
579 			.assertStatus(404);
580 		h.get("http://localhost/h/ha1/hb1")
581 			.run()
582 			.assertStatus(404);
583 		h.get("http://localhost/h/ha1")
584 			.run()
585 			.assertStatus(404);
586 		h.get("http://localhost/h")
587 			.run()
588 			.assertStatus(404);
589 		h.get("http://localhost/h//hb1/f/x1/x2")
590 			.run()
591 			.assertStatus(404);
592 		h.get("http://localhost/h/ha1//f/x1/x2")
593 			.run()
594 			.assertStatus(404);
595 		h.get("http://localhost/h/ha1/hb1/f//x2")
596 			.run()
597 			.assertStatus(404);
598 		h.get("http://localhost/h/ha1/hb1/f/x1/")
599 			.run()
600 			.assertStatus(404);
601 		h.get("http://localhost/h///f//")
602 			.run()
603 			.assertStatus(404);
604 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/foo")
605 			.run()
606 			.assertContent("b: {a:'x1',b:'x2',ha:'ha1',hb:'hb1','/**':'foo','/*':'foo'}");
607 		h.get("http://localhost/h//hb1/f/x1/x2/foo")
608 			.run()
609 			.assertStatus(404);
610 		h.get("http://localhost/h/ha1//f/x1/x2/foo")
611 			.run()
612 			.assertStatus(404);
613 		h.get("http://localhost/h/ha1/hb1/f//x2/foo")
614 			.run()
615 			.assertStatus(404);
616 		h.get("http://localhost/h/ha1/hb1/f/x1//foo")
617 			.run()
618 			.assertStatus(404);
619 		h.get("http://localhost/h///f///foo")
620 			.run()
621 			.assertStatus(404);
622 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/fc")
623 			.run()
624 			.assertContent("c: {a:'x1',b:'x2',ha:'ha1',hb:'hb1'}");
625 		h.get("http://localhost/h//hb1/f/x1/x2/a")
626 			.run()
627 			.assertStatus(404);
628 		h.get("http://localhost/h/ha1//f/x1/x2/a")
629 			.run()
630 			.assertStatus(404);
631 		h.get("http://localhost/h/ha1/hb1/f//x2/a")
632 			.run()
633 			.assertStatus(404);
634 		h.get("http://localhost/h/ha1/hb1/f/x1//a")
635 			.run()
636 			.assertStatus(404);
637 		h.get("http://localhost/h///f///a")
638 			.run()
639 			.assertStatus(404);
640 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/fd/x3/x4")
641 			.run()
642 			.assertContent("d: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',c:'x3',d:'x4'}");
643 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/fe/x3/x4")
644 			.run()
645 			.assertContent("e: {a:'x1, x3',b:'x2, x4',ha:'ha1',hb:'hb1'}");
646 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/ff/x3/x4")
647 			.run()
648 			.assertContent("f: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',c:'x3',d:'x4'}");
649 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/ff/x3/x4/")
650 			.run()
651 			.assertContent("f: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',c:'x3',d:'x4','/**':'','/*':''}");
652 		h.get("http://localhost/h/ha1/hb1/f/x1/x2/ff/x3/x4/foo/bar")
653 			.run()
654 			.assertContent("f: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',c:'x3',d:'x4','/**':'foo/bar','/*':'foo/bar'}");
655 	}
656 
657 	//------------------------------------------------------------------------------------------------------------------
658 	// Path variables on parents and child class.
659 	//------------------------------------------------------------------------------------------------------------------
660 
661 	@Rest(path="/i/{ia}/{ib}", children={H.class})
662 	public static class I {}
663 
664 	@Test void i01_pathVariablesOnParentAndChildClass() throws Exception {
665 		var i = MockRestClient.createLax(I.class).servletPath("/i").build();
666 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2")
667 			.run()
668 			.assertContent("a: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',ia:'ia1',ib:'ib1'}");
669 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1")
670 			.run()
671 			.assertStatus(404);
672 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f")
673 			.run()
674 			.assertStatus(404);
675 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1")
676 			.run()
677 			.assertStatus(404);
678 		i.get("http://localhost/i/ia1/ib1/h/ha1")
679 			.run()
680 			.assertStatus(404);
681 		i.get("http://localhost/i/ia1/ib1/h")
682 			.run()
683 			.assertStatus(404);
684 		i.get("http://localhost/i/ia1/ib1")
685 			.run()
686 			.assertStatus(404);
687 		i.get("http://localhost/i/ia1")
688 			.run()
689 			.assertStatus(404);
690 		i.get("http://localhost/i")
691 			.run()
692 			.assertStatus(404);
693 		i.get("http://localhost/i//ib1/h/ha1/hb1/f/x1/x2")
694 			.run()
695 			.assertStatus(404);
696 		i.get("http://localhost/i/ia1//h/ha1/hb1/f/x1/x2")
697 			.run()
698 			.assertStatus(404);
699 		i.get("http://localhost/i/ia1/ib1/h//hb1/f/x1/x2")
700 			.run()
701 			.assertStatus(404);
702 		i.get("http://localhost/i/ia1/ib1/h/ha1//f/x1/x2")
703 			.run()
704 			.assertStatus(404);
705 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f//x2")
706 			.run()
707 			.assertStatus(404);
708 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/")
709 			.run()
710 			.assertStatus(404);
711 		i.get("http://localhost/i///h///f//")
712 			.run()
713 			.assertStatus(404);
714 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/foo")
715 			.run()
716 			.assertContent("b: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',ia:'ia1',ib:'ib1','/**':'foo','/*':'foo'}");
717 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/fc")
718 			.run()
719 			.assertContent("c: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',ia:'ia1',ib:'ib1'}");
720 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/fd/x3/x4")
721 			.run()
722 			.assertContent("d: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',ia:'ia1',ib:'ib1',c:'x3',d:'x4'}");
723 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/fe/x3/x4")
724 			.run()
725 			.assertContent("e: {a:'x1, x3',b:'x2, x4',ha:'ha1',hb:'hb1',ia:'ia1',ib:'ib1'}");
726 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/ff/x3/x4")
727 			.run()
728 			.assertContent("f: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',ia:'ia1',ib:'ib1',c:'x3',d:'x4'}");
729 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/ff/x3/x4/")
730 			.run()
731 			.assertContent("f: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',ia:'ia1',ib:'ib1',c:'x3',d:'x4','/**':'','/*':''}");
732 		i.get("http://localhost/i/ia1/ib1/h/ha1/hb1/f/x1/x2/ff/x3/x4/foo/bar")
733 			.run()
734 			.assertContent("f: {a:'x1',b:'x2',ha:'ha1',hb:'hb1',ia:'ia1',ib:'ib1',c:'x3',d:'x4','/**':'foo/bar','/*':'foo/bar'}");
735 	}
736 
737 	//------------------------------------------------------------------------------------------------------------------
738 	// Optional path parameter.
739 	//------------------------------------------------------------------------------------------------------------------
740 
741 	@Rest(serializers=Json5Serializer.class)
742 	public static class J {
743 		@RestGet(path="/a/{f1}")
744 		public Object a(@Path("f1") Optional<Integer> f1) {
745 			assertNotNull(f1);
746 			return f1;
747 		}
748 		@RestGet(path="/b/{f1}")
749 		public Object b(@Path("f1") Optional<ABean> f1) {
750 			assertNotNull(f1);
751 			return f1;
752 		}
753 		@RestGet(path="/c/{f1}")
754 		public Object c(@Path("f1") Optional<List<ABean>> f1) {
755 			assertNotNull(f1);
756 			return f1;
757 		}
758 		@RestGet(path="/d/{f1}")
759 		public Object d(@Path("f1") List<Optional<ABean>> f1) {
760 			return f1;
761 		}
762 	}
763 
764 	@Test void j01_optionalParam() throws Exception {
765 		var j = MockRestClient.buildJson(J.class);
766 		j.get("/a/123")
767 			.run()
768 			.assertStatus(200)
769 			.assertContent("123");
770 		j.get("/b/a=1,b=foo")
771 			.run()
772 			.assertStatus(200)
773 			.assertContent("{a:1,b:'foo'}");
774 		j.get("/c/@((a=1,b=foo))")
775 			.run()
776 			.assertStatus(200)
777 			.assertContent("[{a:1,b:'foo'}]");
778 		j.get("/d/@((a=1,b=foo))")
779 			.run()
780 			.assertStatus(200)
781 			.assertContent("[{a:1,b:'foo'}]");
782 	}
783 
784 	//------------------------------------------------------------------------------------------------------------------
785 	// Basic tests
786 	//------------------------------------------------------------------------------------------------------------------
787 
788 	@Rest(path="/k1/{k1}",children=K2.class)
789 	public static class K1 {}
790 
791 	@Rest(path="/k2")
792 	public static class K2 {
793 		@RestGet(path="/")
794 		public String a(@Path("k1") @Schema(r=false) String k1) {
795 			return k1 == null ? "nil" : k1;
796 		}
797 		@RestGet(path="/foo/{bar}")
798 		public String b(@Path("k1") @Schema(r=false) String k1, @Path("bar") @Schema(r=false) String bar) {
799 			return (k1 == null ? "nil" : k1) + "," + (bar == null ? "nil" : bar);
800 		}
801 	}
802 
803 	@Test void k01_basic() throws Exception {
804 		var k1 = MockRestClient.build(K1.class);
805 		var k2 = MockRestClient.build(K2.class);
806 
807 		k1.get("http://localhost/k1/foo/k2")
808 			.run()
809 			.assertStatus(200)
810 			.assertContent("foo");
811 		k1.get("http://localhost/k1/foo/k2/foo/xxx")
812 			.run()
813 			.assertStatus(200)
814 			.assertContent("foo,xxx");
815 		k2.get("/")
816 			.run()
817 			.assertStatus(200)
818 			.assertContent("nil");
819 		k2.get("/foo/xxx")
820 			.run()
821 			.assertStatus(200)
822 			.assertContent("nil,xxx");
823 	}
824 
825 	//------------------------------------------------------------------------------------------------------------------
826 	// Multiple paths
827 	//------------------------------------------------------------------------------------------------------------------
828 
829 	@Rest(path="/l1/{l1}",children=L2.class)
830 	public static class L1 {}
831 
832 	@Rest(path="/l2")
833 	public static class L2 {
834 		@RestGet(path={"/","/{foo}"})
835 		public String a(@Path("l1") @Schema(r=false) String l1, @Path("foo") @Schema(r=false) String foo) {
836 			return "1," + (l1 == null ? "nil" : l1) + "," + (foo == null ? "nil" : foo);
837 		}
838 		@RestGet(path={"/foo","/foo/{foo}"})
839 		public String b(@Path("l1") @Schema(r=false) String l1, @Path("foo") @Schema(r=false) String foo) {
840 			return "2," + (l1 == null ? "nil" : l1) + "," + (foo == null ? "nil" : foo);
841 		}
842 	}
843 
844 	@Test void l01_multiplePaths() throws Exception {
845 		var l1 = MockRestClient.build(L1.class);
846 		var l2 = MockRestClient.build(L2.class);
847 
848 		l1.get("http://localhost/l1/l1foo/l2")
849 			.run()
850 			.assertStatus(200)
851 			.assertContent("1,l1foo,nil");
852 		l1.get("http://localhost/l1/l1foo/l2/l2foo")
853 			.run()
854 			.assertStatus(200)
855 			.assertContent("1,l1foo,l2foo");
856 		l1.get("http://localhost/l1/l1foo/l2/foo")
857 			.run()
858 			.assertStatus(200)
859 			.assertContent("2,l1foo,nil");
860 		l1.get("http://localhost/l1/l1foo/l2/foo/l2foo")
861 			.run()
862 			.assertStatus(200)
863 			.assertContent("2,l1foo,l2foo");
864 		l2.get("http://localhost/l2")
865 			.run()
866 			.assertStatus(200)
867 			.assertContent("1,nil,nil");
868 		l2.get("http://localhost/l2/l2foo")
869 			.run()
870 			.assertStatus(200)
871 			.assertContent("1,nil,l2foo");
872 		l2.get("http://localhost/l2/foo")
873 			.run()
874 			.assertStatus(200)
875 			.assertContent("2,nil,nil");
876 		l2.get("http://localhost/l2/foo/l2foo")
877 			.run()
878 			.assertStatus(200)
879 			.assertContent("2,nil,l2foo");
880 	}
881 }