| // *************************************************************************************************************************** |
| // * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * |
| // * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * |
| // * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * |
| // * with the License. You may obtain a copy of the License at * |
| // * * |
| // * http://www.apache.org/licenses/LICENSE-2.0 * |
| // * * |
| // * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * |
| // * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * |
| // * specific language governing permissions and limitations under the License. * |
| // *************************************************************************************************************************** |
| package org.apache.juneau.rest.mock2; |
| |
| import static org.apache.juneau.rest.util.RestUtils.*; |
| import static org.apache.juneau.internal.StringUtils.*; |
| |
| import java.io.*; |
| import java.util.*; |
| import java.util.concurrent.*; |
| |
| import org.apache.juneau.marshall.*; |
| import org.apache.juneau.parser.*; |
| import org.apache.juneau.rest.*; |
| import org.apache.juneau.rest.annotation.*; |
| import org.apache.juneau.rest.util.*; |
| import org.apache.juneau.serializer.*; |
| |
| /** |
| * Creates a mocked interface against a REST resource class. |
| * |
| * <p> |
| * Allows you to test your REST resource classes without a running servlet container. |
| * |
| * <h5 class='figure'>Example:</h5> |
| * <p class='bcode w800'> |
| * <jk>public class</jk> MockTest { |
| * |
| * <jc>// Our REST resource to test.</jc> |
| * <ja>@Rest</ja>(serializers=JsonSerializer.Simple.<jk>class</jk>, parsers=JsonParser.<jk>class</jk>) |
| * <jk>public static class</jk> MyRest { |
| * |
| * <ja>@RestMethod</ja>(name=<jsf>PUT</jsf>, path=<js>"/String"</js>) |
| * <jk>public</jk> String echo(<ja>@Body</ja> String b) { |
| * <jk>return</jk> b; |
| * } |
| * } |
| * |
| * <ja>@Test</ja> |
| * <jk>public void</jk> testEcho() <jk>throws</jk> Exception { |
| * MockRest |
| * .<jsm>create</jsm>(MyRest.<jk>class</jk>) |
| * .put(<js>"/String"</js>, <js>"'foo'"</js>) |
| * .execute() |
| * .assertStatus(200) |
| * .assertBody(<js>"'foo'"</js>); |
| * } |
| * </p> |
| * |
| * <ul class='seealso'> |
| * <li class='link'>{@doc juneau-rest-mock.MockRest} |
| * </ul> |
| */ |
| public class MockRest implements MockHttpConnection { |
| private static Map<Class<?>,RestContext> CONTEXTS_DEBUG = new ConcurrentHashMap<>(), CONTEXTS_NORMAL = new ConcurrentHashMap<>(); |
| |
| private final RestContext ctx; |
| |
| /** Requests headers to add to every request. */ |
| protected final Map<String,Object> headers; |
| |
| /** Debug mode enabled. */ |
| protected final boolean debug; |
| |
| final String contextPath, servletPath; |
| |
| /** |
| * Constructor. |
| * |
| * @param b Builder. |
| */ |
| protected MockRest(Builder b) { |
| try { |
| debug = b.debug; |
| Class<?> c = b.impl instanceof Class ? (Class<?>)b.impl : b.impl.getClass(); |
| Map<Class<?>,RestContext> contexts = debug ? CONTEXTS_DEBUG : CONTEXTS_NORMAL; |
| if (! contexts.containsKey(c)) { |
| Object o = b.impl instanceof Class ? ((Class<?>)b.impl).newInstance() : b.impl; |
| RestContextBuilder rcb = RestContext.create(o); |
| if (debug) { |
| rcb.debug(Enablement.TRUE); |
| rcb.callLoggerConfig(RestCallLoggerConfig.DEFAULT_DEBUG); |
| } |
| RestContext rc = rcb.build(); |
| if (o instanceof RestServlet) { |
| ((RestServlet)o).setContext(rc); |
| } else { |
| rc.postInit(); |
| } |
| rc.postInitChildFirst(); |
| contexts.put(c, rc); |
| } |
| ctx = contexts.get(c); |
| headers = new LinkedHashMap<>(b.headers); |
| contextPath = b.contextPath; |
| servletPath = b.servletPath; |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| /** |
| * Creates a new builder with the specified REST implementation bean or bean class. |
| * |
| * <p> |
| * No <c>Accept</c> or <c>Content-Type</c> header is specified by default. |
| * |
| * @param impl |
| * The REST bean or bean class annotated with {@link Rest @Rest}. |
| * <br>If a class, it must have a no-arg constructor. |
| * @return A new builder. |
| */ |
| public static Builder create(Object impl) { |
| return new Builder(impl); |
| } |
| |
| /** |
| * Convenience method for creating a MockRest over the specified REST implementation bean or bean class. |
| * |
| * <p> |
| * <c>Accept</c> header is set to <c>"application/json+simple"</c> by default. |
| * <c>Content-Type</c> header is set to <c>"application/json"</c> by default. |
| * |
| * <p> |
| * Equivalent to calling: |
| * <p class='bpcode w800'> |
| * MockRest.create(impl, SimpleJson.<jsf>DEFAULT</jsf>).build(); |
| * </p> |
| * |
| * @param impl |
| * The REST bean or bean class annotated with {@link Rest @Rest}. |
| * <br>If a class, it must have a no-arg constructor. |
| * @return A new {@link MockRest} object. |
| */ |
| public static MockRest build(Object impl) { |
| return build(impl, SimpleJson.DEFAULT); |
| } |
| |
| /** |
| * Convenience method for creating a MockRest over the specified REST implementation bean or bean class. |
| * |
| * <p> |
| * <c>Accept</c> and <c>Content-Type</c> headers are set to the primary media types on the specified marshall. |
| * |
| * <p> |
| * Note that the marshall itself is not involved in any serialization or parsing. |
| * |
| * <p> |
| * Equivalent to calling: |
| * <p class='bpcode w800'> |
| * MockRest.create(impl, SimpleJson.<jsf>DEFAULT</jsf>).marshall(m).build(); |
| * </p> |
| * |
| * @param impl |
| * The REST bean or bean class annotated with {@link Rest @Rest}. |
| * <br>If a class, it must have a no-arg constructor. |
| * @param m |
| * The marshall to use for specifying the <c>Accept</c> and <c>Content-Type</c> headers. |
| * <br>If <jk>null</jk>, headers will be reset. |
| * @return A new {@link MockRest} object. |
| */ |
| public static MockRest build(Object impl, Marshall m) { |
| return create(impl).marshall(m).build(); |
| } |
| |
| /** |
| * Convenience method for creating a MockRest over the specified REST implementation bean or bean class. |
| * |
| * <p> |
| * <c>Accept</c> and <c>Content-Type</c> headers are set to the primary media types on the specified serializer and parser. |
| * |
| * <p> |
| * Note that the marshall itself is not involved in any serialization or parsing. |
| * |
| * <p> |
| * Equivalent to calling: |
| * <p class='bpcode w800'> |
| * MockRest.create(impl, SimpleJson.<jsf>DEFAULT</jsf>).serializer(s).parser(p).build(); |
| * </p> |
| * |
| * @param impl |
| * The REST bean or bean class annotated with {@link Rest @Rest}. |
| * <br>If a class, it must have a no-arg constructor. |
| * @param s |
| * The serializer to use for specifying the <c>Content-Type</c> header. |
| * <br>If <jk>null</jk>, header will be reset. |
| * @param p |
| * The parser to use for specifying the <c>Accept</c> header. |
| * <br>If <jk>null</jk>, header will be reset. |
| * @return A new {@link MockRest} object. |
| */ |
| public static MockRest build(Object impl, Serializer s, Parser p) { |
| return create(impl).serializer(s).parser(p).build(); |
| } |
| |
| /** |
| * Builder class. |
| */ |
| public static class Builder { |
| Object impl; |
| boolean debug; |
| Map<String,Object> headers = new LinkedHashMap<>(); |
| String contextPath, servletPath; |
| |
| Builder(Object impl) { |
| this.impl = impl; |
| } |
| |
| /** |
| * Enable debug mode. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder debug() { |
| this.debug = true; |
| header("X-Debug", true); |
| return this; |
| } |
| |
| /** |
| * Enable no-trace mode. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder noTrace() { |
| header("X-NoTrace", true); |
| return this; |
| } |
| |
| /** |
| * Adds a header to every request. |
| * |
| * @param name The header name. |
| * @param value |
| * The header value. |
| * <br>Can be <jk>null</jk> (will be skipped). |
| * @return This object (for method chaining). |
| */ |
| public Builder header(String name, Object value) { |
| this.headers.put(name, value); |
| return this; |
| } |
| |
| /** |
| * Adds the specified headers to every request. |
| * |
| * @param value |
| * The header values. |
| * <br>Can be <jk>null</jk> (existing values will be cleared). |
| * <br><jk>null</jk> null map values will be ignored. |
| * @return This object (for method chaining). |
| */ |
| public Builder headers(Map<String,Object> value) { |
| if (value != null) |
| this.headers.putAll(value); |
| else |
| this.headers.clear(); |
| return this; |
| } |
| |
| /** |
| * Specifies the <c>Accept</c> header to every request. |
| * |
| * @param value The <c>Accept</c> header value. |
| * @return This object (for method chaining). |
| */ |
| public Builder accept(String value) { |
| return header("Accept", value); |
| } |
| |
| /** |
| * Specifies the <c>Content-Type</c> header to every request. |
| * |
| * @param value The <c>Content-Type</c> header value. |
| * @return This object (for method chaining). |
| */ |
| public Builder contentType(String value) { |
| return header("Content-Type", value); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"application/json"</js>. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder json() { |
| return accept("application/json").contentType("application/json"); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"application/json+simple"</js>. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder simpleJson() { |
| return accept("application/json+simple").contentType("application/json+simple"); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/xml"</js>. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder xml() { |
| return accept("text/xml").contentType("text/xml"); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/html"</js>. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder html() { |
| return accept("text/html").contentType("text/html"); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/plain"</js>. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder plainText() { |
| return accept("text/plain").contentType("text/plain"); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"octal/msgpack"</js>. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder msgpack() { |
| return accept("octal/msgpack").contentType("octal/msgpack"); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/uon"</js>. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder uon() { |
| return accept("text/uon").contentType("text/uon"); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"application/x-www-form-urlencoded"</js>. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder urlEnc() { |
| return accept("application/x-www-form-urlencoded").contentType("application/x-www-form-urlencoded"); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/yaml"</js>. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder yaml() { |
| return accept("text/yaml").contentType("text/yaml"); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to <js>"text/openapi"</js>. |
| * |
| * @return This object (for method chaining). |
| */ |
| public Builder openapi() { |
| return accept("text/openapi").contentType("text/openapi"); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Content-Type</c> header to the primary media type on the specified serializer. |
| * |
| * @param value |
| * The serializer to get the media type from. |
| * <br>If <jk>null</jk>, header will be reset. |
| * @return This object (for method chaining). |
| */ |
| public Builder serializer(Serializer value) { |
| return contentType(value == null ? null : value.getPrimaryMediaType().toString()); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> header to the primary media type on the specified parser. |
| * |
| * @param value |
| * The parser to get the media type from. |
| * <br>If <jk>null</jk>, header will be reset. |
| * @return This object (for method chaining). |
| */ |
| public Builder parser(Parser value) { |
| return accept(value == null ? null : value.getPrimaryMediaType().toString()); |
| } |
| |
| /** |
| * Convenience method for setting the <c>Accept</c> and <c>Content-Type</c> headers to the primary media types on the specified marshall. |
| * |
| * @param value |
| * The marshall to get the media types from. |
| * <br>If <jk>null</jk>, headers will be reset. |
| * @return This object (for method chaining). |
| */ |
| public Builder marshall(Marshall value) { |
| contentType(value == null ? null : value.getSerializer().getPrimaryMediaType().toString()); |
| accept(value == null ? null : value.getParser().getPrimaryMediaType().toString()); |
| return this; |
| } |
| |
| /** |
| * Identifies the context path for the REST resource. |
| * |
| * <p> |
| * If not specified, uses <js>""</js>. |
| * |
| * @param value |
| * The context path. |
| * <br>Must not be <jk>null</jk> and must either be blank or start but not end with a <js>'/'</js> character. |
| * @return This object (for method chaining). |
| */ |
| public Builder contextPath(String value) { |
| validateContextPath(value); |
| this.contextPath = value; |
| return this; |
| } |
| |
| /** |
| * Identifies the servlet path for the REST resource. |
| * |
| * <p> |
| * If not specified, uses <js>""</js>. |
| * |
| * @param value |
| * The servlet path. |
| * <br>Must not be <jk>null</jk> and must either be blank or start but not end with a <js>'/'</js> character. |
| * @return This object (for method chaining). |
| */ |
| public Builder servletPath(String value) { |
| validateServletPath(value); |
| this.servletPath = value; |
| return this; |
| } |
| |
| /** |
| * Create a new {@link MockRest} object based on the settings on this builder. |
| * |
| * @return A new {@link MockRest} object. |
| */ |
| public MockRest build() { |
| return new MockRest(this); |
| } |
| } |
| |
| /** |
| * Performs a REST request against the REST interface. |
| * |
| * @param method The HTTP method |
| * @param path The URI path. |
| * @param headers Optional headers to include in the request. |
| * @param body |
| * The body of the request. |
| * <br>Can be any of the following data types: |
| * <ul> |
| * <li><code><jk>byte</jk>[]</code> |
| * <li>{@link Reader} |
| * <li>{@link InputStream} |
| * <li>{@link CharSequence} |
| * </ul> |
| * Any other types are converted to a string using the <c>toString()</c> method. |
| * @return A new servlet request. |
| */ |
| @Override /* MockHttpConnection */ |
| public MockServletRequest request(String method, String path, Map<String,Object> headers, Object body) { |
| String p = RestUtils.trimContextPath(ctx.getPath(), path); |
| return MockServletRequest.create(method, p).contextPath(emptyIfNull(contextPath)).servletPath(emptyIfNull(servletPath)).body(body).headers(this.headers).headers(headers).debug(debug).restContext(ctx); |
| } |
| |
| /** |
| * Performs a REST request against the REST interface. |
| * |
| * @param method The HTTP method |
| * @param path The URI path. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest request(String method, String path) { |
| return request(method, path, null, null); |
| } |
| |
| /** |
| * Performs a REST request against the REST interface. |
| * |
| * @param method The HTTP method |
| * @param path The URI path. |
| * @param body |
| * The body of the request. |
| * <br>Can be any of the following data types: |
| * <ul> |
| * <li><code><jk>byte</jk>[]</code> |
| * <li>{@link Reader} |
| * <li>{@link InputStream} |
| * <li>{@link CharSequence} |
| * </ul> |
| * Any other types are converted to a string using the <c>toString()</c> method. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest request(String method, String path, Object body) { |
| return request(method, path, null, body); |
| } |
| |
| /** |
| * Performs a REST request against the REST interface. |
| * |
| * @param method The HTTP method |
| * @param headers Optional headers to include in the request. |
| * @param path The URI path. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest request(String method, Map<String,Object> headers, String path) { |
| return request(method, path, headers, null); |
| } |
| |
| /** |
| * Perform a GET request. |
| * |
| * @param path The URI path. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest get(String path) { |
| return request("GET", path, null, null); |
| } |
| |
| /** |
| * Shortcut for <code>get(<js>""</js>)</code> |
| * |
| * @return A new servlet request. |
| */ |
| public MockServletRequest get() { |
| return get(""); |
| } |
| |
| /** |
| * Perform a PUT request. |
| * |
| * @param path The URI path. |
| * @param body |
| * The body of the request. |
| * <br>Can be any of the following data types: |
| * <ul> |
| * <li><code><jk>byte</jk>[]</code> |
| * <li>{@link Reader} |
| * <li>{@link InputStream} |
| * <li>{@link CharSequence} |
| * </ul> |
| * Any other types are converted to a string using the <c>toString()</c> method. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest put(String path, Object body) { |
| return request("PUT", path, null, body); |
| } |
| |
| /** |
| * Perform a POST request. |
| * |
| * @param path The URI path. |
| * @param body |
| * The body of the request. |
| * <br>Can be any of the following data types: |
| * <ul> |
| * <li><code><jk>byte</jk>[]</code> |
| * <li>{@link Reader} |
| * <li>{@link InputStream} |
| * <li>{@link CharSequence} |
| * </ul> |
| * Any other types are converted to a string using the <c>toString()</c> method. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest post(String path, Object body) { |
| return request("POST", path, null, body); |
| } |
| |
| /** |
| * Perform a DELETE request. |
| * |
| * @param path The URI path. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest delete(String path) { |
| return request("DELETE", path, null, null); |
| } |
| |
| /** |
| * Perform a HEAD request. |
| * |
| * @param path The URI path. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest head(String path) { |
| return request("HEAD", path, null, null); |
| } |
| |
| /** |
| * Perform an OPTIONS request. |
| * |
| * @param path The URI path. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest options(String path) { |
| return request("OPTIONS", path, null, null); |
| } |
| |
| /** |
| * Perform a PATCH request. |
| * |
| * @param path The URI path. |
| * @param body |
| * The body of the request. |
| * <br>Can be any of the following data types: |
| * <ul> |
| * <li><code><jk>byte</jk>[]</code> |
| * <li>{@link Reader} |
| * <li>{@link InputStream} |
| * <li>{@link CharSequence} |
| * </ul> |
| * Any other types are converted to a string using the <c>toString()</c> method. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest patch(String path, Object body) { |
| return request("PATCH", path, null, body); |
| } |
| |
| /** |
| * Perform a CONNECT request. |
| * |
| * @param path The URI path. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest connect(String path) { |
| return request("CONNECT", path, null, null); |
| } |
| |
| /** |
| * Perform a TRACE request. |
| * |
| * @param path The URI path. |
| * @return A new servlet request. |
| */ |
| public MockServletRequest trace(String path) { |
| return request("TRACE", path, null, null); |
| } |
| |
| /** |
| * Returns the headers that were defined in this class. |
| * |
| * @return |
| * The headers that were defined in this class. |
| * <br>Never <jk>null</jk>. |
| */ |
| public Map<String,Object> getHeaders() { |
| return headers; |
| } |
| } |