/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch.test.rest.client.http;
import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.elasticsearch.client.support.Headers;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.network.NetworkAddress;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.http.HttpServerTransport;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Map;
/**
* Executable builder for an http request
* Holds an {@link org.apache.http.client.HttpClient} that is used to send the built http request
*/
public class HttpRequestBuilder {
private static final ESLogger logger = Loggers.getLogger(HttpRequestBuilder.class);
static final Charset DEFAULT_CHARSET = Charset.forName("utf-8");
private final CloseableHttpClient httpClient;
private String protocol = "http";
private String host;
private int port;
private String path = "";
private final Map<String, String> params = Maps.newHashMap();
private final Map<String, String> headers = Maps.newHashMap();
private String method = HttpGetWithEntity.METHOD_NAME;
private String body;
public HttpRequestBuilder(CloseableHttpClient httpClient) {
this.httpClient = httpClient;
}
public HttpRequestBuilder host(String host) {
this.host = host;
return this;
}
public HttpRequestBuilder httpTransport(HttpServerTransport httpServerTransport) {
InetSocketTransportAddress transportAddress = (InetSocketTransportAddress) httpServerTransport.boundAddress().publishAddress();
return host(NetworkAddress.format(transportAddress.address().getAddress())).port(transportAddress.address().getPort());
}
public HttpRequestBuilder port(int port) {
this.port = port;
return this;
}
/**
* Sets the path to send the request to. Url encoding needs to be applied by the caller.
* Use {@link #pathParts(String...)} instead if the path needs to be encoded, part by part.
*/
public HttpRequestBuilder path(String path) {
this.path = path;
return this;
}
/**
* Sets the path by providing the different parts (without slashes), which will be properly encoded.
*/
public HttpRequestBuilder pathParts(String... path) {
//encode rules for path and query string parameters are different. We use URI to encode the path, and URLEncoder for each query string parameter (see addParam).
//We need to encode each path part separately though, as each one might contain slashes that need to be escaped, which needs to be done manually.
if (path.length == 0) {
this.path = "/";
return this;
}
StringBuilder finalPath = new StringBuilder();
for (String pathPart : path) {
try {
finalPath.append('/');
// We append "/" to the path part to handle parts that start with - or other invalid characters
URI uri = new URI(null, null, null, -1, "/" + pathPart, null, null);
//manually escape any slash that each part may contain
finalPath.append(uri.getRawPath().substring(1).replaceAll("/", "%2F"));
} catch(URISyntaxException e) {
throw new RuntimeException("unable to build uri", e);
}
}
this.path = finalPath.toString();
return this;
}
public HttpRequestBuilder addParam(String name, String value) {
try {
this.params.put(name, URLEncoder.encode(value, "utf-8"));
return this;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public HttpRequestBuilder addHeaders(Headers headers) {
for (String header : headers.headers().names()) {
this.headers.put(header, headers.headers().get(header));
}
return this;
}
public HttpRequestBuilder addHeader(String name, String value) {
this.headers.put(name, value);
return this;
}
public HttpRequestBuilder protocol(String protocol) {
this.protocol = protocol;
return this;
}
public HttpRequestBuilder method(String method) {
this.method = method;
return this;
}
public HttpRequestBuilder body(String body) {
if (Strings.hasLength(body)) {
this.body = body;
}
return this;
}
public HttpResponse execute() throws IOException {
HttpUriRequest httpUriRequest = buildRequest();
if (logger.isTraceEnabled()) {
StringBuilder stringBuilder = new StringBuilder(httpUriRequest.getMethod()).append(" ").append(httpUriRequest.getURI());
if (Strings.hasLength(body)) {
stringBuilder.append("\n").append(body);
}
logger.trace("sending request \n{}", stringBuilder.toString());
}
for (Map.Entry<String, String> entry : this.headers.entrySet()) {
logger.trace("adding header [{} => {}]", entry.getKey(), entry.getValue());
httpUriRequest.addHeader(entry.getKey(), entry.getValue());
}
try (CloseableHttpResponse closeableHttpResponse = httpClient.execute(httpUriRequest)) {
HttpResponse httpResponse = new HttpResponse(httpUriRequest, closeableHttpResponse);
logger.trace("got response \n{}\n{}", closeableHttpResponse, httpResponse.hasBody() ? httpResponse.getBody() : "");
return httpResponse;
}
}
private HttpUriRequest buildRequest() {
if (HttpGetWithEntity.METHOD_NAME.equalsIgnoreCase(method)) {
return addOptionalBody(new HttpGetWithEntity(buildUri()));
}
if (HttpHead.METHOD_NAME.equalsIgnoreCase(method)) {
checkBodyNotSupported();
return new HttpHead(buildUri());
}
if (HttpOptions.METHOD_NAME.equalsIgnoreCase(method)) {
checkBodyNotSupported();
return new HttpOptions(buildUri());
}
if (HttpDeleteWithEntity.METHOD_NAME.equalsIgnoreCase(method)) {
return addOptionalBody(new HttpDeleteWithEntity(buildUri()));
}
if (HttpPut.METHOD_NAME.equalsIgnoreCase(method)) {
return addOptionalBody(new HttpPut(buildUri()));
}
if (HttpPost.METHOD_NAME.equalsIgnoreCase(method)) {
return addOptionalBody(new HttpPost(buildUri()));
}
throw new UnsupportedOperationException("method [" + method + "] not supported");
}
private URI buildUri() {
StringBuilder uriBuilder = new StringBuilder(protocol).append("://").append(host).append(":").append(port).append(path);
if (params.size() > 0) {
uriBuilder.append("?").append(Joiner.on('&').withKeyValueSeparator("=").join(params));
}
//using this constructor no url encoding happens, as we did everything upfront in addParam and pathPart methods
return URI.create(uriBuilder.toString());
}
private HttpEntityEnclosingRequestBase addOptionalBody(HttpEntityEnclosingRequestBase requestBase) {
if (Strings.hasText(body)) {
requestBase.setEntity(new StringEntity(body, DEFAULT_CHARSET));
}
return requestBase;
}
private void checkBodyNotSupported() {
if (Strings.hasText(body)) {
throw new IllegalArgumentException("request body not supported with head request");
}
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder(method).append(" '")
.append(host).append(":").append(port).append(path).append("'");
if (!params.isEmpty()) {
stringBuilder.append(", params=").append(params);
}
if (Strings.hasLength(body)) {
stringBuilder.append(", body=\n").append(body);
}
return stringBuilder.toString();
}
}