1 package fr.ifremer.quadrige3.core.service.http;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 import com.google.gson.Gson;
25 import com.google.gson.reflect.TypeToken;
26 import fr.ifremer.quadrige3.core.ProgressionCoreModel;
27 import fr.ifremer.quadrige3.core.config.QuadrigeConfiguration;
28 import fr.ifremer.quadrige3.core.dao.technical.Assert;
29 import fr.ifremer.quadrige3.core.dao.technical.Files;
30 import fr.ifremer.quadrige3.core.dao.technical.gson.Gsons;
31 import fr.ifremer.quadrige3.core.dao.technical.http.CustomHttpHeaders;
32 import fr.ifremer.quadrige3.core.dao.technical.http.CustomHttpStatus;
33 import fr.ifremer.quadrige3.core.exception.*;
34 import fr.ifremer.quadrige3.core.security.AuthenticationInfo;
35 import org.apache.commons.lang3.builder.ToStringBuilder;
36 import org.apache.commons.lang3.builder.ToStringStyle;
37 import org.apache.commons.logging.Log;
38 import org.apache.commons.logging.LogFactory;
39 import org.apache.http.*;
40 import org.apache.http.auth.AuthScope;
41 import org.apache.http.auth.UsernamePasswordCredentials;
42 import org.apache.http.client.CredentialsProvider;
43 import org.apache.http.client.HttpRequestRetryHandler;
44 import org.apache.http.client.config.RequestConfig;
45 import org.apache.http.client.methods.HttpPost;
46 import org.apache.http.client.methods.HttpUriRequest;
47 import org.apache.http.client.protocol.HttpClientContext;
48 import org.apache.http.client.utils.URIBuilder;
49 import org.apache.http.conn.HttpHostConnectException;
50 import org.apache.http.entity.ContentType;
51 import org.apache.http.entity.mime.MultipartEntityBuilder;
52 import org.apache.http.entity.mime.content.ContentBody;
53 import org.apache.http.entity.mime.content.StringBody;
54 import org.apache.http.impl.client.BasicCredentialsProvider;
55 import org.apache.http.impl.client.CloseableHttpClient;
56 import org.apache.http.impl.client.HttpClients;
57 import org.apache.http.protocol.HTTP;
58 import org.apache.http.util.EntityUtils;
59 import org.nuiton.i18n.I18n;
60 import org.springframework.context.annotation.Lazy;
61 import org.springframework.stereotype.Service;
62
63 import javax.annotation.Resource;
64 import javax.net.ssl.SSLException;
65 import java.io.*;
66 import java.lang.reflect.Type;
67 import java.net.*;
68 import java.nio.charset.Charset;
69 import java.nio.charset.StandardCharsets;
70 import java.nio.file.Path;
71 import java.util.*;
72 import java.util.function.Function;
73 import java.util.regex.Matcher;
74 import java.util.regex.Pattern;
75 import java.util.stream.Collectors;
76
77 import static org.nuiton.i18n.I18n.t;
78
79
80
81
82
83
84 @Service("httpService")
85 @Lazy
86 public class HttpServiceImpl implements HttpService, Closeable {
87
88 private final static Log LOG = LogFactory.getLog(HttpServiceImpl.class);
89
90 private final static Pattern PATTERN_INTERNAL_SERVER_ERROR_MESSAGE = Pattern.compile("\"\\[([0-9]+)] .*");
91 private static final String PART_UPLOAD_FILE = "file";
92 private static final String PART_UPLOAD_FILENAME = "filename";
93 private static final int MAX_RETRY_COUNT = 3;
94
95 @Resource
96 private QuadrigeConfiguration configuration;
97
98 private RequestConfig requestConfig = null;
99
100 private Gson gson = null;
101
102 private Map<AuthenticationInfo, CloseableHttpClient> httpClientMap = new HashMap<>();
103
104 @Override
105 public CloseableHttpClient getHttpClient(AuthenticationInfo authenticationInfo) {
106 return httpClientMap.computeIfAbsent(authenticationInfo, this::newHttpClient);
107 }
108
109 @Override
110 public Gson getGson() {
111 if (gson == null) {
112 gson = Gsons
113 .newBuilder(configuration.getDbTimezone())
114 .create();
115 }
116 return gson;
117 }
118
119 @Override
120 public URI getPathUri(String... paths) {
121 try {
122 URI baseURI = configuration.getSynchronizationSiteUrl().toURI();
123 String pathToAppend = Arrays.stream(paths).filter(Objects::nonNull).collect(Collectors.joining("/"));
124
125 URIBuilder builder = new URIBuilder(baseURI);
126 String path = baseURI.getPath() + pathToAppend;
127
128
129 if (path.contains("//")) {
130 path = path.replaceAll("/[/]+", "/");
131 }
132
133 builder.setPath(path);
134 return builder.build();
135
136 } catch (URISyntaxException e) {
137 throw new QuadrigeTechnicalException(t("quadrige3.error.remote.synchronizationSiteUrl", e.getMessage()), e);
138 }
139 }
140
141 @Override
142 public void executeRequest(AuthenticationInfo authenticationInfo, HttpUriRequest request) {
143 executeRequest(authenticationInfo, request, (Class<?>) null);
144 }
145
146 @Override
147 public <T> T executeRequest(AuthenticationInfo authenticationInfo, HttpUriRequest request, Class<? extends T> resultClass) {
148 return executeRequest(authenticationInfo, request, resultClass, false);
149 }
150
151 @Override
152 public <T> T executeRequest(AuthenticationInfo authenticationInfo, HttpUriRequest request, Class<? extends T> resultClass, boolean allowNullResult) {
153
154
155 return executeRequest(authenticationInfo, request,
156 response -> resultClass != null
157 ? parseResponse(response, resultClass, allowNullResult)
158 : null);
159 }
160
161 @Override
162 public <T> T executeRequest(AuthenticationInfo authenticationInfo, HttpUriRequest request, Type type) {
163
164
165 return executeRequest(authenticationInfo, request,
166 response -> type != null
167 ? parseResponse(response, type, false)
168 : null);
169
170 }
171
172 @Override
173 public void executeDownloadFileRequest(AuthenticationInfo authenticationInfo, HttpUriRequest request, ProgressionCoreModel progressionModel, File outputFile) {
174 Assert.notNull(request);
175 Assert.notNull(progressionModel);
176 Assert.notNull(outputFile);
177
178 boolean errorReturned = false;
179 try {
180
181 HttpResponse response = getHttpClient(authenticationInfo).execute(request);
182
183 int statusCode = response.getStatusLine().getStatusCode();
184 if (statusCode != CustomHttpStatus.SC_OK) {
185
186 errorReturned = true;
187 switch (statusCode) {
188
189 case CustomHttpStatus.SC_UNAUTHORIZED:
190 case CustomHttpStatus.SC_FORBIDDEN:
191 throw new HttpAuthenticationException(I18n.t("quadrige3.error.authenticate.unauthorized"));
192 case CustomHttpStatus.SC_NOT_FOUND:
193 throw new HttpNotFoundException(I18n.t("quadrige3.error.notFound"));
194 case CustomHttpStatus.SC_SERVICE_UNAVAILABLE:
195 throw new HttpUnavailableException(I18n.t("quadrige3.error.server.unavailable"));
196 default:
197 throw new QuadrigeTechnicalException(
198 String.format("error while downloading file from [%s]; server responds: %s", request.getURI(), response.getStatusLine().getReasonPhrase()));
199
200 }
201
202 }
203
204
205 progressionModel.setTotal(response.getEntity().getContentLength());
206
207
208 Path output = outputFile.toPath();
209 Files.deleteQuietly(output);
210 java.nio.file.Files.createDirectories(output.getParent());
211
212
213 try (InputStream inputStream = new BufferedInputStream(response.getEntity().getContent());
214 OutputStream outputStream = java.nio.file.Files.newOutputStream(output)) {
215 Files.copyStream(inputStream, outputStream, progressionModel);
216 }
217
218 } catch (InterruptedIOException e) {
219 throw new QuadrigeTechnicalException(I18n.t("quadrige3.error.timeout"), e);
220 } catch (IOException e) {
221 throw new QuadrigeTechnicalException(I18n.t("quadrige3.error.connect"), e);
222 } finally {
223 if (errorReturned) {
224
225 evictClient(authenticationInfo);
226 }
227 }
228 }
229
230 @Override
231 public void executeUploadFileRequest(AuthenticationInfo authenticationInfo, HttpPost request, ProgressionCoreModel progressionModel, File inputFile,
232 Map<String, ContentBody> additionalParts) {
233
234 MultipartEntityBuilder builder = MultipartEntityBuilder.create();
235
236 builder.addPart(PART_UPLOAD_FILE, new ProgressFileBody(inputFile, progressionModel));
237
238 builder.addPart(PART_UPLOAD_FILENAME, new StringBody(inputFile.getName(), ContentType.create("text/plain", Charset.defaultCharset())));
239
240 if (additionalParts != null) {
241 additionalParts.keySet().forEach(partName -> builder.addPart(partName, additionalParts.get(partName)));
242 }
243
244 request.setEntity(builder.build());
245 request.addHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE);
246
247
248 executeRequest(authenticationInfo, request);
249
250 }
251
252 @Override
253 public void close() throws IOException {
254
255 for (CloseableHttpClient closeableHttpClient : httpClientMap.values()) {
256 closeableHttpClient.close();
257 }
258 }
259
260 private <T> T executeRequest(AuthenticationInfo authenticationInfo, HttpUriRequest request, Function<HttpResponse, T> parser) {
261 T result;
262
263 if (LOG.isDebugEnabled()) {
264 LOG.debug("Executing request : " + request.getRequestLine());
265 }
266
267
268 if (I18n.getDefaultLocale() != null) {
269 request.setHeader(HttpHeaders.ACCEPT_LANGUAGE, I18n.getDefaultLocale().getLanguage());
270 } else if (LOG.isWarnEnabled()) {
271 LOG.warn(String.format("I18n not initialized properly: no default locale found ! Could not set HTTP header [%s] with user language", HttpHeaders.ACCEPT_LANGUAGE));
272 }
273
274 boolean errorReturned = false;
275 try {
276 HttpResponse response = getHttpClient(authenticationInfo).execute(request);
277
278 if (LOG.isDebugEnabled()) {
279 LOG.debug("Received response : " + response.getStatusLine());
280 }
281
282 int statusCode = response.getStatusLine().getStatusCode();
283 if (statusCode == CustomHttpStatus.SC_OK) {
284
285
286 result = parser.apply(response);
287 EntityUtils.consume(response.getEntity());
288
289 } else {
290
291 errorReturned = true;
292 switch (statusCode) {
293
294 case CustomHttpStatus.SC_INTERNAL_SERVER_ERROR: {
295 String errorMessage = parseStringResponse(response);
296 switch (getInternalServerErrorCode(errorMessage)) {
297 case CustomHttpStatus.SC_BAD_UPDATE_DT:
298 throw new BadUpdateDtException(errorMessage);
299 case CustomHttpStatus.SC_DATA_LOCKED:
300 throw new DataLockedException(errorMessage);
301 case CustomHttpStatus.SC_DELETE_FORBIDDEN: {
302 List<String> objectIds = null;
303
304 Header header = response.getFirstHeader(CustomHttpHeaders.HH_DELETE_FORBIDDEN_OBJECT_IDS);
305 if (header != null) {
306 objectIds = getGson().fromJson(header.getValue(), new TypeToken<ArrayList<String>>() {
307 }.getType());
308 }
309 throw new DeleteForbiddenException(errorMessage, objectIds);
310 }
311 case CustomHttpStatus.SC_SAVE_FORBIDDEN: {
312 SaveForbiddenException.Type exceptionType = null;
313 List<String> objectIds = null;
314
315 Header header = response.getFirstHeader(CustomHttpHeaders.HH_SAVE_FORBIDDEN_TYPE);
316 if (header != null)
317 exceptionType = SaveForbiddenException.Type.valueOf(header.getValue().toUpperCase());
318 header = response.getFirstHeader(CustomHttpHeaders.HH_SAVE_FORBIDDEN_OBJECT_IDS);
319 if (header != null) {
320 objectIds = getGson().fromJson(header.getValue(), new TypeToken<ArrayList<String>>() {
321 }.getType());
322 }
323 throw new SaveForbiddenException(exceptionType, errorMessage, objectIds);
324 }
325 default:
326 throw new QuadrigeTechnicalException(I18n.t("quadrige3.error.server.internal", response.getStatusLine().toString()), new Exception(errorMessage));
327 }
328 }
329
330 case CustomHttpStatus.SC_UNAUTHORIZED:
331 case CustomHttpStatus.SC_FORBIDDEN:
332 throw new HttpAuthenticationException(I18n.t("quadrige3.error.authenticate.unauthorized"));
333 case CustomHttpStatus.SC_NOT_FOUND:
334 throw new HttpNotFoundException(I18n.t("quadrige3.error.notFound"));
335 case CustomHttpStatus.SC_SERVICE_UNAVAILABLE:
336 throw new HttpUnavailableException(I18n.t("quadrige3.error.server.unavailable"));
337 default:
338 throw new QuadrigeTechnicalException(I18n.t("quadrige3.error.request.failed", response.getStatusLine().toString()));
339 }
340 }
341
342 } catch (InterruptedIOException e) {
343 throw new QuadrigeTechnicalException(I18n.t("quadrige3.error.timeout"), e);
344 } catch (IOException e) {
345 throw new QuadrigeTechnicalException(I18n.t("quadrige3.error.connect"), e);
346 } finally {
347 if (errorReturned) {
348
349 evictClient(authenticationInfo);
350 }
351 }
352
353 return result;
354 }
355
356 private void evictClient(AuthenticationInfo authenticationInfo) {
357
358 CloseableHttpClient client = httpClientMap.get(authenticationInfo);
359 if (client != null) {
360 try {
361 client.close();
362 } catch (IOException ignored) {
363 }
364 httpClientMap.remove(authenticationInfo);
365 }
366 }
367
368 private CloseableHttpClient newHttpClient(AuthenticationInfo authenticationInfo) {
369
370 return HttpClients.custom()
371 .setDefaultRequestConfig(getRequestConfig())
372 .setDefaultCredentialsProvider(getCredentialsProvider(authenticationInfo))
373 .setRetryHandler(getRetryHandler())
374 .evictExpiredConnections()
375 .build();
376 }
377
378 private HttpRequestRetryHandler getRetryHandler() {
379 return (exception, executionCount, context) -> {
380 boolean retrying = true;
381 if (exception instanceof NoRouteToHostException) {
382
383 retrying = false;
384 } else if (exception instanceof InterruptedIOException) {
385
386 retrying = false;
387 } else if (exception instanceof UnknownHostException) {
388
389 retrying = false;
390 } else if (exception instanceof SSLException) {
391
392 retrying = false;
393 } else if (exception instanceof HttpHostConnectException) {
394
395 retrying = false;
396 }
397
398 if (retrying && executionCount >= MAX_RETRY_COUNT) {
399
400 return false;
401 }
402
403 HttpClientContext clientContext = HttpClientContext.adapt(context);
404 if (!retrying) {
405 if (LOG.isWarnEnabled()) {
406 LOG.warn("Failed request to " + clientContext.getRequest().getRequestLine() + ": " + exception.getMessage());
407 }
408 return false;
409 }
410
411 HttpRequest request = clientContext.getRequest();
412 boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
413 if (idempotent) {
414
415 if (LOG.isWarnEnabled()) LOG.warn("Failed (but will retry) request to " + request.getRequestLine() + ": " + exception.getMessage());
416 return true;
417 }
418 return false;
419 };
420 }
421
422
423
424
425
426
427
428
429 private RequestConfig getRequestConfig() {
430 if (requestConfig == null) {
431 Integer timeOut = configuration.getSynchronizationSiteTimeout();
432
433
434 requestConfig = RequestConfig.custom()
435 .setSocketTimeout(timeOut)
436 .setConnectTimeout(timeOut)
437 .setConnectionRequestTimeout(timeOut)
438 .build();
439 }
440
441 return requestConfig;
442 }
443
444
445
446
447
448
449
450
451
452 private CredentialsProvider getCredentialsProvider(AuthenticationInfo authenticationInfo) {
453
454 if (authenticationInfo == null) {
455 return null;
456 }
457
458 URL url = configuration.getSynchronizationSiteUrl();
459
460 CredentialsProvider result = new BasicCredentialsProvider();
461 result.setCredentials(new AuthScope(url.getHost(), url.getPort()),
462 new UsernamePasswordCredentials(authenticationInfo.getLogin(), authenticationInfo.getPassword()));
463 return result;
464 }
465
466 private String parseStringResponse(HttpResponse response) throws IOException {
467 try (InputStream content = response.getEntity().getContent()) {
468 String stringContent = getContentAsString(content);
469 if (LOG.isDebugEnabled()) {
470 LOG.debug("Parsing response:\n" + stringContent);
471 }
472 return stringContent;
473 }
474 }
475
476 private <T> T parseResponse(HttpResponse response, Object objectType, boolean allowEmptyResponse) {
477 T result = null;
478
479 try (InputStream content = response.getEntity().getContent()) {
480 Reader reader = new InputStreamReader(content, StandardCharsets.UTF_8);
481 if (objectType instanceof Type)
482 result = getGson().fromJson(reader, (Type) objectType);
483 } catch (IOException e) {
484 throw new QuadrigeTechnicalException(e.getMessage());
485 }
486
487 if (!allowEmptyResponse && result == null) {
488 throw new QuadrigeTechnicalException("emptyResponse");
489 }
490
491 if (LOG.isTraceEnabled()) {
492 LOG.trace("response: " + (result != null ? ToStringBuilder.reflectionToString(result, ToStringStyle.SHORT_PREFIX_STYLE) : "null"));
493 }
494
495 return result;
496 }
497
498 private String getContentAsString(InputStream content) throws IOException {
499 Reader reader = new InputStreamReader(content, StandardCharsets.UTF_8);
500 StringBuilder result = new StringBuilder();
501 char[] buf = new char[64];
502 int len;
503 while ((len = reader.read(buf)) != -1) {
504 result.append(buf, 0, len);
505 }
506 return result.toString();
507 }
508
509 private int getInternalServerErrorCode(String message) {
510 if (message != null) {
511 Matcher matcher = PATTERN_INTERNAL_SERVER_ERROR_MESSAGE.matcher(message);
512 if (matcher.matches()) {
513 String errorCodeAsStr = matcher.group(1);
514 return Integer.parseInt(errorCodeAsStr);
515 }
516 }
517
518 return CustomHttpStatus.SC_INTERNAL_SERVER_ERROR;
519 }
520
521 }