Package: CachingRestClientSupport$1
CachingRestClientSupport$1
name | instruction | branch | complexity | line | method | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
handleError(ClientHttpResponse) |
|
|
|
|
|
||||||||||||||||||||
hasError(ClientHttpResponse) |
|
|
|
|
|
||||||||||||||||||||
{...} |
|
|
|
|
|
Coverage
1: /*
2: * *********************************************************************************************************************
3: *
4: * blueMarine II: Semantic Media Centre
5: * http://tidalwave.it/projects/bluemarine2
6: *
7: * Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *********************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
12: * the License. You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
17: * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
18: * specific language governing permissions and limitations under the License.
19: *
20: * *********************************************************************************************************************
21: *
22: * git clone https://bitbucket.org/tidalwave/bluemarine2-src
23: * git clone https://github.com/tidalwave-it/bluemarine2-src
24: *
25: * *********************************************************************************************************************
26: */
27: package it.tidalwave.bluemarine2.rest;
28:
29: import javax.annotation.Nonnegative;
30: import javax.annotation.Nonnull;
31: import javax.annotation.PostConstruct;
32: import java.util.List;
33: import java.util.Optional;
34: import java.io.IOException;
35: import java.nio.file.Path;
36: import java.net.URI;
37: import java.security.MessageDigest;
38: import java.security.NoSuchAlgorithmException;
39: import org.springframework.http.HttpHeaders;
40: import org.springframework.http.ResponseEntity;
41: import org.springframework.http.client.ClientHttpRequestInterceptor;
42: import org.springframework.http.client.ClientHttpResponse;
43: import org.springframework.web.client.ResponseErrorHandler;
44: import org.springframework.web.client.RestTemplate;
45: import lombok.Getter;
46: import lombok.Setter;
47: import lombok.extern.slf4j.Slf4j;
48: import static java.util.Collections.*;
49: import static java.nio.charset.StandardCharsets.UTF_8;
50: import static org.springframework.http.HttpHeaders.*;
51:
52: /***********************************************************************************************************************
53: *
54: * @author Fabrizio Giudici
55: *
56: **********************************************************************************************************************/
57: @Slf4j
58: public class CachingRestClientSupport
59: {
60: public enum CacheMode
61: {
62: /** Always use the network. */
63: DONT_USE_CACHE
64: {
65: @Override @Nonnull
66: public ResponseEntity<String> request (@Nonnull final CachingRestClientSupport api,
67: @Nonnull final String url)
68: throws IOException, InterruptedException
69: {
70: return api.requestFromNetwork(url);
71: }
72: },
73: /** Never use the network. */
74: ONLY_USE_CACHE
75: {
76: @Override @Nonnull
77: public ResponseEntity<String> request (@Nonnull final CachingRestClientSupport api,
78: @Nonnull final String url)
79: throws IOException
80: {
81: return api.requestFromCache(url).get();
82: }
83: },
84: /** First try the cache, then the network. */
85: USE_CACHE
86: {
87: @Override @Nonnull
88: public ResponseEntity<String> request (@Nonnull final CachingRestClientSupport api,
89: @Nonnull final String url)
90: throws IOException, InterruptedException
91: {
92: return api.requestFromCacheAndThenNetwork(url);
93: }
94: };
95:
96: @Nonnull
97: public abstract ResponseEntity<String> request (@Nonnull CachingRestClientSupport api,
98: @Nonnull String url)
99: throws IOException, InterruptedException;
100: }
101:
102: private final RestTemplate restTemplate = new RestTemplate(); // FIXME: inject?
103:
104: @Getter @Setter
105: private CacheMode cacheMode = CacheMode.USE_CACHE;
106:
107: @Getter @Setter
108: private Path cachePath;
109:
110: @Getter @Setter
111: private String accept = "application/xml";
112:
113: @Getter @Setter
114: private String userAgent = "blueMarine II (fabrizio.giudici@tidalwave.it)";
115:
116: @Getter @Setter
117: private long throttleLimit;
118:
119: @Getter @Setter @Nonnegative
120: private int maxRetry = 3;
121:
122: @Getter @Setter
123: private List<Integer> retryStatusCodes = List.of(503);
124:
125: private long latestNetworkAccessTimestamp = 0;
126:
127: /*******************************************************************************************************************
128: *
129: *
130: *
131: ******************************************************************************************************************/
132: private static final ResponseErrorHandler IGNORE_HTTP_ERRORS = new ResponseErrorHandler()
133: {
134: @Override
135: public boolean hasError (@Nonnull final ClientHttpResponse response)
136: throws IOException
137: {
138: return false;
139: }
140:
141: @Override
142: public void handleError (@Nonnull final ClientHttpResponse response)
143: throws IOException
144: {
145: }
146: };
147:
148: /*******************************************************************************************************************
149: *
150: *
151: *
152: ******************************************************************************************************************/
153: private final ClientHttpRequestInterceptor interceptor = (request, body, execution) ->
154: {
155: final HttpHeaders headers = request.getHeaders();
156: headers.add(USER_AGENT, userAgent);
157: headers.add(ACCEPT, accept);
158: return execution.execute(request, body);
159: };
160:
161: /*******************************************************************************************************************
162: *
163: *
164: *
165: ******************************************************************************************************************/
166: public CachingRestClientSupport()
167: {
168: restTemplate.setInterceptors(singletonList(interceptor));
169: restTemplate.setErrorHandler(IGNORE_HTTP_ERRORS);
170: }
171:
172: /*******************************************************************************************************************
173: *
174: *
175: *
176: ******************************************************************************************************************/
177: @PostConstruct
178: void initialize()
179: {
180: }
181:
182: /*******************************************************************************************************************
183: *
184: * Performs a web request.
185: *
186: * @return the response
187: *
188: ******************************************************************************************************************/
189: @Nonnull
190: protected ResponseEntity<String> request (@Nonnull final String url)
191: throws IOException, InterruptedException
192: {
193: log.debug("request({})", url);
194: return cacheMode.request(this, url);
195: }
196:
197: /*******************************************************************************************************************
198: *
199: *
200: *
201: ******************************************************************************************************************/
202: @Nonnull
203: private Optional<ResponseEntity<String>> requestFromCache (@Nonnull final String url)
204: throws IOException
205: {
206: log.debug("requestFromCache({})", url);
207: return ResponseEntityIo.load(cachePath.resolve(fixedPath(url)));
208: }
209:
210: /*******************************************************************************************************************
211: *
212: *
213: *
214: ******************************************************************************************************************/
215: @Nonnull
216: private synchronized ResponseEntity<String> requestFromNetwork (@Nonnull final String url)
217: throws IOException, InterruptedException
218: {
219: log.debug("requestFromNetwork({})", url);
220: ResponseEntity<String> response = null;
221:
222: for (int retry = 0; retry < maxRetry; retry++)
223: {
224: final long now = System.currentTimeMillis();
225: final long delta = now - latestNetworkAccessTimestamp;
226: final long toWait = Math.max(throttleLimit - delta, 0);
227:
228: if (toWait > 0)
229: {
230: log.info(">>>> throttle limit: waiting for {} msec...", toWait);
231: Thread.sleep(toWait);
232: }
233:
234: latestNetworkAccessTimestamp = now;
235: response = restTemplate.getForEntity(URI.create(url), String.class);
236: final int httpStatusCode = response.getStatusCodeValue();
237: log.debug(">>>> HTTP status code: {}", httpStatusCode);
238:
239: if (!retryStatusCodes.contains(httpStatusCode))
240: {
241: break;
242: }
243:
244: log.warn("HTTP status code: {} - retry #{}", httpStatusCode, retry + 1);
245: }
246:
247: // log.trace(">>>> response: {}", response);
248: return response;
249: }
250:
251: /*******************************************************************************************************************
252: *
253: *
254: *
255: ******************************************************************************************************************/
256: @Nonnull
257: private ResponseEntity<String> requestFromCacheAndThenNetwork (@Nonnull final String url)
258: throws IOException, InterruptedException
259: {
260: log.debug("requestFromCacheAndThenNetwork({})", url);
261:
262: return requestFromCache(url).orElseGet(() ->
263: {
264: try
265: {
266: final ResponseEntity<String> response = requestFromNetwork(url);
267: final int httpStatusCode = response.getStatusCodeValue();
268:
269: if (!retryStatusCodes.contains(httpStatusCode))
270: {
271: ResponseEntityIo.store(cachePath.resolve(fixedPath(url)), response, emptyList());
272: }
273:
274: return response;
275: }
276: catch (IOException | InterruptedException e)
277: {
278: throw new RestException(e); // FIXME
279: }
280: });
281: }
282:
283: /*******************************************************************************************************************
284: *
285: *
286: ******************************************************************************************************************/
287: @Nonnull
288: /* package */ static String fixedPath (@Nonnull final String url)
289: {
290: String s = url.replace("://", "/");
291: int i = s.lastIndexOf('/');
292:
293: if (i >= 0)
294: {
295: final String lastSegment = s.substring(i + 1);
296:
297: if (lastSegment.length() > 255) // FIXME: and Mac OS X
298: {
299: try
300: {
301: final MessageDigest digestComputer = MessageDigest.getInstance("SHA1");
302: s = s.substring(0, i) + "/" + toString(digestComputer.digest(lastSegment.getBytes(UTF_8)));
303: }
304: catch (NoSuchAlgorithmException e)
305: {
306: throw new RuntimeException(e);
307: }
308: }
309: }
310:
311: return s;
312: }
313:
314: /*******************************************************************************************************************
315: *
316: *
317: ******************************************************************************************************************/
318: @Nonnull
319: private static String toString (@Nonnull final byte[] bytes)
320: {
321: final StringBuilder builder = new StringBuilder();
322:
323: for (final byte b : bytes)
324: {
325: final int value = b & 0xff;
326: builder.append(Integer.toHexString(value >>> 4)).append(Integer.toHexString(value & 0x0f));
327: }
328:
329: return builder.toString();
330: }
331: }