View Javadoc
1   /**
2    * Copyright 2013-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.common.http.service;
17  
18  import static com.google.common.base.Optional.absent;
19  import static com.google.common.base.Optional.fromNullable;
20  import static com.google.common.base.Preconditions.checkState;
21  import static com.google.common.collect.Lists.newArrayList;
22  import static java.lang.System.currentTimeMillis;
23  import static org.kuali.common.http.model.HttpStatus.SUCCESS;
24  import static org.kuali.common.util.base.Exceptions.illegalState;
25  import static org.kuali.common.util.base.Threads.sleep;
26  import static org.kuali.common.util.log.Loggers.newLogger;
27  
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.util.List;
31  
32  import org.apache.commons.io.IOUtils;
33  import org.apache.http.HttpEntity;
34  import org.apache.http.client.HttpRequestRetryHandler;
35  import org.apache.http.client.config.RequestConfig;
36  import org.apache.http.client.methods.CloseableHttpResponse;
37  import org.apache.http.client.methods.HttpGet;
38  import org.apache.http.config.SocketConfig;
39  import org.apache.http.impl.client.CloseableHttpClient;
40  import org.apache.http.impl.client.HttpClients;
41  import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
42  import org.kuali.common.http.model.HttpContext;
43  import org.kuali.common.http.model.HttpRequestResult;
44  import org.kuali.common.http.model.HttpStatus;
45  import org.kuali.common.http.model.HttpWaitResult;
46  import org.kuali.common.util.FormatUtils;
47  import org.slf4j.Logger;
48  
49  import com.google.common.base.Optional;
50  
51  public class DefaultHttpService implements HttpService {
52  
53  	private static final Logger logger = newLogger();
54  
55  	@Override
56  	public HttpWaitResult wait(String url) {
57  		return wait(HttpContext.create(url));
58  	}
59  
60  	@Override
61  	public HttpWaitResult wait(HttpContext context) {
62  		HttpWaitResult result = getWaitResult(context);
63  		HttpStatus actual = result.getStatus();
64  		if (!context.isQuiet()) {
65  			checkState(SUCCESS.equals(result.getStatus()), "[%s] returned [%s]", context.getUrl(), actual);
66  		}
67  		return result;
68  	}
69  
70  	protected HttpWaitResult getWaitResult(HttpContext context) {
71  		logger.debug(context.getUrl());
72  		CloseableHttpClient client = getHttpClient(context);
73  		long start = currentTimeMillis();
74  		long end = start + context.getOverallTimeoutMillis();
75  		List<HttpRequestResult> requestResults = newArrayList();
76  		Object[] args = { context.getLogMsgPrefix(), context.getUrl(), FormatUtils.getTime(context.getOverallTimeoutMillis()) };
77  		if (!context.isQuiet()) {
78  			logger.info("{} - [{}] - [Timeout in {}]", args);
79  		}
80  		int retryAttempts = 0;
81  		for (;;) {
82  			HttpRequestResult rr = doRequest(client, context);
83  			requestResults.add(rr);
84  			if (!isFinishState(context, rr, end, retryAttempts)) {
85  				logHttpRequestResult(context.getLogMsgPrefix(), rr, context.getUrl(), end, context.isQuiet());
86  				sleep(context.getSleepIntervalMillis());
87  			} else {
88  				HttpStatus status = getResultStatus(context, retryAttempts, rr, end);
89  				HttpWaitResult waitResult = HttpWaitResult.builder(status, rr, start).requestResults(requestResults).build();
90  				logWaitResult(waitResult, context.getUrl(), context.getLogMsgPrefix(), context.isQuiet());
91  				return waitResult;
92  			}
93  			retryAttempts++;
94  		}
95  	}
96  
97  	protected void logHttpRequestResult(String logMsgPrefix, HttpRequestResult result, String url, long end, boolean quiet) {
98  		String statusText = getStatusText(result);
99  		String timeout = FormatUtils.getTime(end - currentTimeMillis());
100 		Object[] args = { logMsgPrefix, url, statusText, timeout };
101 		if (!quiet) {
102 			logger.info("{} - [{}] - [{}] - [Timeout in {}]", args);
103 		}
104 	}
105 
106 	protected void logWaitResult(HttpWaitResult result, String url, String logMsgPrefix, boolean quiet) {
107 		String status = result.getStatus().toString();
108 		String elapsed = FormatUtils.getTime(result.getStop() - result.getStart());
109 		String statusText = getStatusText(result.getFinalRequestResult());
110 		Object[] args = { logMsgPrefix, url, status, statusText, elapsed };
111 		if (!quiet) {
112 			logger.info("{} - [{}] - [{} - {}]  Total time: {}", args);
113 		}
114 	}
115 
116 	protected String getStatusText(HttpRequestResult result) {
117 		if (result.getException().isPresent()) {
118 			return result.getStatusText();
119 		} else {
120 			int code = result.getStatusCode().get();
121 			return code + " - " + result.getStatusText();
122 		}
123 	}
124 
125 	protected HttpStatus getResultStatus(HttpContext context, int retryAttempts, HttpRequestResult rr, long end) {
126 		// Make sure we haven't exceeded the max number of retry attempts
127 		if (maxRetriesExceeded(context, retryAttempts)) {
128 			return HttpStatus.MAX_RETRIES_EXCEEDED;
129 		}
130 
131 		// If we've gone past our max allotted time, we've timed out
132 		if (rr.getStop() > end) {
133 			return HttpStatus.TIMEOUT;
134 		}
135 
136 		// Don't think we'll ever fall into this. Logic always continues re-trying until timeout is exceeded if an IO exception is returned
137 		if (rr.getException().isPresent()) {
138 			return HttpStatus.IO_EXCEPTION;
139 		}
140 
141 		// If we have not timed out and there is no exception, we must have gotten a valid http response code back
142 		checkState(rr.getStatusCode().isPresent(), "statusCode should never be null here");
143 
144 		// If there is a status code and it matches a success code, we are done
145 		if (isSuccess(context.getSuccessCodes(), rr.getStatusCode().get())) {
146 			return HttpStatus.SUCCESS;
147 		} else {
148 			return HttpStatus.INVALID_HTTP_STATUS_CODE;
149 		}
150 	}
151 
152 	protected boolean maxRetriesExceeded(HttpContext context, int retryAttempts) {
153 		if (context.getMaxRetries().isPresent()) {
154 			return retryAttempts > context.getMaxRetries().get();
155 		} else {
156 			return false;
157 		}
158 	}
159 
160 	protected boolean quitTrying(HttpContext context, int retryAttempts) {
161 		if (context.getMaxRetries().isPresent()) {
162 			return retryAttempts >= context.getMaxRetries().get();
163 		} else {
164 			return false;
165 		}
166 	}
167 
168 	protected boolean isFinishState(HttpContext context, HttpRequestResult rr, long end, int retryAttempts) {
169 		// If we've gone past the number of max retries allowed, we are done
170 		if (quitTrying(context, retryAttempts)) {
171 			return true;
172 		}
173 
174 		// If we've gone past our max allotted time, we are done
175 		if (rr.getStop() > end) {
176 			return true;
177 		}
178 
179 		// If there is no status code at all, we need to keep trying
180 		if (!rr.getStatusCode().isPresent()) {
181 			return false;
182 		}
183 
184 		// If there is a status code and it matches a success code, we are done
185 		if (isSuccess(context.getSuccessCodes(), rr.getStatusCode().get())) {
186 			return true;
187 		}
188 
189 		// If there is a status code and it matches a continue waiting code, we need to keep trying
190 		if (isContinueWaiting(context.getContinueWaitingCodes(), rr.getStatusCode().get())) {
191 			return false;
192 		} else {
193 			// We got an http status code, but it wasn't one we were expecting. We need to fail
194 			return true;
195 		}
196 	}
197 
198 	protected HttpRequestResult doRequest(CloseableHttpClient client, HttpContext context) {
199 		long start = currentTimeMillis();
200 		try {
201 			HttpGet httpGet = new HttpGet(context.getUrl());
202 			CloseableHttpResponse response = client.execute(httpGet);
203 			Optional<String> responseBody = getResponseBodyAsString(response, context);
204 			int statusCode = response.getStatusLine().getStatusCode();
205 			String statusText = response.getStatusLine().getReasonPhrase();
206 			return HttpRequestResult.builder(statusText, statusCode, responseBody, start).build();
207 		} catch (IOException e) {
208 			return HttpRequestResult.builder(e, start).build();
209 		}
210 	}
211 
212 	protected Optional<String> getResponseBodyAsString(CloseableHttpResponse response, HttpContext context) {
213 		InputStream in = null;
214 		try {
215 			Optional<HttpEntity> entity = fromNullable(response.getEntity());
216 			if (!entity.isPresent()) {
217 				return absent();
218 			}
219 			in = entity.get().getContent();
220 			byte[] buffer = new byte[4096];
221 			int length = in.read(buffer);
222 			long bytesRead = 0;
223 			StringBuilder sb = new StringBuilder();
224 			while (length != -1) {
225 				String content = new String(buffer, 0, length, context.getEncoding());
226 				sb.append(content);
227 				bytesRead += length;
228 				if (isMaxBytes(bytesRead, context)) {
229 					break;
230 				}
231 				length = in.read(buffer);
232 			}
233 			return Optional.of(sb.toString());
234 		} catch (IOException e) {
235 			throw illegalState("unexpected io error", e);
236 		} finally {
237 			IOUtils.closeQuietly(response);
238 			IOUtils.closeQuietly(in);
239 		}
240 	}
241 
242 	protected boolean isMaxBytes(long bytesRead, HttpContext context) {
243 		if (context.getMaxBytes().isPresent()) {
244 			long max = context.getMaxBytes().get();
245 			return bytesRead >= max;
246 		} else {
247 			return false;
248 		}
249 	}
250 
251 	protected boolean isSuccess(List<Integer> successCodes, int resultCode) {
252 		return isMatch(resultCode, successCodes);
253 	}
254 
255 	protected boolean isContinueWaiting(List<Integer> continueWaitingCodes, int resultCode) {
256 		return isMatch(resultCode, continueWaitingCodes);
257 	}
258 
259 	protected boolean isMatch(int i, List<Integer> integers) {
260 		for (int integer : integers) {
261 			if (i == integer) {
262 				return true;
263 			}
264 		}
265 		return false;
266 	}
267 
268 	protected CloseableHttpClient getHttpClient(HttpContext context) {
269 		int timeout = context.getRequestTimeoutMillis();
270 		SocketConfig socketConfig = SocketConfig.custom().setSoTimeout(timeout).build();
271 		HttpRequestRetryHandler retryHandler = new StandardHttpRequestRetryHandler(0, false);
272 		RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(timeout).setConnectTimeout(timeout).setConnectionRequestTimeout(timeout)
273 				.setStaleConnectionCheckEnabled(true).build();
274 		return HttpClients.custom().setRetryHandler(retryHandler).setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(requestConfig).build();
275 	}
276 
277 }