blob: 786ec8be06cfa92c5cf0d6ae8dcc7e06671270a0 [file] [log] [blame]
// ***************************************************************************************************************************
// * 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
* .<jsf>create</jsf>(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;
}
}