001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.rest.mock2; 014 015import static org.apache.juneau.rest.util.RestUtils.*; 016import static org.apache.juneau.internal.StringUtils.*; 017 018import java.io.*; 019import java.util.*; 020import java.util.concurrent.*; 021 022import org.apache.juneau.marshall.*; 023import org.apache.juneau.parser.*; 024import org.apache.juneau.rest.*; 025import org.apache.juneau.rest.annotation.*; 026import org.apache.juneau.rest.util.*; 027import org.apache.juneau.serializer.*; 028 029/** 030 * Creates a mocked interface against a REST resource class. 031 * 032 * <p> 033 * Allows you to test your REST resource classes without a running servlet container. 034 * 035 * <h5 class='figure'>Example:</h5> 036 * <p class='bcode w800'> 037 * <jk>public class</jk> MockTest { 038 * 039 * <jc>// Our REST resource to test.</jc> 040 * <ja>@Rest</ja>(serializers=JsonSerializer.Simple.<jk>class</jk>, parsers=JsonParser.<jk>class</jk>) 041 * <jk>public static class</jk> MyRest { 042 * 043 * <ja>@RestMethod</ja>(name=<jsf>PUT</jsf>, path=<js>"/String"</js>) 044 * <jk>public</jk> String echo(<ja>@Body</ja> String b) { 045 * <jk>return</jk> b; 046 * } 047 * } 048 * 049 * <ja>@Test</ja> 050 * <jk>public void</jk> testEcho() <jk>throws</jk> Exception { 051 * MockRest 052 * .<jsm>create</jsm>(MyRest.<jk>class</jk>) 053 * .put(<js>"/String"</js>, <js>"'foo'"</js>) 054 * .execute() 055 * .assertStatus(200) 056 * .assertBody(<js>"'foo'"</js>); 057 * } 058 * </p> 059 * 060 * <ul class='seealso'> 061 * <li class='link'>{@doc juneau-rest-mock.MockRest} 062 * </ul> 063 */ 064public class MockRest implements MockHttpConnection { 065 private static Map<Class<?>,RestContext> CONTEXTS_DEBUG = new ConcurrentHashMap<>(), CONTEXTS_NORMAL = new ConcurrentHashMap<>(); 066 067 private final RestContext ctx; 068 069 /** Requests headers to add to every request. */ 070 protected final Map<String,Object> headers; 071 072 /** Debug mode enabled. */ 073 protected final boolean debug; 074 075 final String contextPath, servletPath; 076 077 /** 078 * Constructor. 079 * 080 * @param b Builder. 081 */ 082 protected MockRest(Builder b) { 083 try { 084 debug = b.debug; 085 Class<?> c = b.impl instanceof Class ? (Class<?>)b.impl : b.impl.getClass(); 086 Map<Class<?>,RestContext> contexts = debug ? CONTEXTS_DEBUG : CONTEXTS_NORMAL; 087 if (! contexts.containsKey(c)) { 088 Object o = b.impl instanceof Class ? ((Class<?>)b.impl).newInstance() : b.impl; 089 RestContextBuilder rcb = RestContext.create(o); 090 if (debug) { 091 rcb.debug(Enablement.TRUE); 092 rcb.callLoggerConfig(RestCallLoggerConfig.DEFAULT_DEBUG); 093 } 094 RestContext rc = rcb.build(); 095 if (o instanceof RestServlet) { 096 ((RestServlet)o).setContext(rc); 097 } else { 098 rc.postInit(); 099 } 100 rc.postInitChildFirst(); 101 contexts.put(c, rc); 102 } 103 ctx = contexts.get(c); 104 headers = new LinkedHashMap<>(b.headers); 105 contextPath = b.contextPath; 106 servletPath = b.servletPath; 107 } catch (Exception e) { 108 throw new RuntimeException(e); 109 } 110 } 111 112 /** 113 * Creates a new builder with the specified REST implementation bean or bean class. 114 * 115 * <p> 116 * No <c>Accept</c> or <c>Content-Type</c> header is specified by default. 117 * 118 * @param impl 119 * The REST bean or bean class annotated with {@link Rest @Rest}. 120 * <br>If a class, it must have a no-arg constructor. 121 * @return A new builder. 122 */ 123 public static Builder create(Object impl) { 124 return new Builder(impl); 125 } 126 127 /** 128 * Convenience method for creating a MockRest over the specified REST implementation bean or bean class. 129 * 130 * <p> 131 * <c>Accept</c> header is set to <c>"application/json+simple"</c> by default. 132 * <c>Content-Type</c> header is set to <c>"application/json"</c> by default. 133 * 134 * <p> 135 * Equivalent to calling: 136 * <p class='bpcode w800'> 137 * MockRest.create(impl, SimpleJson.<jsf>DEFAULT</jsf>).build(); 138 * </p> 139 * 140 * @param impl 141 * The REST bean or bean class annotated with {@link Rest @Rest}. 142 * <br>If a class, it must have a no-arg constructor. 143 * @return A new {@link MockRest} object. 144 */ 145 public static MockRest build(Object impl) { 146 return build(impl, SimpleJson.DEFAULT); 147 } 148 149 /** 150 * Convenience method for creating a MockRest over the specified REST implementation bean or bean class. 151 * 152 * <p> 153 * <c>Accept</c> and <c>Content-Type</c> headers are set to the primary media types on the specified marshall. 154 * 155 * <p> 156 * Note that the marshall itself is not involved in any serialization or parsing. 157 * 158 * <p> 159 * Equivalent to calling: 160 * <p class='bpcode w800'> 161 * MockRest.create(impl, SimpleJson.<jsf>DEFAULT</jsf>).marshall(m).build(); 162 * </p> 163 * 164 * @param impl 165 * The REST bean or bean class annotated with {@link Rest @Rest}. 166 * <br>If a class, it must have a no-arg constructor. 167 * @param m 168 * The marshall to use for specifying the <c>Accept</c> and <c>Content-Type</c> headers. 169 * <br>If <jk>null</jk>, headers will be reset. 170 * @return A new {@link MockRest} object. 171 */ 172 public static MockRest build(Object impl, Marshall m) { 173 return create(impl).marshall(m).build(); 174 } 175 176 /** 177 * Convenience method for creating a MockRest over the specified REST implementation bean or bean class. 178 * 179 * <p> 180 * <c>Accept</c> and <c>Content-Type</c> headers are set to the primary media types on the specified serializer and parser. 181 * 182 * <p> 183 * Note that the marshall itself is not involved in any serialization or parsing. 184 * 185 * <p> 186 * Equivalent to calling: 187 * <p class='bpcode w800'> 188 * MockRest.create(impl, SimpleJson.<jsf>DEFAULT</jsf>).serializer(s).parser(p).build(); 189 * </p> 190 * 191 * @param impl 192 * The REST bean or bean class annotated with {@link Rest @Rest}. 193 * <br>If a class, it must have a no-arg constructor. 194 * @param s 195 * The serializer to use for specifying the <c>Content-Type</c> header. 196 * <br>If <jk>null</jk>, header will be reset. 197 * @param p 198 * The parser to use for specifying the <c>Accept</c> header. 199 * <br>If <jk>null</jk>, header will be reset. 200 * @return A new {@link MockRest} object. 201 */ 202 public static MockRest build(Object impl, Serializer s, Parser p) { 203 return create(impl).serializer(s).parser(p).build(); 204 } 205 206 /** 207 * Builder class. 208 */ 209 public static class Builder { 210 Object impl; 211 boolean debug; 212 Map<String,Object> headers = new LinkedHashMap<>(); 213 String contextPath, servletPath; 214 215 Builder(Object impl) { 216 this.impl = impl; 217 } 218 219 /** 220 * Enable debug mode. 221 * 222 * @return This object (for method chaining). 223 */ 224 public Builder debug() { 225 this.debug = true; 226 header("X-Debug", true); 227 return this; 228 } 229 230 /** 231 * Enable no-trace mode. 232 * 233 * @return This object (for method chaining). 234 */ 235 public Builder noTrace() { 236 header("X-NoTrace", true); 237 return this; 238 } 239 240 /** 241 * Adds a header to every request. 242 * 243 * @param name The header name. 244 * @param value 245 * The header value. 246 * <br>Can be <jk>null</jk> (will be skipped). 247 * @return This object (for method chaining). 248 */ 249 public Builder header(String name, Object value) { 250 this.headers.put(name, value); 251 return this; 252 } 253 254 /** 255 * Adds the specified headers to every request. 256 * 257 * @param value 258 * The header values. 259 * <br>Can be <jk>null</jk> (existing values will be cleared). 260 * <br><jk>null</jk> null map values will be ignored. 261 * @return This object (for method chaining). 262 */ 263 public Builder headers(Map<String,Object> value) { 264 if (value != null) 265 this.headers.putAll(value); 266 else 267 this.headers.clear(); 268 return this; 269 } 270 271 /** 272 * Specifies the <c>Accept</c> header to every request. 273 * 274 * @param value The <c>Accept</c> header value. 275 * @return This object (for method chaining). 276 */ 277 public Builder accept(String value) { 278 return header("Accept", value); 279 } 280 281 /** 282 * Specifies the <c>Content-Type</c> header to every request. 283 * 284 * @param value The <c>Content-Type</c> header value. 285 * @return This object (for method chaining). 286 */ 287 public Builder contentType(String value) { 288 return header("Content-Type", value); 289 } 290 291 /** 292 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"application/json"</js>. 293 * 294 * @return This object (for method chaining). 295 */ 296 public Builder json() { 297 return accept("application/json").contentType("application/json"); 298 } 299 300 /** 301 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"application/json+simple"</js>. 302 * 303 * @return This object (for method chaining). 304 */ 305 public Builder simpleJson() { 306 return accept("application/json+simple").contentType("application/json+simple"); 307 } 308 309 /** 310 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/xml"</js>. 311 * 312 * @return This object (for method chaining). 313 */ 314 public Builder xml() { 315 return accept("text/xml").contentType("text/xml"); 316 } 317 318 /** 319 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/html"</js>. 320 * 321 * @return This object (for method chaining). 322 */ 323 public Builder html() { 324 return accept("text/html").contentType("text/html"); 325 } 326 327 /** 328 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/plain"</js>. 329 * 330 * @return This object (for method chaining). 331 */ 332 public Builder plainText() { 333 return accept("text/plain").contentType("text/plain"); 334 } 335 336 /** 337 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"octal/msgpack"</js>. 338 * 339 * @return This object (for method chaining). 340 */ 341 public Builder msgpack() { 342 return accept("octal/msgpack").contentType("octal/msgpack"); 343 } 344 345 /** 346 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/uon"</js>. 347 * 348 * @return This object (for method chaining). 349 */ 350 public Builder uon() { 351 return accept("text/uon").contentType("text/uon"); 352 } 353 354 /** 355 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"application/x-www-form-urlencoded"</js>. 356 * 357 * @return This object (for method chaining). 358 */ 359 public Builder urlEnc() { 360 return accept("application/x-www-form-urlencoded").contentType("application/x-www-form-urlencoded"); 361 } 362 363 /** 364 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/yaml"</js>. 365 * 366 * @return This object (for method chaining). 367 */ 368 public Builder yaml() { 369 return accept("text/yaml").contentType("text/yaml"); 370 } 371 372 /** 373 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/openapi"</js>. 374 * 375 * @return This object (for method chaining). 376 */ 377 public Builder openapi() { 378 return accept("text/openapi").contentType("text/openapi"); 379 } 380 381 /** 382 * Convenience method for setting the <c>Content-Type</c> header to the primary media type on the specified serializer. 383 * 384 * @param value 385 * The serializer to get the media type from. 386 * <br>If <jk>null</jk>, header will be reset. 387 * @return This object (for method chaining). 388 */ 389 public Builder serializer(Serializer value) { 390 return contentType(value == null ? null : value.getPrimaryMediaType().toString()); 391 } 392 393 /** 394 * Convenience method for setting the <c>Accept</c> header to the primary media type on the specified parser. 395 * 396 * @param value 397 * The parser to get the media type from. 398 * <br>If <jk>null</jk>, header will be reset. 399 * @return This object (for method chaining). 400 */ 401 public Builder parser(Parser value) { 402 return accept(value == null ? null : value.getPrimaryMediaType().toString()); 403 } 404 405 /** 406 * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to the primary media types on the specified marshall. 407 * 408 * @param value 409 * The marshall to get the media types from. 410 * <br>If <jk>null</jk>, headers will be reset. 411 * @return This object (for method chaining). 412 */ 413 public Builder marshall(Marshall value) { 414 contentType(value == null ? null : value.getSerializer().getPrimaryMediaType().toString()); 415 accept(value == null ? null : value.getParser().getPrimaryMediaType().toString()); 416 return this; 417 } 418 419 /** 420 * Identifies the context path for the REST resource. 421 * 422 * <p> 423 * If not specified, uses <js>""</js>. 424 * 425 * @param value 426 * The context path. 427 * <br>Must not be <jk>null</jk> and must either be blank or start but not end with a <js>'/'</js> character. 428 * @return This object (for method chaining). 429 */ 430 public Builder contextPath(String value) { 431 validateContextPath(value); 432 this.contextPath = value; 433 return this; 434 } 435 436 /** 437 * Identifies the servlet path for the REST resource. 438 * 439 * <p> 440 * If not specified, uses <js>""</js>. 441 * 442 * @param value 443 * The servlet path. 444 * <br>Must not be <jk>null</jk> and must either be blank or start but not end with a <js>'/'</js> character. 445 * @return This object (for method chaining). 446 */ 447 public Builder servletPath(String value) { 448 validateServletPath(value); 449 this.servletPath = value; 450 return this; 451 } 452 453 /** 454 * Create a new {@link MockRest} object based on the settings on this builder. 455 * 456 * @return A new {@link MockRest} object. 457 */ 458 public MockRest build() { 459 return new MockRest(this); 460 } 461 } 462 463 /** 464 * Performs a REST request against the REST interface. 465 * 466 * @param method The HTTP method 467 * @param path The URI path. 468 * @param headers Optional headers to include in the request. 469 * @param body 470 * The body of the request. 471 * <br>Can be any of the following data types: 472 * <ul> 473 * <li><code><jk>byte</jk>[]</code> 474 * <li>{@link Reader} 475 * <li>{@link InputStream} 476 * <li>{@link CharSequence} 477 * </ul> 478 * Any other types are converted to a string using the <c>toString()</c> method. 479 * @return A new servlet request. 480 */ 481 @Override /* MockHttpConnection */ 482 public MockServletRequest request(String method, String path, Map<String,Object> headers, Object body) { 483 String p = RestUtils.trimContextPath(ctx.getPath(), path); 484 return MockServletRequest.create(method, p).contextPath(emptyIfNull(contextPath)).servletPath(emptyIfNull(servletPath)).body(body).headers(this.headers).headers(headers).debug(debug).restContext(ctx); 485 } 486 487 /** 488 * Performs a REST request against the REST interface. 489 * 490 * @param method The HTTP method 491 * @param path The URI path. 492 * @return A new servlet request. 493 */ 494 public MockServletRequest request(String method, String path) { 495 return request(method, path, null, null); 496 } 497 498 /** 499 * Performs a REST request against the REST interface. 500 * 501 * @param method The HTTP method 502 * @param path The URI path. 503 * @param body 504 * The body of the request. 505 * <br>Can be any of the following data types: 506 * <ul> 507 * <li><code><jk>byte</jk>[]</code> 508 * <li>{@link Reader} 509 * <li>{@link InputStream} 510 * <li>{@link CharSequence} 511 * </ul> 512 * Any other types are converted to a string using the <c>toString()</c> method. 513 * @return A new servlet request. 514 */ 515 public MockServletRequest request(String method, String path, Object body) { 516 return request(method, path, null, body); 517 } 518 519 /** 520 * Performs a REST request against the REST interface. 521 * 522 * @param method The HTTP method 523 * @param headers Optional headers to include in the request. 524 * @param path The URI path. 525 * @return A new servlet request. 526 */ 527 public MockServletRequest request(String method, Map<String,Object> headers, String path) { 528 return request(method, path, headers, null); 529 } 530 531 /** 532 * Perform a GET request. 533 * 534 * @param path The URI path. 535 * @return A new servlet request. 536 */ 537 public MockServletRequest get(String path) { 538 return request("GET", path, null, null); 539 } 540 541 /** 542 * Shortcut for <code>get(<js>""</js>)</code> 543 * 544 * @return A new servlet request. 545 */ 546 public MockServletRequest get() { 547 return get(""); 548 } 549 550 /** 551 * Perform a PUT request. 552 * 553 * @param path The URI path. 554 * @param body 555 * The body of the request. 556 * <br>Can be any of the following data types: 557 * <ul> 558 * <li><code><jk>byte</jk>[]</code> 559 * <li>{@link Reader} 560 * <li>{@link InputStream} 561 * <li>{@link CharSequence} 562 * </ul> 563 * Any other types are converted to a string using the <c>toString()</c> method. 564 * @return A new servlet request. 565 */ 566 public MockServletRequest put(String path, Object body) { 567 return request("PUT", path, null, body); 568 } 569 570 /** 571 * Perform a POST request. 572 * 573 * @param path The URI path. 574 * @param body 575 * The body of the request. 576 * <br>Can be any of the following data types: 577 * <ul> 578 * <li><code><jk>byte</jk>[]</code> 579 * <li>{@link Reader} 580 * <li>{@link InputStream} 581 * <li>{@link CharSequence} 582 * </ul> 583 * Any other types are converted to a string using the <c>toString()</c> method. 584 * @return A new servlet request. 585 */ 586 public MockServletRequest post(String path, Object body) { 587 return request("POST", path, null, body); 588 } 589 590 /** 591 * Perform a DELETE request. 592 * 593 * @param path The URI path. 594 * @return A new servlet request. 595 */ 596 public MockServletRequest delete(String path) { 597 return request("DELETE", path, null, null); 598 } 599 600 /** 601 * Perform a HEAD request. 602 * 603 * @param path The URI path. 604 * @return A new servlet request. 605 */ 606 public MockServletRequest head(String path) { 607 return request("HEAD", path, null, null); 608 } 609 610 /** 611 * Perform an OPTIONS request. 612 * 613 * @param path The URI path. 614 * @return A new servlet request. 615 */ 616 public MockServletRequest options(String path) { 617 return request("OPTIONS", path, null, null); 618 } 619 620 /** 621 * Perform a PATCH request. 622 * 623 * @param path The URI path. 624 * @param body 625 * The body of the request. 626 * <br>Can be any of the following data types: 627 * <ul> 628 * <li><code><jk>byte</jk>[]</code> 629 * <li>{@link Reader} 630 * <li>{@link InputStream} 631 * <li>{@link CharSequence} 632 * </ul> 633 * Any other types are converted to a string using the <c>toString()</c> method. 634 * @return A new servlet request. 635 */ 636 public MockServletRequest patch(String path, Object body) { 637 return request("PATCH", path, null, body); 638 } 639 640 /** 641 * Perform a CONNECT request. 642 * 643 * @param path The URI path. 644 * @return A new servlet request. 645 */ 646 public MockServletRequest connect(String path) { 647 return request("CONNECT", path, null, null); 648 } 649 650 /** 651 * Perform a TRACE request. 652 * 653 * @param path The URI path. 654 * @return A new servlet request. 655 */ 656 public MockServletRequest trace(String path) { 657 return request("TRACE", path, null, null); 658 } 659 660 /** 661 * Returns the headers that were defined in this class. 662 * 663 * @return 664 * The headers that were defined in this class. 665 * <br>Never <jk>null</jk>. 666 */ 667 public Map<String,Object> getHeaders() { 668 return headers; 669 } 670}