source: josm/trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java@ 10627

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

sonar - squid:S1166 - Exception handlers should preserve the original exceptions

  • Property svn:eol-style set to native
File size: 13.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.imagery;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.ByteArrayInputStream;
7import java.io.IOException;
8import java.net.URL;
9import java.util.HashSet;
10import java.util.List;
11import java.util.Map;
12import java.util.Map.Entry;
13import java.util.Set;
14import java.util.concurrent.ConcurrentHashMap;
15import java.util.concurrent.ConcurrentMap;
16import java.util.concurrent.ThreadPoolExecutor;
17import java.util.logging.Level;
18import java.util.logging.Logger;
19
20import org.apache.commons.jcs.access.behavior.ICacheAccess;
21import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
22import org.openstreetmap.gui.jmapviewer.Tile;
23import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
24import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
25import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
26import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
27import org.openstreetmap.josm.Main;
28import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
29import org.openstreetmap.josm.data.cache.CacheEntry;
30import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
31import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
32import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
33import org.openstreetmap.josm.data.preferences.LongProperty;
34import org.openstreetmap.josm.tools.HttpClient;
35
36/**
37 * @author Wiktor Niesiobędzki
38 *
39 * Class bridging TMS requests to JCS cache requests
40 * @since 8168
41 */
42public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener {
43 private static final Logger LOG = FeatureAdapter.getLogger(TMSCachedTileLoaderJob.class.getCanonicalName());
44 private static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires",
45 30 /*days*/ * 24 /*hours*/ * 60 /*minutes*/ * 60 /*seconds*/ *1000L /*milliseconds*/);
46 private static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires",
47 1 /*hour*/ * 60 /*minutes*/ * 60 /*seconds*/ *1000L /*milliseconds*/);
48 private final Tile tile;
49 private volatile URL url;
50
51 // we need another deduplication of Tile Loader listeners, as for each submit, new TMSCachedTileLoaderJob was created
52 // that way, we reduce calls to tileLoadingFinished, and general CPU load due to surplus Map repaints
53 private static final ConcurrentMap<String, Set<TileLoaderListener>> inProgress = new ConcurrentHashMap<>();
54
55 /**
56 * Constructor for creating a job, to get a specific tile from cache
57 * @param listener Tile loader listener
58 * @param tile to be fetched from cache
59 * @param cache object
60 * @param connectTimeout when connecting to remote resource
61 * @param readTimeout when connecting to remote resource
62 * @param headers HTTP headers to be sent together with request
63 * @param downloadExecutor that will be executing the jobs
64 */
65 public TMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
66 ICacheAccess<String, BufferedImageCacheEntry> cache,
67 int connectTimeout, int readTimeout, Map<String, String> headers,
68 ThreadPoolExecutor downloadExecutor) {
69 super(cache, connectTimeout, readTimeout, headers, downloadExecutor);
70 this.tile = tile;
71 if (listener != null) {
72 String deduplicationKey = getCacheKey();
73 synchronized (inProgress) {
74 Set<TileLoaderListener> newListeners = inProgress.get(deduplicationKey);
75 if (newListeners == null) {
76 newListeners = new HashSet<>();
77 inProgress.put(deduplicationKey, newListeners);
78 }
79 newListeners.add(listener);
80 }
81 }
82 }
83
84 @Override
85 public Tile getTile() {
86 return getCachedTile();
87 }
88
89 @Override
90 public String getCacheKey() {
91 if (tile != null) {
92 TileSource tileSource = tile.getTileSource();
93 String tsName = tileSource.getName();
94 if (tsName == null) {
95 tsName = "";
96 }
97 return tsName.replace(':', '_') + ':' + tileSource.getTileId(tile.getZoom(), tile.getXtile(), tile.getYtile());
98 }
99 return null;
100 }
101
102 /*
103 * this doesn't needs to be synchronized, as it's not that costly to keep only one execution
104 * in parallel, but URL creation and Tile.getUrl() are costly and are not needed when fetching
105 * data from cache, that's why URL creation is postponed until it's needed
106 *
107 * We need to have static url value for TileLoaderJob, as for some TileSources we might get different
108 * URL's each call we made (servers switching), and URL's are used below as a key for duplicate detection
109 *
110 */
111 @Override
112 public URL getUrl() throws IOException {
113 if (url == null) {
114 synchronized (this) {
115 if (url == null)
116 url = new URL(tile.getUrl());
117 }
118 }
119 return url;
120 }
121
122 @Override
123 public boolean isObjectLoadable() {
124 if (cacheData != null) {
125 byte[] content = cacheData.getContent();
126 try {
127 return content != null || cacheData.getImage() != null || isNoTileAtZoom();
128 } catch (IOException e) {
129 LOG.log(Level.WARNING, "JCS TMS - error loading from cache for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
130 Main.warn(e);
131 }
132 }
133 return false;
134 }
135
136 @Override
137 protected boolean isResponseLoadable(Map<String, List<String>> headers, int statusCode, byte[] content) {
138 attributes.setMetadata(tile.getTileSource().getMetadata(headers));
139 if (tile.getTileSource().isNoTileAtZoom(headers, statusCode, content)) {
140 attributes.setNoTileAtZoom(true);
141 return false; // do no try to load data from no-tile at zoom, cache empty object instead
142 }
143 return super.isResponseLoadable(headers, statusCode, content);
144 }
145
146 @Override
147 protected boolean cacheAsEmpty() {
148 return isNoTileAtZoom() || super.cacheAsEmpty();
149 }
150
151 @Override
152 public void submit(boolean force) {
153 tile.initLoading();
154 try {
155 super.submit(this, force);
156 } catch (IOException e) {
157 // if we fail to submit the job, mark tile as loaded and set error message
158 Main.warn(e, false);
159 tile.finishLoading();
160 tile.setError(e.getMessage());
161 }
162 }
163
164 @Override
165 public void loadingFinished(CacheEntry object, CacheEntryAttributes attributes, LoadResult result) {
166 this.attributes = attributes; // as we might get notification from other object than our selfs, pass attributes along
167 Set<TileLoaderListener> listeners;
168 synchronized (inProgress) {
169 listeners = inProgress.remove(getCacheKey());
170 }
171 boolean status = result.equals(LoadResult.SUCCESS);
172
173 try {
174 tile.finishLoading(); // whatever happened set that loading has finished
175 // set tile metadata
176 if (this.attributes != null) {
177 for (Entry<String, String> e: this.attributes.getMetadata().entrySet()) {
178 tile.putValue(e.getKey(), e.getValue());
179 }
180 }
181
182 switch(result) {
183 case SUCCESS:
184 handleNoTileAtZoom();
185 int httpStatusCode = attributes.getResponseCode();
186 if (!isNoTileAtZoom() && httpStatusCode >= 400) {
187 if (attributes.getErrorMessage() == null) {
188 tile.setError(tr("HTTP error {0} when loading tiles", httpStatusCode));
189 } else {
190 tile.setError(tr("Error downloading tiles: {0}", attributes.getErrorMessage()));
191 }
192 status = false;
193 }
194 status &= tryLoadTileImage(object); //try to keep returned image as background
195 break;
196 case FAILURE:
197 tile.setError("Problem loading tile");
198 tryLoadTileImage(object);
199 break;
200 case CANCELED:
201 tile.loadingCanceled();
202 // do nothing
203 }
204
205 // always check, if there is some listener interested in fact, that tile has finished loading
206 if (listeners != null) { // listeners might be null, if some other thread notified already about success
207 for (TileLoaderListener l: listeners) {
208 l.tileLoadingFinished(tile, status);
209 }
210 }
211 } catch (IOException e) {
212 LOG.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
213 tile.setError(e);
214 tile.setLoaded(false);
215 if (listeners != null) { // listeners might be null, if some other thread notified already about success
216 for (TileLoaderListener l: listeners) {
217 l.tileLoadingFinished(tile, false);
218 }
219 }
220 }
221 }
222
223 /**
224 * For TMS use BaseURL as settings discovery, so for different paths, we will have different settings (useful for developer servers)
225 *
226 * @return base URL of TMS or server url as defined in super class
227 */
228 @Override
229 protected String getServerKey() {
230 TileSource ts = tile.getSource();
231 if (ts instanceof AbstractTMSTileSource) {
232 return ((AbstractTMSTileSource) ts).getBaseUrl();
233 }
234 return super.getServerKey();
235 }
236
237 @Override
238 protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
239 return new BufferedImageCacheEntry(content);
240 }
241
242 @Override
243 public void submit() {
244 submit(false);
245 }
246
247 @Override
248 protected CacheEntryAttributes parseHeaders(HttpClient.Response urlConn) {
249 CacheEntryAttributes ret = super.parseHeaders(urlConn);
250 // keep the expiration time between MINIMUM_EXPIRES and MAXIMUM_EXPIRES, so we will cache the tiles
251 // at least for some short period of time, but not too long
252 if (ret.getExpirationTime() < now + MINIMUM_EXPIRES.get()) {
253 ret.setExpirationTime(now + MINIMUM_EXPIRES.get());
254 }
255 if (ret.getExpirationTime() > now + MAXIMUM_EXPIRES.get()) {
256 ret.setExpirationTime(now + MAXIMUM_EXPIRES.get());
257 }
258 return ret;
259 }
260
261 /**
262 * Method for getting the tile from cache only, without trying to reach remote resource
263 * @return tile or null, if nothing (useful) was found in cache
264 */
265 public Tile getCachedTile() {
266 BufferedImageCacheEntry data = get();
267 if (isObjectLoadable() && isCacheElementValid()) {
268 try {
269 // set tile metadata
270 if (this.attributes != null) {
271 for (Entry<String, String> e: this.attributes.getMetadata().entrySet()) {
272 tile.putValue(e.getKey(), e.getValue());
273 }
274 }
275
276 if (data != null) {
277 if (data.getImage() != null) {
278 tile.setImage(data.getImage());
279 tile.finishLoading();
280 } else {
281 // we had some data, but we didn't get any image. Malformed image?
282 tile.setError(tr("Could not load image from tile server"));
283 }
284 }
285 if (isNoTileAtZoom()) {
286 handleNoTileAtZoom();
287 tile.finishLoading();
288 }
289 if (attributes != null && attributes.getResponseCode() >= 400) {
290 if (attributes.getErrorMessage() == null) {
291 tile.setError(tr("HTTP error {0} when loading tiles", attributes.getResponseCode()));
292 } else {
293 tile.setError(tr("Error downloading tiles: {0}", attributes.getErrorMessage()));
294 }
295 }
296 return tile;
297 } catch (IOException e) {
298 LOG.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
299 Main.warn(e);
300 return null;
301 }
302
303 } else {
304 return tile;
305 }
306 }
307
308 private boolean handleNoTileAtZoom() {
309 if (isNoTileAtZoom()) {
310 LOG.log(Level.FINE, "JCS TMS - Tile valid, but no file, as no tiles at this level {0}", tile);
311 tile.setError("No tile at this zoom level");
312 tile.putValue("tile-info", "no-tile");
313 return true;
314 }
315 return false;
316 }
317
318 private boolean isNoTileAtZoom() {
319 if (attributes == null) {
320 LOG.warning("Cache attributes are null");
321 }
322 return attributes != null && attributes.isNoTileAtZoom();
323 }
324
325 private boolean tryLoadTileImage(CacheEntry object) throws IOException {
326 if (object != null) {
327 byte[] content = object.getContent();
328 if (content != null && content.length > 0) {
329 tile.loadImage(new ByteArrayInputStream(content));
330 if (tile.getImage() == null) {
331 tile.setError(tr("Could not load image from tile server"));
332 return false;
333 }
334 }
335 }
336 return true;
337 }
338}
Note: See TracBrowser for help on using the repository browser.