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

Last change on this file since 8425 was 8425, checked in by wiktorn, 9 years ago

Limit the tile expiration date between 1 hour to 1 month. Fix Sonar warning about log variable naming

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