/**
* Candybean is a next generation automation and testing framework suite.
* It is a collection of components that foster test automation, execution
* configuration, data abstraction, results illustration, tag-based execution,
* top-down and bottom-up batches, mobile variants, test translation across
* languages, plain-language testing, and web service testing.
* Copyright (C) 2013 SugarCRM, Inc. <candybean@sugarcrm.com>
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* <p>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* <p>
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sugarcrm.candybean.webservices;
import com.sugarcrm.candybean.exceptions.CandybeanException;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.*;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
/**
* Helps make web-service calls and supports DELETE, GET, POST, and PUT
*/
public class WS {
public enum OP {DELETE, GET, POST, PUT}
// The list of HTTP return codes indicating success
private static final Set<Integer> ACCEPTABLE_RETURN_CODE_SET = new HashSet<>(Arrays.asList(
new Integer[]{200, 201, 202}));
/**
* Send a DELETE, GET, POST, or PUT http request
*
* @param op The type of http request
* @param uri The http endpoint
* @param headers Map of header key value pairs
* @param body String representation of the request body
* @return Key Value pairs of the response
* @throws Exception When http request failed
* @deprecated Use {@link #request(OP, String, Map, String, ContentType)}
*/
@Deprecated
public static Map<String, Object> request(OP op, String uri, Map<String, String> headers, String body) throws Exception {
return request(op, uri, headers, body, ContentType.DEFAULT_TEXT);
}
/**
* Send a DELETE, GET, POST, or PUT http request
*
* @param op The type of http request
* @param uri The http endpoint
* @param payload Map of key value pairs for the body
* @param postHeaders List of Maps of header key value pairs
* @param body String representation of the request body (ignored)
* @return Key Value pairs of the response
* @throws Exception When http request failed
* @deprecated This is a work around for old compatibility, use {@link #request(OP, String, Map, String, ContentType)}
* or {@link #request(OP, String, Map, Map)}
*/
@Deprecated
public static Map<String, Object> request(OP op, String uri, Map<String, String> payload, String body, ArrayList<HashMap<String, String>> postHeaders) throws Exception {
HashMap<String, String> headers = new HashMap<>();
for (HashMap<String, String> map : postHeaders) {
for (Map.Entry<String, String> entry : map.entrySet()) {
headers.put(entry.getKey(), entry.getValue());
}
}
HashMap<String, Object> newPayload = new HashMap<>();
for (Map.Entry<String, String> entry : payload.entrySet()) {
newPayload.put(entry.getKey(), entry.getValue());
}
if (body == null) {
return request(op, uri, headers, newPayload);
} else {
return request(op, uri, headers, body, ContentType.DEFAULT_TEXT);
}
}
/**
* Send a DELETE, GET, POST, or PUT http request using a JSON body
*
* @param op The type of http request
* @param uri The http endpoint
* @param headers Map of header key value pairs
* @param body Map of Key Value pairs
* @return Key Value pairs of the response
* @throws Exception If HTTP request failed
*/
public static Map<String, Object> request(OP op, String uri, Map<String, String> headers, Map<String, Object> body) throws Exception {
return request(op, uri, headers, body, ContentType.APPLICATION_JSON);
}
/**
* Send a DELETE, GET, POST, or PUT http request, intended for multipart data and JSON requests
*
* @param op The type of http request
* @param uri The http endpoint
* @param headers Map of header key value pairs
* @param body A recursive map representing a JSON, where Object is one of String|Map<String,Object>
* @param contentType The intended content type of the body
* @return Key Value pairs of the response
* @throws Exception If HTTP request failed
*/
public static Map<String, Object> request(OP op, String uri, Map<String, String> headers, Map<String, Object> body, ContentType contentType) throws Exception {
if (!verifyBody(body)) {
throw new CandybeanException("Body is not representable as JSON");
}
switch (op) {
case DELETE:
return handleRequest(new HttpDelete(uri), headers);
case GET:
return handleRequest(new HttpGet(uri), headers);
case POST:
HttpPost post = new HttpPost(uri);
addBodyToRequest(post, body, contentType);
return handleRequest(post, headers);
case PUT:
HttpPut put = new HttpPut(uri);
addBodyToRequest(put, body, contentType);
return handleRequest(put, headers);
default:
/*
* JLS 13.4.26: Adding or reordering constants in an enum type will not break compatibility with
* pre-existing binaries.
* Thus we include a default in the case that a future version of the enum has a case which is not
* one of the above
*/
throw new Exception("Unrecognized OP type... Perhaps your binaries are the wrong version?");
}
}
/**
* Send a DELETE, GET, POST, or PUT http request
*
* @param op The type of http request
* @param uri The http endpoint
* @param headers Map of header key value pairs
* @param body String representation of the request body
* @param contentType The content type of the body
* @return Key Value pairs of the response
* @throws CandybeanException When http request failed
*/
public static Map<String, Object> request(OP op, String uri, Map<String, String> headers, String body, ContentType contentType) throws CandybeanException {
switch (op) {
case DELETE:
return handleRequest(new HttpDelete(uri), headers);
case GET:
return handleRequest(new HttpGet(uri), headers);
case POST:
HttpPost post = new HttpPost(uri);
if (body != null) {
post.setEntity(new StringEntity(body, contentType));
}
return handleRequest(post, headers);
case PUT:
HttpPut put = new HttpPut(uri);
if (body != null) {
put.setEntity(new StringEntity(body, contentType));
}
return handleRequest(put, headers);
default:
/*
* JLS 13.4.26: Adding or reordering constants in an enum type will not break compatibility with
* pre-existing binaries.
* Thus we include a default in the case that a future version of the enum has a case which is not
* one of the above
*/
throw new CandybeanException("Unrecognized OP type... Perhaps your binaries are the wrong version?");
}
}
/**
* Protected method to handle sending and receiving an http request
*
* @param request The Http request that is being send
* @param headers Map of Key Value header pairs
* @return Key Value pairs of the response
* @throws CandybeanException If the response is null or not an acceptable HTTP code
*/
protected static Map<String, Object> handleRequest(HttpUriRequest request, Map<String, String> headers) throws CandybeanException {
// Add the request headers and execute the request
for (Map.Entry<String, String> header : headers.entrySet()) {
request.addHeader(new BasicHeader(header.getKey(), header.getValue()));
}
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
try {
HttpResponse response = httpClient.execute(request);
// Check for invalid responses or error return codes
if (response == null) {
throw new CandybeanException("Http Request failed: Response null");
}
// Cast the response into a Map and return
JSONObject parse = (JSONObject) JSONValue.parse(new InputStreamReader(response.getEntity().getContent()));
@SuppressWarnings("unchecked")
Map<String, Object> mapParse = (Map<String, Object>) parse;
int code = response.getStatusLine().getStatusCode();
if (!ACCEPTABLE_RETURN_CODE_SET.contains(code)) {
throw new CandybeanException("HTTP request received HTTP code: " + code + "\n"
+ "Response: " + response.toString());
} else if (mapParse == null) {
throw new CandybeanException("Could not format response\n"
+ "Response: " + response.toString());
}
return mapParse;
} catch (IOException | IllegalStateException e) {
// Cast the other possible exceptions as a CandybeanException
throw new CandybeanException(e);
}
}
/**
* Private helper method to abstract creating a POST/PUT request.
* Side Effect: Adds the body to the request
*
* @param request A PUT or POST request
* @param body Map of Key Value pairs
* @param contentType The intended content type of the body
*/
protected static void addBodyToRequest(HttpEntityEnclosingRequestBase request, Map<String, Object> body, ContentType contentType) {
if (body != null) {
if (contentType == ContentType.MULTIPART_FORM_DATA) {
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
for (Map.Entry<String, Object> entry : body.entrySet()) {
builder.addTextBody(entry.getKey(), (String) entry.getValue());
}
request.setEntity(builder.build());
} else {
JSONObject jsonBody = new JSONObject(body);
StringEntity strBody = new StringEntity(jsonBody.toJSONString(), ContentType.APPLICATION_JSON);
request.setEntity(strBody);
}
}
}
/**
* Verifies that a body is correctly formed
*
* @param body The body to verify
* @return True if the body is correctly formed, else False;
*/
@SuppressWarnings("unchecked")
protected static boolean verifyBody(Map<String, Object> body) {
for (Map.Entry<String, Object> entry : body.entrySet()) {
if (entry.getValue() instanceof Map) {
if (!verifyBody((Map<String, Object>) entry.getValue())) {
return false;
}
} else if (!(entry.getValue() instanceof String)) {
return false;
}
}
return true;
}
}