/*
* -----------------------------------------------------------------------\
* PerfCake
*
* Copyright (C) 2010 - 2016 the original author or authors.
*
* Licensed 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.perfcake.message.sender;
import org.perfcake.PerfCakeException;
import org.perfcake.message.Message;
import org.perfcake.reporting.MeasurementUnit;
import org.perfcake.util.StringTemplate;
import org.perfcake.util.Utils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Sends messages via HTTP protocol.
*
* @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a>
* @author <a href="mailto:pavel.macik@gmail.com">Pavel Macík</a>
*/
public class HttpSender extends AbstractSender {
/**
* The sender's logger.
*/
private static final Logger log = LogManager.getLogger(HttpSender.class);
/**
* HTTP cookies header.
*/
private static final String COOKIES_HEADER = "Set-Cookie";
/**
* The URL where the HTTP request is sent.
*/
private URL url;
/**
* The HTTP method that will be used.
*/
private Method method = Method.POST;
/**
* A string template determining the HTTP method to be used dynamically for each request.
* If not configured (set to null), static configuration in {@link #method} is used instead.
*/
private StringTemplate dynamicMethod = null;
/**
* HTTP method that should be used for the current send operation, pre-calculated in {@link #preSend(Message, Map, Properties)}.
*/
private Method currentMethod;
/**
* Enumeration on available HTTP methods.
*
* @see java.net.HttpURLConnection#setRequestMethod(String)
*/
public static enum Method {
GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE
}
/**
* The list of response codes that are expected to be returned by HTTP response.
*/
private List<Integer> expectedResponseCodeList = new ArrayList<>();
/**
* The property for expected response codes.
*/
private String expectedResponseCodes = null;
/**
* The HTTP request connection.
*/
protected HttpURLConnection requestConnection;
/**
* When true, cookies are stored between HTTP requests.
*/
private boolean storeCookies = false;
/**
* Cookies storage for each client.
*/
private static ThreadLocal<CookieManager> localCookieManager = new ThreadLocal<>();
/**
* The request payload.
*/
private String payload;
/**
* The request payload length.
*/
private int payloadLength;
@Override
public void doInit(final Properties messageAttributes) throws PerfCakeException {
final String targetUrl = safeGetTarget(messageAttributes);
try {
if(log.isDebugEnabled()){
log.debug("Setting target URL to: " + targetUrl);
}
url = new URL(targetUrl);
} catch (MalformedURLException e) {
throw new PerfCakeException(String.format("Cannot initialize HTTP connection, invalid URL %s: ", targetUrl), e);
}
}
@Override
public void doClose() {
// nop
}
/**
* Sets the value of expectedResponseCodes property.
*
* @param expectedResponseCodes
* The expectedResponseCodes property to set.
* @return Instance of this to support fluent API.
*/
public HttpSender setExpectedResponseCodes(final String expectedResponseCodes) {
this.expectedResponseCodes = expectedResponseCodes;
setExpectedResponseCodesList(expectedResponseCodes.split(","));
return this;
}
/**
* Gets the list of expected response codes.
*
* @return The list of expected response codes.
*/
public List<Integer> getExpectedResponseCodeList() {
return expectedResponseCodeList;
}
/**
* Gets the value of expectedResponseCodes property.
*
* @return The expectedResponseCodes.
*/
public String getExpectedResponseCodes() {
return expectedResponseCodes;
}
/**
* Sets a list of expected response codes.
*
* @param codes
* The array of codes.
* @return Instance of this to support fluent API.
*/
protected HttpSender setExpectedResponseCodesList(final String[] codes) {
final LinkedList<Integer> numCodes = new LinkedList<Integer>();
for (final String code : codes) {
numCodes.add(Integer.parseInt(code.trim()));
}
expectedResponseCodeList = numCodes;
return this;
}
/**
* Checks if the code is expected.
*
* @param code
* Checked response code.
* @return true/false according to if the code is expected or not.
*/
private boolean checkResponseCode(final int code) {
if (expectedResponseCodeList.isEmpty()) {
return true;
}
for (final int i : expectedResponseCodeList) {
if (i == code) {
return true;
}
}
return false;
}
@Override
public void preSend(final Message message, final Properties messageAttributes) throws Exception {
super.preSend(message, messageAttributes);
if (storeCookies && localCookieManager.get() == null) {
localCookieManager.set(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
}
currentMethod = getDynamicMethod(messageAttributes);
payloadLength = 0;
if (message == null) {
payload = null;
} else if (message.getPayload() != null) {
payload = message.getPayload().toString();
payloadLength = payload.length();
}
requestConnection = (HttpURLConnection) url.openConnection();
requestConnection.setRequestMethod(currentMethod.name());
requestConnection.setDoInput(true);
if (currentMethod == Method.POST || currentMethod == Method.PUT) {
requestConnection.setDoOutput(true);
}
requestConnection.setRequestProperty("Content-Type", "text/plain; charset=utf-8");
if (payloadLength > 0) {
requestConnection.setRequestProperty("Content-Length", Integer.toString(payloadLength));
}
if (storeCookies) {
popCookies();
}
if (log.isDebugEnabled()) {
log.debug("Setting HTTP headers: ");
}
// set message properties as HTTP headers
if (message != null) {
for (final Entry<Object, Object> property : message.getProperties().entrySet()) {
final String pKey = property.getKey().toString();
final String pValue = property.getValue().toString();
requestConnection.setRequestProperty(pKey, pValue);
if (log.isDebugEnabled()) {
log.debug(pKey + ": " + pValue);
}
}
}
// set message headers as HTTP headers
if (message != null) {
if (message.getHeaders().size() > 0) {
for (final Entry<Object, Object> property : message.getHeaders().entrySet()) {
final String pKey = property.getKey().toString();
final String pValue = property.getValue().toString();
requestConnection.setRequestProperty(pKey, pValue);
if (log.isDebugEnabled()) {
log.debug(pKey + ": " + pValue);
}
}
}
}
if (log.isDebugEnabled()) {
log.debug("End of HTTP headers.");
}
}
@Override
public Serializable doSend(final Message message, final MeasurementUnit measurementUnit) throws Exception {
int respCode;
requestConnection.connect();
if (payload != null && (currentMethod == Method.POST || currentMethod == Method.PUT)) {
final OutputStreamWriter out = new OutputStreamWriter(requestConnection.getOutputStream(), Utils.getDefaultEncoding());
out.write(payload, 0, payloadLength);
out.flush();
out.close();
requestConnection.getOutputStream().close();
}
respCode = requestConnection.getResponseCode();
if (!checkResponseCode(respCode)) {
final StringBuilder errorMess = new StringBuilder();
errorMess.append("The server returned an unexpected HTTP response code: ").append(respCode).append(" ").append("\"").append(requestConnection.getResponseMessage()).append("\". Expected HTTP codes are ");
for (final int code : expectedResponseCodeList) {
errorMess.append(Integer.toString(code)).append(", ");
}
throw new PerfCakeException(errorMess.substring(0, errorMess.length() - 2) + ".");
}
InputStream rcis;
if (respCode < 400) {
rcis = requestConnection.getInputStream();
} else {
rcis = requestConnection.getErrorStream();
}
String payload = null;
if (rcis != null) {
final char[] cbuf = new char[10 * 1024];
final InputStreamReader read = new InputStreamReader(rcis, Utils.getDefaultEncoding());
// note that Content-Length is available at this point
final StringBuilder sb = new StringBuilder();
int ch = read.read(cbuf);
while (ch != -1) {
sb.append(cbuf, 0, ch);
ch = read.read(cbuf);
}
read.close();
rcis.close();
payload = sb.toString();
}
return payload;
}
@Override
public void postSend(final Message message) throws Exception {
super.postSend(message);
if (storeCookies) {
pushCookies();
}
requestConnection.disconnect();
}
/**
* Stores cookies from request connection in the thread local cookie manager.
*/
private void pushCookies() {
final Map<String, List<String>> headerFields = requestConnection.getHeaderFields();
final List<String> cookiesHeader = headerFields.get(COOKIES_HEADER);
if (cookiesHeader != null) {
for (String cookie : cookiesHeader) {
localCookieManager.get().getCookieStore().add(null, HttpCookie.parse(cookie).get(0));
}
}
}
/**
* Sets the stored cookies to the request connection.
*/
private void popCookies() {
if (localCookieManager.get().getCookieStore().getCookies().size() > 0) {
requestConnection.setRequestProperty("Cookie",
StringUtils.join(localCookieManager.get().getCookieStore().getCookies(), ";"));
}
}
/**
* Gets the value of HTTP method.
*
* @return The HTTP method.
*/
public Method getMethod() {
return method;
}
/**
* Sets the value of HTTP method.
*
* @param method
* The HTTP method to set.
* @return Instance of this to support fluent API.
*/
public HttpSender setMethod(final Method method) {
this.method = method;
return this;
}
/**
* Sets the template used to determine HTTP method dynamically.
*
* @param dynamicMethod
* The string template to dynamically determine HTTP method.
* @return Instance of this to support fluent API.
*/
public HttpSender setDynamicMethod(final String dynamicMethod) {
if (dynamicMethod == null || dynamicMethod.isEmpty()) {
this.dynamicMethod = null;
} else {
this.dynamicMethod = new StringTemplate(dynamicMethod);
}
return this;
}
/**
* Gets the template used to determine HTTP method dynamically.
*
* @param placeholders
* The properties to render the string template.
* @return The HTTP method.
*/
public Method getDynamicMethod(final Properties placeholders) {
if (dynamicMethod == null) {
return this.method;
} else {
return Method.valueOf(dynamicMethod.toString(placeholders));
}
}
/**
* Gets whether the sender will store cookies between requests.
*
* @return If and only if the cookies will be stored between requests.
*/
public boolean isStoreCookies() {
return storeCookies;
}
/**
* Sets whether the sender will store cookies between requests.
*
* @param storeCookies
* True if and only if the cookies should be stored between requests.
* @return Instance of this to support fluent API.
*/
public HttpSender setStoreCookies(final boolean storeCookies) {
this.storeCookies = storeCookies;
return this;
}
}