1 package com.atlassian.sal.core.net;
2
3 import com.atlassian.sal.api.net.Request;
4 import com.atlassian.sal.api.net.RequestFilePart;
5 import com.atlassian.sal.api.net.Response;
6 import com.atlassian.sal.api.net.ResponseException;
7 import com.atlassian.sal.api.net.ResponseHandler;
8 import com.atlassian.sal.api.net.ResponseProtocolException;
9 import com.atlassian.sal.api.net.ResponseStatusException;
10 import com.atlassian.sal.api.net.ReturningResponseHandler;
11 import com.google.common.base.Preconditions;
12 import org.apache.http.HttpHost;
13 import org.apache.http.NameValuePair;
14 import org.apache.http.auth.AuthScope;
15 import org.apache.http.auth.UsernamePasswordCredentials;
16 import org.apache.http.client.ClientProtocolException;
17 import org.apache.http.client.config.CookieSpecs;
18 import org.apache.http.client.config.RequestConfig;
19 import org.apache.http.client.entity.UrlEncodedFormEntity;
20 import org.apache.http.client.methods.CloseableHttpResponse;
21 import org.apache.http.client.methods.HttpPost;
22 import org.apache.http.client.methods.HttpPut;
23 import org.apache.http.client.methods.HttpUriRequest;
24 import org.apache.http.client.methods.RequestBuilder;
25 import org.apache.http.client.protocol.HttpClientContext;
26 import org.apache.http.entity.ContentType;
27 import org.apache.http.entity.StringEntity;
28 import org.apache.http.entity.mime.MultipartEntityBuilder;
29 import org.apache.http.impl.auth.BasicScheme;
30 import org.apache.http.impl.client.CloseableHttpClient;
31 import org.apache.http.message.BasicNameValuePair;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import java.io.IOException;
36 import java.nio.charset.StandardCharsets;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.LinkedList;
41 import java.util.List;
42 import java.util.Map;
43
44
45
46
47 public class HttpClientRequest<T extends Request<?, ?>, RESP extends Response> implements Request<HttpClientRequest<?, ?>, HttpClientResponse> {
48 private static final Logger log = LoggerFactory.getLogger(HttpClientRequest.class);
49
50 private final CloseableHttpClient httpClient;
51 private final List<NameValuePair> requestParameters;
52 protected final HttpClientContext httpClientContext;
53 final RequestBuilder requestBuilder;
54 final RequestConfig.Builder requestConfigBuilder;
55
56
57 private final Map<String, List<String>> headers = new HashMap<>();
58
59 public HttpClientRequest(CloseableHttpClient httpClient, HttpClientContext httpClientContext, MethodType initialMethodType, String initialUrl) {
60 this.httpClient = httpClient;
61 this.httpClientContext = httpClientContext;
62 this.requestBuilder = RequestBuilder.create(initialMethodType.toString()).setUri(initialUrl);
63 this.requestParameters = new LinkedList<>();
64
65 final ConnectionConfig connectionConfig = new SystemPropertiesConnectionConfig();
66 this.requestConfigBuilder = RequestConfig.custom()
67 .setConnectTimeout(connectionConfig.getConnectionTimeout())
68 .setSocketTimeout(connectionConfig.getSocketTimeout())
69 .setMaxRedirects(connectionConfig.getMaxRedirects())
70 .setCookieSpec(CookieSpecs.STANDARD);
71 }
72
73 @Override
74 public String execute() throws ResponseException {
75 return executeAndReturn(response -> {
76 if (!response.isSuccessful()) {
77 throw new ResponseStatusException("Unexpected response received. Status code: " + response.getStatusCode(),
78 response);
79 }
80 return response.getResponseBodyAsString();
81 });
82 }
83
84 @Override
85 public void execute(final ResponseHandler<? super HttpClientResponse> responseHandler) throws ResponseException {
86 executeAndReturn((ReturningResponseHandler<HttpClientResponse, Void>) response -> {
87 responseHandler.handle(response);
88 return null;
89 });
90 }
91
92 @Override
93 public <RET> RET executeAndReturn(final ReturningResponseHandler<? super HttpClientResponse, RET> responseHandler)
94 throws ResponseException {
95 if (!requestParameters.isEmpty()) {
96 requestBuilder.setEntity(new UrlEncodedFormEntity(requestParameters, StandardCharsets.UTF_8));
97 }
98 final HttpUriRequest request = requestBuilder.setConfig(requestConfigBuilder.build()).build();
99 log.debug("Executing request:{}", request);
100
101 try (final CloseableHttpResponse response = httpClient.execute(request, httpClientContext)) {
102 return responseHandler.handle(new HttpClientResponse(response));
103 } catch (ClientProtocolException cpe) {
104 throw new ResponseProtocolException(cpe);
105 } catch (IOException e) {
106 throw new ResponseException(e);
107 }
108 }
109
110 @Override
111 public Map<String, List<String>> getHeaders() {
112 return Collections.unmodifiableMap(headers);
113 }
114
115 @Override
116 public HttpClientRequest addBasicAuthentication(final String hostname, final String username, final String password) {
117 httpClientContext.getCredentialsProvider().setCredentials(
118 new AuthScope(hostname, AuthScope.ANY_PORT),
119 new UsernamePasswordCredentials(username, password));
120 httpClientContext.getAuthCache().put(new HttpHost(hostname), new BasicScheme());
121 return this;
122 }
123
124 @Override
125 public HttpClientRequest setConnectionTimeout(final int connectionTimeout) {
126 requestConfigBuilder.setConnectionRequestTimeout(connectionTimeout);
127 return this;
128 }
129
130 @Override
131 public HttpClientRequest setSoTimeout(final int soTimeout) {
132 requestConfigBuilder.setSocketTimeout(soTimeout);
133 return this;
134 }
135
136 @Override
137 public HttpClientRequest setUrl(final String url) {
138 requestBuilder.setUri(url);
139 return this;
140 }
141
142 @Override
143 public HttpClientRequest setRequestBody(final String requestBody) {
144 return setRequestBody(requestBody, ContentType.TEXT_PLAIN.getMimeType());
145 }
146
147 @Override
148 public HttpClientRequest setRequestBody(final String requestBodyString, final String contentTypeString) {
149 Preconditions.checkNotNull(requestBodyString);
150 Preconditions.checkNotNull(contentTypeString);
151 Preconditions.checkState(isRequestBodyMethod(), "Only PUT or POST methods accept a request body.");
152
153 requestBuilder.setEntity(new StringEntity(requestBodyString, ContentType.create(contentTypeString, StandardCharsets.UTF_8)));
154 return this;
155 }
156
157 @Override
158 public HttpClientRequest setFiles(final List<RequestFilePart> requestBodyFiles) {
159 Preconditions.checkNotNull(requestBodyFiles);
160 Preconditions.checkState(isRequestBodyMethod(), "Only PUT or POST methods accept a request body.");
161
162 final MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
163
164 for (RequestFilePart requestBodyFile : requestBodyFiles) {
165 final ContentType fileContentType = ContentType.create(requestBodyFile.getContentType());
166 multipartEntityBuilder.addBinaryBody(requestBodyFile.getParameterName(), requestBodyFile.getFile(), fileContentType, requestBodyFile.getFileName());
167 }
168
169 requestBuilder.setEntity(multipartEntityBuilder.build());
170 return this;
171 }
172
173 @Override
174 public HttpClientRequest addRequestParameters(final String... params) {
175 Preconditions.checkNotNull(params);
176 Preconditions.checkState(isRequestBodyMethod(), "Only PUT or POST methods accept a request body.");
177
178 if (params.length % 2 != 0) {
179 throw new IllegalArgumentException("You must enter an even number of arguments.");
180 }
181
182 for (int i = 0; i < params.length; i += 2) {
183 final String name = params[i];
184 final String value = params[i + 1];
185 requestParameters.add(new BasicNameValuePair(name, value));
186 }
187
188 return this;
189 }
190
191 private boolean isRequestBodyMethod() {
192 final String methodType = requestBuilder.getMethod();
193 return HttpPost.METHOD_NAME.equals(methodType) || HttpPut.METHOD_NAME.equals(methodType);
194 }
195
196 @Override
197 public HttpClientRequest addHeader(final String headerName, final String headerValue) {
198 headers.computeIfAbsent(headerName, k -> new ArrayList<>())
199 .add(headerValue);
200 requestBuilder.addHeader(headerName, headerValue);
201 return this;
202 }
203
204 @Override
205 public HttpClientRequest setHeader(final String headerName, final String headerValue) {
206 headers.put(headerName, new ArrayList<>(Collections.singletonList(headerValue)));
207 requestBuilder.setHeader(headerName, headerValue);
208 return this;
209 }
210
211 @Override
212 public HttpClientRequest setFollowRedirects(final boolean follow) {
213 requestConfigBuilder.setRedirectsEnabled(follow);
214 return this;
215 }
216
217 @Override
218 public HttpClientRequest setEntity(final Object entity) {
219 throw new UnsupportedOperationException("This SAL request does not support object marshalling. Use the RequestFactory component instead.");
220 }
221 }