/* * The MIT License * * Copyright 2017 Intuit Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.intuit.karate.http; import com.intuit.karate.KarateException; import com.intuit.karate.Script; import com.intuit.karate.ScriptContext; import com.intuit.karate.ScriptValue; import com.intuit.karate.XmlUtils; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import java.io.InputStream; import java.util.List; import java.util.Map; import java.util.Properties; import jdk.nashorn.api.scripting.ScriptObjectMirror; import org.w3c.dom.Node; /** * * @author pthomas3 */ public abstract class HttpClient<T> { protected static final String APPLICATION_JSON = "application/json"; protected static final String APPLICATION_XML = "application/xml"; protected static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; protected static final String TEXT_PLAIN = "text/plain"; protected static final String MULTIPART_FORM_DATA = "multipart/form-data"; protected static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded"; private static final String KARATE_HTTP_PROPERTIES = "karate-http.properties"; protected HttpRequest request; public abstract void configure(HttpConfig config, ScriptContext context); protected abstract T getEntity(List<MultiPartItem> multiPartItems, String mediaType); protected abstract T getEntity(MultiValuedMap formFields, String mediaType); protected abstract T getEntity(InputStream stream, String mediaType); protected abstract T getEntity(String content, String mediaType); protected abstract void buildUrl(String url); protected abstract void buildPath(String path); protected abstract void buildParam(String name, Object... values); protected abstract void buildHeader(String name, Object value, boolean replace); protected abstract void buildCookie(Cookie cookie); protected abstract HttpResponse makeHttpRequest(T entity, long startTime); protected abstract String getRequestUri(); private T getEntityInternal(ScriptValue body, String mediaType) { switch (body.getType()) { case JSON: if (mediaType == null) { mediaType = APPLICATION_JSON; } DocumentContext json = body.getValue(DocumentContext.class); return HttpClient.this.getEntity(json.jsonString(), mediaType); case MAP: if (mediaType == null) { mediaType = APPLICATION_JSON; } Map<String, Object> map = body.getValue(Map.class); DocumentContext mapDoc = JsonPath.parse(map); return HttpClient.this.getEntity(mapDoc.jsonString(), mediaType); case LIST: if (mediaType == null) { mediaType = APPLICATION_JSON; } List list = body.getValue(List.class); DocumentContext listDoc = JsonPath.parse(list); return HttpClient.this.getEntity(listDoc.jsonString(), mediaType); case XML: Node node = body.getValue(Node.class); if (mediaType == null) { mediaType = APPLICATION_XML; } return HttpClient.this.getEntity(XmlUtils.toString(node), mediaType); case INPUT_STREAM: InputStream is = body.getValue(InputStream.class); if (mediaType == null) { mediaType = APPLICATION_OCTET_STREAM; } return HttpClient.this.getEntity(is, mediaType); default: if (mediaType == null) { mediaType = TEXT_PLAIN; } return HttpClient.this.getEntity(body.getAsString(), mediaType); } } private T buildRequestInternal(HttpRequest request, ScriptContext context) { String method = request.getMethod(); if (method == null) { String msg = "'method' is required to make an http call"; context.logger.error(msg); throw new RuntimeException(msg); } method = method.toUpperCase(); request.setMethod(method); this.request = request; String url = request.getUrl(); if (url == null) { String msg = "url not set, please refer to the keyword documentation for 'url'"; context.logger.error(msg); throw new RuntimeException(msg); } buildUrl(url); if (request.getPaths() != null) { for (String path : request.getPaths()) { buildPath(path); } } if (request.getParams() != null) { for (Map.Entry<String, List> entry : request.getParams().entrySet()) { buildParam(entry.getKey(), entry.getValue().toArray()); } } if (request.getHeaders() != null) { for (Map.Entry<String, List> entry : request.getHeaders().entrySet()) { for (Object value : entry.getValue()) { buildHeader(entry.getKey(), value, false); } } } Map<String, Object> configuredHeaders = evalConfiguredHeaders(context); if (configuredHeaders != null) { for (Map.Entry<String, Object> entry : configuredHeaders.entrySet()) { buildHeader(entry.getKey(), entry.getValue(), true); } } if (request.getCookies() != null) { for (Cookie cookie : request.getCookies().values()) { buildCookie(cookie); } } if ("POST".equals(method) || "PUT".equals(method) || "PATCH".equals(method) || "DELETE".equals(method)) { String mediaType = request.getContentType(); if (request.getMultiPartItems() != null) { if (mediaType == null) { mediaType = MULTIPART_FORM_DATA; } return HttpClient.this.getEntity(request.getMultiPartItems(), mediaType); } else if (request.getFormFields() != null) { return getEntity(request.getFormFields(), APPLICATION_FORM_URLENCODED); } else { ScriptValue body = request.getBody(); if ((body == null || body.isNull())) { if ("DELETE".equals(method)) { return null; // traditional DELETE, we also support using a request body for DELETE } else { String msg = "request body is required for a " + method + ", please use the 'request' keyword"; throw new RuntimeException(msg); } } return getEntityInternal(body, mediaType); } } else { return null; } } protected static long getResponseTime(long startTime) { long endTime = System.currentTimeMillis(); long responseTime = endTime - startTime; return responseTime; } public HttpResponse invoke(HttpRequest request, ScriptContext context) { T body = buildRequestInternal(request, context); long startTime = System.currentTimeMillis(); try { HttpResponse response = makeHttpRequest(body, startTime); context.logger.debug("response time in milliseconds: {}", response.getTime()); return response; } catch (Exception e) { long responseTime = getResponseTime(startTime); String message = "http call failed after " + responseTime + " milliseconds for URL: " + getRequestUri(); context.logger.error(e.getMessage() + ", " + message); throw new KarateException(message, e); } } private static Map<String, Object> evalConfiguredHeaders(ScriptContext context) { ScriptValue headersValue = context.getConfiguredHeaders(); switch (headersValue.getType()) { case JS_FUNCTION: ScriptObjectMirror som = headersValue.getValue(ScriptObjectMirror.class); ScriptValue sv = Script.evalFunctionCall(som, null, context); switch (sv.getType()) { case JS_OBJECT: case MAP: return sv.getValue(Map.class); default: return null; } case JSON: DocumentContext json = headersValue.getValue(DocumentContext.class); return json.read("$"); default: return null; } } public static HttpClient construct() { try { InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(KARATE_HTTP_PROPERTIES); if (is == null) { String msg = KARATE_HTTP_PROPERTIES + " not found"; throw new RuntimeException(msg); } Properties props = new Properties(); props.load(is); String className = props.getProperty("client.class"); Class clazz = Class.forName(className); return (HttpClient) clazz.newInstance(); } catch (Exception e) { String msg = "failed to construct class by name: " + e.getMessage() + ", aborting"; throw new RuntimeException(msg); } } }