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 *       .<jsf>create</jsf>(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}