source: josm/trunk/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java@ 8318

Last change on this file since 8318 was 8318, checked in by Don-vip, 9 years ago

fix various Sonar issues:

  • squid:S1068: Unused private fields should be removed
  • squid:S1155: Collection.isEmpty() should be used to test for emptiness
  • squid:S1185: Overriding methods should do more than simply call the same method in the super class
  • squid:S1694: An abstract class should have both abstract and concrete methods
  • squid:S1905: Redundant casts should not be used
  • squid:S2065: Fields in non-serializable classes should not be "transient"
  • squid:S2583: Conditions should not unconditionally evaluate to "TRUE" or to "FALSE"
  • squid:ModifiersOrderCheck: Modifiers should be declared in the correct order
File size: 18.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.cache;
3
4import java.io.ByteArrayOutputStream;
5import java.io.FileNotFoundException;
6import java.io.IOException;
7import java.io.InputStream;
8import java.net.HttpURLConnection;
9import java.net.URL;
10import java.net.URLConnection;
11import java.util.HashSet;
12import java.util.Map;
13import java.util.Random;
14import java.util.Set;
15import java.util.concurrent.ConcurrentHashMap;
16import java.util.concurrent.ConcurrentMap;
17import java.util.concurrent.Executor;
18import java.util.concurrent.LinkedBlockingDeque;
19import java.util.concurrent.RejectedExecutionException;
20import java.util.concurrent.ThreadPoolExecutor;
21import java.util.concurrent.TimeUnit;
22import java.util.logging.Level;
23import java.util.logging.Logger;
24
25import org.apache.commons.jcs.access.behavior.ICacheAccess;
26import org.apache.commons.jcs.engine.behavior.ICacheElement;
27import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
28import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
29import org.openstreetmap.josm.data.preferences.IntegerProperty;
30
31/**
32 * @author Wiktor Niesiobędzki
33 *
34 * @param <K> cache entry key type
35 *
36 * Generic loader for HTTP based tiles. Uses custom attribute, to check, if entry has expired
37 * according to HTTP headers sent with tile. If so, it tries to verify using Etags
38 * or If-Modified-Since / Last-Modified.
39 *
40 * If the tile is not valid, it will try to download it from remote service and put it
41 * to cache. If remote server will fail it will try to use stale entry.
42 *
43 * This class will keep only one Job running for specified tile. All others will just finish, but
44 * listeners will be gathered and notified, once download job will be finished
45 *
46 * @since 8168
47 */
48public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements ICachedLoaderJob<K>, Runnable {
49 private static final Logger log = FeatureAdapter.getLogger(JCSCachedTileLoaderJob.class.getCanonicalName());
50 protected static final long DEFAULT_EXPIRE_TIME = 1000L * 60 * 60 * 24 * 7; // 7 days
51 // Limit for the max-age value send by the server.
52 protected static final long EXPIRE_TIME_SERVER_LIMIT = 1000L * 60 * 60 * 24 * 28; // 4 weeks
53 // Absolute expire time limit. Cached tiles that are older will not be used,
54 // even if the refresh from the server fails.
55 protected static final long ABSOLUTE_EXPIRE_TIME_LIMIT = Long.MAX_VALUE; // unlimited
56
57 /**
58 * maximum download threads that will be started
59 */
60 public static final IntegerProperty THREAD_LIMIT = new IntegerProperty("cache.jcs.max_threads", 10);
61
62 public static class LIFOQueue extends LinkedBlockingDeque<Runnable> {
63 public LIFOQueue(int capacity) {
64 super(capacity);
65 }
66
67 @Override
68 public boolean offer(Runnable t) {
69 return super.offerFirst(t);
70 }
71
72 @Override
73 public Runnable remove() {
74 return super.removeFirst();
75 }
76 }
77
78
79 /*
80 * ThreadPoolExecutor starts new threads, until THREAD_LIMIT is reached. Then it puts tasks into LIFOQueue, which is fairly
81 * small, but we do not want a lot of outstanding tasks queued, but rather prefer the class consumer to resubmit the task, which are
82 * important right now.
83 *
84 * This way, if some task gets outdated (for example - user paned the map, and we do not want to download this tile any more),
85 * the task will not be resubmitted, and thus - never queued.
86 *
87 * There is no point in canceling tasks, that are already taken by worker threads (if we made so much effort, we can at least cache
88 * the response, so later it could be used). We could actually cancel what is in LIFOQueue, but this is a tradeoff between simplicity
89 * and performance (we do want to have something to offer to worker threads before tasks will be resubmitted by class consumer)
90 */
91 private static Executor DOWNLOAD_JOB_DISPATCHER = new ThreadPoolExecutor(
92 2, // we have a small queue, so threads will be quickly started (threads are started only, when queue is full)
93 THREAD_LIMIT.get().intValue(), // do not this number of threads
94 30, // keepalive for thread
95 TimeUnit.SECONDS,
96 // make queue of LIFO type - so recently requested tiles will be loaded first (assuming that these are which user is waiting to see)
97 new LIFOQueue(5)
98 );
99
100 private static ConcurrentMap<String,Set<ICachedLoaderListener>> inProgress = new ConcurrentHashMap<>();
101 private static ConcurrentMap<String, Boolean> useHead = new ConcurrentHashMap<>();
102
103 private long now; // when the job started
104
105 private ICacheAccess<K, V> cache;
106 private ICacheElement<K, V> cacheElement;
107 protected V cacheData = null;
108 protected CacheEntryAttributes attributes = null;
109
110 // HTTP connection parameters
111 private int connectTimeout;
112 private int readTimeout;
113 private Map<String, String> headers;
114
115 /**
116 * @param cache cache instance that we will work on
117 * @param headers
118 * @param readTimeout
119 * @param connectTimeout
120 */
121 public JCSCachedTileLoaderJob(ICacheAccess<K,V> cache,
122 int connectTimeout, int readTimeout,
123 Map<String, String> headers) {
124
125 this.cache = cache;
126 this.now = System.currentTimeMillis();
127 this.connectTimeout = connectTimeout;
128 this.readTimeout = readTimeout;
129 this.headers = headers;
130 }
131
132 private void ensureCacheElement() {
133 if (cacheElement == null && getCacheKey() != null) {
134 cacheElement = cache.getCacheElement(getCacheKey());
135 if (cacheElement != null) {
136 attributes = (CacheEntryAttributes) cacheElement.getElementAttributes();
137 cacheData = cacheElement.getVal();
138 }
139 }
140 }
141
142 public V get() {
143 ensureCacheElement();
144 return cacheData;
145 }
146
147 @Override
148 public void submit(ICachedLoaderListener listener) {
149 boolean first = false;
150 URL url = getUrl();
151 String deduplicationKey = null;
152 if (url != null) {
153 // url might be null, for example when Bing Attribution is not loaded yet
154 deduplicationKey = url.toString();
155 }
156 if (deduplicationKey == null) {
157 log.log(Level.WARNING, "No url returned for: {0}, skipping", getCacheKey());
158 return;
159 }
160 synchronized (inProgress) {
161 Set<ICachedLoaderListener> newListeners = inProgress.get(deduplicationKey);
162 if (newListeners == null) {
163 newListeners = new HashSet<>();
164 inProgress.put(deduplicationKey, newListeners);
165 first = true;
166 }
167 newListeners.add(listener);
168 }
169
170 if (first) {
171 ensureCacheElement();
172 if (cacheElement != null && isCacheElementValid() && (isObjectLoadable())) {
173 // we got something in cache, and it's valid, so lets return it
174 log.log(Level.FINE, "JCS - Returning object from cache: {0}", getCacheKey());
175 finishLoading(LoadResult.SUCCESS);
176 return;
177 }
178 // object not in cache, so submit work to separate thread
179 try {
180 if (executionGuard()) {
181 // use getter method, so subclasses may override executors, to get separate thread pool
182 getDownloadExecutor().execute(this);
183 } else {
184 log.log(Level.FINE, "JCS - guard rejected job for: {0}", getCacheKey());
185 finishLoading(LoadResult.REJECTED);
186 }
187 } catch (RejectedExecutionException e) {
188 // queue was full, try again later
189 log.log(Level.FINE, "JCS - rejected job for: {0}", getCacheKey());
190 finishLoading(LoadResult.REJECTED);
191 }
192 }
193 }
194
195 /**
196 * Guard method for execution. If guard returns true, the execution of download task will commence
197 * otherwise, execution will finish with result LoadResult.REJECTED
198 *
199 * It is responsibility of the overriding class, to handle properly situation in finishLoading class
200 * @return
201 */
202 protected boolean executionGuard() {
203 return true;
204 }
205
206 /**
207 * This method is run when job has finished
208 */
209 protected void executionFinished() {
210 }
211
212 /**
213 *
214 * @return checks if object from cache has sufficient data to be returned
215 */
216 protected boolean isObjectLoadable() {
217 byte[] content = cacheData.getContent();
218 return content != null && content.length > 0;
219 }
220
221 /**
222 *
223 * @return cache object as empty, regardless of what remote resource has returned (ex. based on headers)
224 */
225 protected boolean cacheAsEmpty() {
226 return false;
227 }
228
229 /**
230 * @return key under which discovered server settings will be kept
231 */
232 protected String getServerKey() {
233 return getUrl().getHost();
234 }
235
236 /**
237 * this needs to be non-static, so it can be overridden by subclasses
238 */
239 protected Executor getDownloadExecutor() {
240 return DOWNLOAD_JOB_DISPATCHER;
241 }
242
243
244 public void run() {
245 final Thread currentThread = Thread.currentThread();
246 final String oldName = currentThread.getName();
247 currentThread.setName("JCS Downloading: " + getUrl());
248 try {
249 // try to load object from remote resource
250 if (loadObject()) {
251 finishLoading(LoadResult.SUCCESS);
252 } else {
253 // if loading failed - check if we can return stale entry
254 if (isObjectLoadable()) {
255 // try to get stale entry in cache
256 finishLoading(LoadResult.SUCCESS);
257 log.log(Level.FINE, "JCS - found stale object in cache: {0}", getUrl());
258 } else {
259 // failed completely
260 finishLoading(LoadResult.FAILURE);
261 }
262 }
263 } finally {
264 executionFinished();
265 currentThread.setName(oldName);
266 }
267 }
268
269
270 private void finishLoading(LoadResult result) {
271 Set<ICachedLoaderListener> listeners = null;
272 synchronized (inProgress) {
273 listeners = inProgress.remove(getUrl().toString());
274 }
275 if (listeners == null) {
276 log.log(Level.WARNING, "Listener not found for URL: {0}. Listener not notified!", getUrl());
277 return;
278 }
279 try {
280 for (ICachedLoaderListener l: listeners) {
281 l.loadingFinished(cacheData, result);
282 }
283 } catch (Exception e) {
284 log.log(Level.WARNING, "JCS - Error while loading object from cache: {0}; {1}", new Object[]{e.getMessage(), getUrl()});
285 log.log(Level.FINE, "Stacktrace", e);
286 for (ICachedLoaderListener l: listeners) {
287 l.loadingFinished(cacheData, LoadResult.FAILURE);
288 }
289
290 }
291
292 }
293
294 private boolean isCacheElementValid() {
295 long expires = attributes.getExpirationTime();
296
297 // check by expire date set by server
298 if (expires != 0L) {
299 // put a limit to the expire time (some servers send a value
300 // that is too large)
301 expires = Math.min(expires, attributes.getCreateTime() + EXPIRE_TIME_SERVER_LIMIT);
302 if (now > expires) {
303 log.log(Level.FINE, "JCS - Object {0} has expired -> valid to {1}, now is: {2}", new Object[]{getUrl(), Long.toString(expires), Long.toString(now)});
304 return false;
305 }
306 } else {
307 // check by file modification date
308 if (now - attributes.getLastModification() > DEFAULT_EXPIRE_TIME) {
309 log.log(Level.FINE, "JCS - Object has expired, maximum file age reached {0}", getUrl());
310 return false;
311 }
312 }
313 return true;
314 }
315
316 /**
317 * @return true if object was successfully downloaded, false, if there was a loading failure
318 */
319
320 private boolean loadObject() {
321 try {
322 // if we have object in cache, and host doesn't support If-Modified-Since nor If-None-Match
323 // then just use HEAD request and check returned values
324 if (isObjectLoadable() &&
325 Boolean.TRUE.equals(useHead.get(getServerKey())) &&
326 isCacheValidUsingHead()) {
327 log.log(Level.FINE, "JCS - cache entry verified using HEAD request: {0}", getUrl());
328 return true;
329 }
330 URLConnection urlConn = getURLConnection();
331
332 if (isObjectLoadable() &&
333 (now - attributes.getLastModification()) <= ABSOLUTE_EXPIRE_TIME_LIMIT) {
334 urlConn.setIfModifiedSince(attributes.getLastModification());
335 }
336 if (isObjectLoadable() && attributes.getEtag() != null) {
337 urlConn.addRequestProperty("If-None-Match", attributes.getEtag());
338 }
339 if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 304) {
340 // If isModifiedSince or If-None-Match has been set
341 // and the server answers with a HTTP 304 = "Not Modified"
342 log.log(Level.FINE, "JCS - IfModifiedSince/Etag test: local version is up to date: {0}", getUrl());
343 return true;
344 } else if (isObjectLoadable()) {
345 // we have an object in cache, but we haven't received 304 resposne code
346 // check if we should use HEAD request to verify
347 if((attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getRequestProperty("ETag"))) ||
348 attributes.getLastModification() == urlConn.getLastModified()) {
349 // we sent ETag or If-Modified-Since, but didn't get 304 response code
350 // for further requests - use HEAD
351 String serverKey = getServerKey();
352 log.log(Level.INFO, "JCS - Host: {0} found not to return 304 codes for If-Modifed-Since or If-None-Match headers", serverKey);
353 useHead.put(serverKey, Boolean.TRUE);
354 }
355 }
356
357 attributes = parseHeaders(urlConn);
358
359 for (int i = 0; i < 5; ++i) {
360 if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 503) {
361 Thread.sleep(5000+(new Random()).nextInt(5000));
362 continue;
363 }
364 byte[] raw = read(urlConn);
365
366 if (!cacheAsEmpty() && raw != null && raw.length > 0) {
367 cacheData = createCacheEntry(raw);
368 cache.put(getCacheKey(), cacheData, attributes);
369 log.log(Level.FINE, "JCS - downloaded key: {0}, length: {1}, url: {2}",
370 new Object[] {getCacheKey(), raw.length, getUrl()});
371 return true;
372 } else {
373 cacheData = createCacheEntry(new byte[]{});
374 cache.put(getCacheKey(), cacheData, attributes);
375 log.log(Level.FINE, "JCS - Caching empty object {0}", getUrl());
376 return true;
377 }
378 }
379 } catch (FileNotFoundException e) {
380 log.log(Level.FINE, "JCS - Caching empty object as server returned 404 for: {0}", getUrl());
381 cache.put(getCacheKey(), createCacheEntry(new byte[]{}), attributes);
382 return handleNotFound();
383 } catch (Exception e) {
384 log.log(Level.WARNING, "JCS - Exception during download " + getUrl(), e);
385 }
386 log.log(Level.WARNING, "JCS - Silent failure during download: {0}", getUrl());
387 return false;
388
389 }
390
391 /**
392 * @return if we should treat this object as properly loaded
393 */
394 protected abstract boolean handleNotFound();
395
396 protected abstract V createCacheEntry(byte[] content);
397
398 private CacheEntryAttributes parseHeaders(URLConnection urlConn) {
399 CacheEntryAttributes ret = new CacheEntryAttributes();
400 ret.setNoTileAtZoom("no-tile".equals(urlConn.getHeaderField("X-VE-Tile-Info")));
401
402 Long lng = urlConn.getExpiration();
403 if (lng.equals(0L)) {
404 try {
405 String str = urlConn.getHeaderField("Cache-Control");
406 if (str != null) {
407 for (String token: str.split(",")) {
408 if (token.startsWith("max-age=")) {
409 lng = Long.parseLong(token.substring(8)) * 1000 +
410 System.currentTimeMillis();
411 }
412 }
413 }
414 } catch (NumberFormatException e) {} //ignore malformed Cache-Control headers
415 }
416
417 ret.setExpirationTime(lng);
418 ret.setLastModification(now);
419 ret.setEtag(urlConn.getHeaderField("ETag"));
420 return ret;
421 }
422
423 private HttpURLConnection getURLConnection() throws IOException {
424 HttpURLConnection urlConn = (HttpURLConnection) getUrl().openConnection();
425 urlConn.setRequestProperty("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
426 urlConn.setReadTimeout(readTimeout); // 30 seconds read timeout
427 urlConn.setConnectTimeout(connectTimeout);
428 for(Map.Entry<String, String> e: headers.entrySet()) {
429 urlConn.setRequestProperty(e.getKey(), e.getValue());
430 }
431 return urlConn;
432 }
433
434 private boolean isCacheValidUsingHead() throws IOException {
435 HttpURLConnection urlConn = (HttpURLConnection) getUrl().openConnection();
436 urlConn.setRequestMethod("HEAD");
437 long lastModified = urlConn.getLastModified();
438 return (
439 (attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getRequestProperty("ETag"))) ||
440 (lastModified != 0 && lastModified <= attributes.getLastModification())
441 );
442 }
443
444 private static byte[] read(URLConnection urlConn) throws IOException {
445 InputStream input = urlConn.getInputStream();
446 try {
447 ByteArrayOutputStream bout = new ByteArrayOutputStream(input.available());
448 byte[] buffer = new byte[2048];
449 boolean finished = false;
450 do {
451 int read = input.read(buffer);
452 if (read >= 0) {
453 bout.write(buffer, 0, read);
454 } else {
455 finished = true;
456 }
457 } while (!finished);
458 if (bout.size() == 0)
459 return null;
460 return bout.toByteArray();
461 } finally {
462 input.close();
463 }
464 }
465}
Note: See TracBrowser for help on using the repository browser.