X-Git-Url: https://juplo.de/gitweb/?a=blobdiff_plain;f=dist%2Fhttp-resources%2F2.0.0%2Fxref%2Fde%2Fjuplo%2Fhttpresources%2FHttpResourceFetcher.html;fp=dist%2Fhttp-resources%2F2.0.0%2Fxref%2Fde%2Fjuplo%2Fhttpresources%2FHttpResourceFetcher.html;h=47f3f0936005ba3573c352957f1d843f100b81f8;hb=96ec104e2974d001e9bc82c3af8b21029b2042d4;hp=0000000000000000000000000000000000000000;hpb=de1fa457a1c69c673d4dd5c0a2c9af568f74ea12;p=website diff --git a/dist/http-resources/2.0.0/xref/de/juplo/httpresources/HttpResourceFetcher.html b/dist/http-resources/2.0.0/xref/de/juplo/httpresources/HttpResourceFetcher.html new file mode 100644 index 00000000..47f3f093 --- /dev/null +++ b/dist/http-resources/2.0.0/xref/de/juplo/httpresources/HttpResourceFetcher.html @@ -0,0 +1,334 @@ + + +
++1 package de.juplo.httpresources; +2 +3 import org.slf4j.Logger; +4 import org.slf4j.LoggerFactory; +5 import org.springframework.cache.Cache; +6 import org.springframework.http.HttpHeaders; +7 import org.springframework.http.HttpMethod; +8 import org.springframework.http.HttpStatus; +9 import org.springframework.http.client.ClientHttpRequest; +10 import org.springframework.http.client.ClientHttpRequestFactory; +11 import org.springframework.http.client.ClientHttpResponse; +12 import org.springframework.util.FileCopyUtils; +13 +14 import java.io.IOException; +15 import java.io.InputStream; +16 import java.net.URI; +17 import java.time.Clock; +18 import java.util.HashMap; +19 import java.util.Map; +20 import java.util.function.Predicate; +21 +22 import static org.springframework.http.HttpHeaders.*; +23 +24 public class HttpResourceFetcher +25 { +26 private final static Logger LOG = +27 LoggerFactory.getLogger(HttpResourceFetcher.class); +28 +29 public final static int DEFAULT_TTL = 3600; +30 +31 private final ClientHttpRequestFactory requestFactory; +32 private final Cache cache; +33 private final Clock clock; +34 +35 private final Predicate<String> filter; +36 +37 private final int defaultTTL; +38 private final int minTTL; +39 +40 private final boolean serveStale; +41 +42 +43 public HttpResourceFetcher(ClientHttpRequestFactory factory, Cache cache, Clock clock) +44 { +45 this(factory, cache, clock, DEFAULT_TTL, 0, true); +46 } +47 +48 /** +49 * +50 * @param factory +51 * @param clock +52 * @param defaultTTL default time to live in milliseconds +53 * Default TTL, if no according information is present in the HTTP-headers. +54 * If set to <code>0</code>, caching will be disabled, if no according +55 * HTTP-headers are present. +56 * @param minTTL the minimum time to live, if caching is enabled +57 * A minimum TTL, that will overwrite the time to live, that was extracted +58 * from the HTTP-headers. +59 * The minimum TTL is only applied, if the default TTL is set to a value +60 * greater than zero, hence enabling caching by default. +61 * @param serveStale +62 */ +63 public HttpResourceFetcher( +64 ClientHttpRequestFactory factory, +65 Cache cache, +66 Clock clock, +67 int defaultTTL, +68 int minTTL, +69 boolean serveStale) +70 { +71 this.requestFactory = factory; +72 this.cache = cache; +73 this.clock = clock; +74 this.defaultTTL = defaultTTL; +75 this.minTTL = minTTL; +76 this.serveStale = serveStale; +77 +78 /** Filter may pass on more headers and may also be configurable later */ +79 filter = (key) -> CONTENT_TYPE.equalsIgnoreCase(key); +80 } +81 +82 +83 /** +84 * Fetches the remote resource and reports, if it was modified. +85 * +86 * This method fetches the remote resource, if was not already fetched. +87 * If the resource was already fetched, it revalidates it, if necessary. +88 * +89 * @return {@code true}, if the resource has changed or was fetched for +90 * the first time, otherwise {@code false} +91 */ +92 public HttpData./de/juplo/httpresources/HttpData.html#HttpData">HttpData fetch(URI uri, HttpData data) +93 { +94 HttpData cached = cache.get(uri, HttpData.class); +95 if (cached != null && cached != data) +96 { +97 if (cached.expires >= clock.millis()) +98 return cached; +99 else +100 data = cached; +101 } +102 +103 boolean has_existed = data.content != null; +104 boolean must_revalidate = data.revalidate; +105 +106 ClientHttpRequest request = createGetRequest(uri); +107 if (has_existed) +108 { +109 if (data.eTag != null) +110 request.getHeaders().setIfNoneMatch(data.eTag); +111 if (data.lastModified > 0) +112 request.getHeaders().setIfModifiedSince(data.lastModified); +113 } +114 +115 HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; +116 long request_time = clock.millis(); +117 +118 try (ClientHttpResponse response = request.execute()) +119 { +120 status = response.getStatusCode(); +121 +122 LOG.debug("{} -- {}", status, uri); +123 +124 switch (status) +125 { +126 case OK: +127 +128 InputStream body = response.getBody(); +129 byte[] content = FileCopyUtils.copyToByteArray(body); +130 +131 data = readCachingHeaders(content, response, response.getHeaders(), request_time, defaultTTL); +132 cache.put(uri, data); +133 return data; +134 +135 case NOT_MODIFIED: +136 +137 data = readCachingHeaders(data.content, response, data.headers, request_time, defaultTTL); +138 cache.put(uri, data); +139 return data; +140 +141 +142 case NOT_FOUND: +143 +144 // If stale data is served, 404-responses for already cached +145 // resources are quietly ignored +146 if (!serveStale) +147 { +148 data = readCachingHeaders(null, response, response.getHeaders(), request_time, 0); +149 } +150 +151 cache.put(uri, data); +152 return data; +153 +154 default: +155 +156 // TODO: +157 // A client has at least to distinguish the status-classes, denoted +158 // by the first digit of the code. +159 // See: https://tools.ietf.org/html/rfc7231#section-6 +160 } +161 } +162 catch (IOException e) +163 { +164 LOG.error("Cannot retrieve {}: {}", uri, e.toString()); +165 } +166 +167 +168 if ((data.content == null) || (data.revalidate && !serveStale)) +169 { +170 data = HttpData.SERVER_ERROR; +171 } +172 else +173 { +174 // An existing resource can be considered still existent, if the +175 // request fails and the resource does not have to be revalidated. +176 // It will also be considered as still existent, if revalidation is +177 // enforced through the HTTP-protocol, but serving stale data is +178 // enabled. +179 } +180 +181 cache.put(uri, data); +182 return data; +183 } +184 +185 +186 private HttpData readCachingHeaders( +187 byte[] content, +188 ClientHttpResponse response, +189 HttpHeaders headers, +190 long request_time, +191 long defaultTTL +192 ) +193 { +194 HttpHeaders responseHeaders = response.getHeaders(); +195 // Calculating Freshness Lifetime +196 // See: https://tools.ietf.org/html/rfc7234#section-4.2.1 +197 +198 long now = clock.millis(); +199 +200 long date_value = responseHeaders.getDate(); +201 long apparent_age = 0L; +202 +203 if (date_value < 0L) +204 { +205 // Use the current time, if no date-header is present +206 // See: https://tools.ietf.org/html/rfc7231#section-7.1.1.2 +207 date_value = now; +208 } +209 else +210 { +211 apparent_age = now - date_value; +212 apparent_age = apparent_age < 0 ? 0 : apparent_age; +213 } +214 +215 long corrected_age_value = 0L; +216 String value = responseHeaders.getFirst(AGE); +217 if (value != null) +218 { +219 try +220 { +221 long response_delay = now - request_time; +222 long age_value = Long.parseLong(value) * 1000; // convert s to ms +223 age_value = age_value < 0 ? 0 : age_value; +224 corrected_age_value = age_value + response_delay; +225 } +226 catch (NumberFormatException e) {} +227 } +228 +229 long corrected_initial_age = +230 apparent_age > corrected_age_value +231 ? apparent_age +232 : corrected_age_value; +233 +234 long lastModified = responseHeaders.getLastModified(); +235 lastModified = lastModified < 0L ? 0 : lastModified; +236 +237 String eTag = responseHeaders.getETag(); +238 +239 +240 Map<String, String> directives = new HashMap<>(); +241 for (String field : responseHeaders.getOrEmpty(CACHE_CONTROL)) +242 { +243 for (String directive : field.split("\\s*,\\s*")) +244 { +245 String[] splitted = directive.split("\\s*=\\s*"); +246 if (splitted.length > 2) +247 { +248 LOG.warn("Ingoring garbled directive: {}", directive); +249 continue; +250 } +251 directives.put( +252 splitted[0].trim(), +253 splitted.length == 2 ? splitted[1] : null +254 ); +255 } +256 } +257 +258 long ttl = -1l; +259 try +260 { +261 String maxAge = directives.get("max-age"); +262 if (maxAge != null) +263 ttl = Long.parseUnsignedLong(maxAge) *1000; +264 } +265 catch (NumberFormatException e) {} +266 +267 boolean revalidate = directives.containsKey("must-revalidate"); +268 long expires = 0l; +269 +270 if (!directives.containsKey("no-cache") || +271 directives.get("no-cache") != null +272 ) +273 { +274 if (ttl < 0) +275 { +276 if (responseHeaders.get(EXPIRES) != null) +277 { +278 expires = responseHeaders.getExpires(); +279 // If the "Expires"-header cannot be parsed to a valid date, it has +280 // to be interpreted as a time in the past. +281 // See: https://tools.ietf.org/html/rfc7234#section-5.3 +282 expires = expires == -1 ? 0 : expires; +283 return new HttpData(content, filter(headers), revalidate, lastModified, expires, eTag); +284 } +285 +286 ttl = defaultTTL; +287 } +288 +289 expires = date_value + (minTTL > ttl && defaultTTL > 0 ? minTTL : ttl) - corrected_initial_age; +290 return new HttpData(content, filter(headers), revalidate, lastModified, expires, eTag); +291 } +292 +293 expires = minTTL == 0 || defaultTTL == 0 ? date_value : date_value + minTTL - corrected_initial_age; +294 return new HttpData(content, filter(headers), revalidate, lastModified, expires, eTag); +295 } +296 +297 private HttpHeaders filter(HttpHeaders headers) +298 { +299 HttpHeaders result = new HttpHeaders(); +300 headers.forEach((key, value) -> +301 { +302 if (filter.test(key)) +303 result.put(key, value); +304 }); +305 return result; +306 } +307 +308 private ClientHttpRequest createGetRequest(URI uri) +309 { +310 try +311 { +312 ClientHttpRequest request = requestFactory.createRequest(uri, HttpMethod.GET); +313 request.getHeaders().set(ACCEPT_ENCODING, "gzip"); +314 return request; +315 } +316 catch (IOException e) +317 { +318 throw new RuntimeException(e); +319 } +320 } +321 } ++