1 package com.guinetik.rr.http;
2
3 import com.guinetik.rr.RocketRestOptions;
4 import com.guinetik.rr.auth.TokenExpiredException;
5 import com.guinetik.rr.json.JsonObjectMapper;
6 import com.guinetik.rr.request.RequestSpec;
7 import com.guinetik.rr.util.ResponseLogger;
8 import com.guinetik.rr.util.StreamUtils;
9 import org.slf4j.Logger;
10 import org.slf4j.LoggerFactory;
11
12 import javax.net.ssl.HttpsURLConnection;
13 import javax.net.ssl.SSLContext;
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.io.OutputStream;
17 import java.net.HttpURLConnection;
18 import java.net.URL;
19 import java.nio.charset.StandardCharsets;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.Optional;
23 import java.util.StringJoiner;
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 public class DefaultHttpClient implements RocketClient {
78
79 private static final Logger logger = LoggerFactory.getLogger(DefaultHttpClient.class);
80
81 private String baseUrl;
82 private final RocketRestOptions clientOptions;
83 private SSLContext sslContext;
84
85
86
87
88
89
90 public DefaultHttpClient(String baseUrl) {
91 this(baseUrl, new RocketRestOptions());
92 }
93
94
95
96
97
98
99
100 public DefaultHttpClient(String baseUrl, RocketRestOptions clientOptions) {
101 this.baseUrl = baseUrl.endsWith(HttpConstants.Url.PATH_SEPARATOR) ?
102 baseUrl : baseUrl + HttpConstants.Url.PATH_SEPARATOR;
103 this.clientOptions = clientOptions != null ? clientOptions : new RocketRestOptions();
104 }
105
106 @Override
107 public void configureSsl(SSLContext sslContext) {
108 this.sslContext = sslContext;
109 }
110
111 @Override
112 public void setBaseUrl(String baseUrl) {
113 this.baseUrl = baseUrl;
114 }
115
116
117
118
119
120
121 public RocketRestOptions getClientOptions() {
122 return clientOptions;
123 }
124
125 @Override
126 public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) throws RocketRestException {
127 try {
128 String fullUrl = buildFullUrl(requestSpec);
129 HttpURLConnection connection = configureConnection(fullUrl, requestSpec);
130 setRequestBody(connection, requestSpec);
131
132 return executeRequest(connection, requestSpec);
133 } catch (TokenExpiredException e) {
134 throw e;
135 } catch (Exception e) {
136 if (e instanceof RocketRestException) {
137 throw (RocketRestException) e;
138 }
139 throw new RocketRestException(HttpConstants.Errors.EXECUTE_REQUEST, e);
140 }
141 }
142
143
144
145
146
147
148
149 private String buildFullUrl(RequestSpec<?, ?> requestSpec) throws RocketRestException {
150 String endpoint = requestSpec.getEndpoint();
151
152
153 boolean isAbsoluteUrl = endpoint.startsWith("http://") || endpoint.startsWith("https://");
154
155
156 if (isAbsoluteUrl && !baseUrl.trim().isEmpty() && !baseUrl.equals("/")) {
157 throw new RocketRestException(
158 "Cannot use absolute URL '" + endpoint + "' with base URL '" + baseUrl +
159 "'. Either use a relative path or set base URL to empty string.",
160 400,
161 null);
162 }
163
164
165 if (isAbsoluteUrl) {
166 String fullUrl = endpoint;
167 if (!requestSpec.getQueryParams().isEmpty()) {
168 fullUrl += buildQueryString(requestSpec.getQueryParams());
169 }
170 return fullUrl;
171 }
172
173
174 if (endpoint.startsWith(HttpConstants.Url.PATH_SEPARATOR)) {
175 endpoint = endpoint.substring(1);
176 }
177
178 String fullUrl = baseUrl + endpoint;
179
180 if (!requestSpec.getQueryParams().isEmpty()) {
181 fullUrl += buildQueryString(requestSpec.getQueryParams());
182 }
183
184 return fullUrl;
185 }
186
187
188
189
190 private <Req, Res> HttpURLConnection configureConnection(String fullUrl, RequestSpec<Req, Res> requestSpec)
191 throws IOException {
192 URL url = new URL(fullUrl);
193 HttpURLConnection connection = (HttpURLConnection) url.openConnection();
194
195 if (connection instanceof HttpsURLConnection && sslContext != null) {
196 ((HttpsURLConnection) connection).setSSLSocketFactory(sslContext.getSocketFactory());
197 }
198
199 connection.setConnectTimeout(HttpConstants.Timeouts.DEFAULT_CONNECT_TIMEOUT);
200 connection.setReadTimeout(HttpConstants.Timeouts.DEFAULT_READ_TIMEOUT);
201
202 connection.setRequestMethod(requestSpec.getMethod());
203
204 setRequestHeaders(connection, requestSpec);
205 return connection;
206 }
207
208
209
210
211 private <Req, Res> void setRequestHeaders(HttpURLConnection connection, RequestSpec<Req, Res> requestSpec) {
212 RocketHeaders headers = requestSpec.getHeaders();
213
214 headers.asMap().forEach(connection::setRequestProperty);
215 }
216
217
218
219
220 private <Req, Res> void setRequestBody(HttpURLConnection connection, RequestSpec<Req, Res> requestSpec)
221 throws IOException {
222 boolean hasBody = requestSpec.getBody() != null && isMethodWithBody(requestSpec.getMethod());
223
224 if (hasBody) {
225 connection.setDoOutput(true);
226 String jsonBody;
227
228
229 if (requestSpec.getBody() instanceof String) {
230 jsonBody = (String) requestSpec.getBody();
231 } else {
232
233 jsonBody = JsonObjectMapper.toJsonString(requestSpec.getBody());
234 }
235
236 try (OutputStream os = connection.getOutputStream()) {
237 byte[] input = jsonBody.getBytes(StandardCharsets.UTF_8);
238 os.write(input, 0, input.length);
239 }
240 }
241 }
242
243
244
245
246 private boolean isMethodWithBody(String method) {
247 return method.equals(HttpConstants.Methods.POST) ||
248 method.equals(HttpConstants.Methods.PUT) ||
249 method.equals(HttpConstants.Methods.PATCH);
250 }
251
252
253
254
255 private <Req, Res> Res executeRequest(HttpURLConnection connection, RequestSpec<Req, Res> requestSpec)
256 throws IOException, RocketRestException {
257 if (getClientOptions().getBoolean(RocketRestOptions.LOGGING_ENABLED, true)) {
258 logger.debug("Executing request: {} {}", connection.getRequestMethod(), connection.getURL());
259 }
260 int statusCode = connection.getResponseCode();
261 Map<String, String> responseHeaders = extractResponseHeaders(connection);
262
263 ResponseLogger.logRawResponse(statusCode, responseHeaders, clientOptions);
264
265
266 if (statusCode == HttpConstants.StatusCodes.UNAUTHORIZED) {
267 throw new TokenExpiredException(HttpConstants.Errors.TOKEN_EXPIRED);
268 }
269
270
271 if (statusCode >= HttpConstants.StatusCodes.SUCCESS_MIN &&
272 statusCode < HttpConstants.StatusCodes.SUCCESS_MAX) {
273 return handleSuccessResponse(connection, requestSpec);
274 } else {
275 handleErrorResponse(connection, statusCode);
276
277 return null;
278 }
279 }
280
281
282
283
284 private Map<String, String> extractResponseHeaders(HttpURLConnection connection) {
285 Map<String, String> responseHeaders = new HashMap<>();
286
287 for (int i = 0; ; i++) {
288 String headerName = connection.getHeaderFieldKey(i);
289 String headerValue = connection.getHeaderField(i);
290 if (headerName == null && headerValue == null) {
291 break;
292 }
293 if (headerName != null) {
294 responseHeaders.put(headerName, headerValue);
295 }
296 }
297 return responseHeaders;
298 }
299
300
301
302
303 private <Req, Res> Res handleSuccessResponse(HttpURLConnection connection, RequestSpec<Req, Res> requestSpec)
304 throws IOException {
305
306
307 if (requestSpec.getResponseType() == Void.class) {
308 return null;
309 }
310
311
312 try (InputStream is = connection.getInputStream()) {
313 String responseString = StreamUtils.readInputStreamAsString(is);
314
315
316 ResponseLogger.logResponseBody(responseString, clientOptions);
317
318
319 if (requestSpec.getResponseType() == String.class) {
320 @SuppressWarnings("unchecked")
321 Res result = (Res) responseString;
322 return result;
323 }
324
325
326 return JsonObjectMapper.jsonToObject(responseString, requestSpec.getResponseType());
327 }
328 }
329
330
331
332
333
334
335
336 private void handleErrorResponse(HttpURLConnection connection, int statusCode) throws RocketRestException {
337
338 String errorBody = Optional.ofNullable(connection.getErrorStream())
339 .map(is -> {
340 try {
341 return StreamUtils.readInputStreamAsString(is);
342 } catch (IOException e) {
343 logger.warn("Error reading error stream", e);
344 return null;
345 } finally {
346 try {
347 is.close();
348 } catch (IOException e) {
349
350 }
351 }
352 })
353 .orElse(null);
354
355
356 ResponseLogger.logResponseBody(errorBody, this.getClientOptions());
357
358 throw new RocketRestException(
359 HttpConstants.Errors.REQUEST_FAILED + statusCode,
360 statusCode,
361 errorBody
362 );
363 }
364
365
366
367
368
369
370
371 private String buildQueryString(Map<String, String> params) {
372 if (params.isEmpty()) {
373 return "";
374 }
375 StringJoiner sj = new StringJoiner(
376 HttpConstants.Url.QUERY_SEPARATOR,
377 HttpConstants.Url.QUERY_PREFIX,
378 "");
379
380 params.forEach((key, value) ->
381 sj.add(key + HttpConstants.Url.PARAM_EQUALS + encodeParam(value))
382 );
383 return sj.toString();
384 }
385
386
387
388
389
390
391
392 private String encodeParam(String param) {
393 try {
394 return java.net.URLEncoder.encode(param, HttpConstants.Encoding.UTF8);
395 } catch (Exception e) {
396 logger.warn(HttpConstants.Errors.ENCODE_PARAM, param, e);
397 return param;
398 }
399 }
400 }