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

Last change on this file since 13750 was 13744, checked in by wiktorn, 6 years ago

Remove SupressFBwarning

  • Property svn:eol-style set to native
File size: 12.3 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.nio.charset.StandardCharsets;
10import java.util.HashSet;
11import java.util.List;
12import java.util.Map;
13import java.util.Map.Entry;
14import java.util.Optional;
15import java.util.Set;
16import java.util.concurrent.ConcurrentHashMap;
17import java.util.concurrent.ConcurrentMap;
18import java.util.concurrent.ThreadPoolExecutor;
19import java.util.concurrent.TimeUnit;
20import java.util.regex.Matcher;
21import java.util.regex.Pattern;
22
23import org.apache.commons.jcs.access.behavior.ICacheAccess;
24import org.openstreetmap.gui.jmapviewer.Tile;
25import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
26import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
27import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
28import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
29import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
30import org.openstreetmap.josm.data.cache.CacheEntry;
31import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
32import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
33import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
34import org.openstreetmap.josm.data.preferences.LongProperty;
35import org.openstreetmap.josm.tools.HttpClient;
36import org.openstreetmap.josm.tools.Logging;
37import org.openstreetmap.josm.tools.Utils;
38
39/**
40 * Class bridging TMS requests to JCS cache requests
41 *
42 * @author Wiktor Niesiobędzki
43 * @since 8168
44 */
45public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener {
46 /** General maximum expires for tiles. Might be overridden by imagery settings */
47 public static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires", TimeUnit.DAYS.toMillis(30));
48 /** General minimum expires for tiles. Might be overridden by imagery settings */
49 public static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires", TimeUnit.HOURS.toMillis(1));
50 static final Pattern SERVICE_EXCEPTION_PATTERN = Pattern.compile("(?s).+<ServiceException[^>]*>(.+)</ServiceException>.+");
51 protected final Tile tile;
52 private volatile URL url;
53 private final TileJobOptions options;
54
55 // we need another deduplication of Tile Loader listeners, as for each submit, new TMSCachedTileLoaderJob was created
56 // that way, we reduce calls to tileLoadingFinished, and general CPU load due to surplus Map repaints
57 private static final ConcurrentMap<String, Set<TileLoaderListener>> inProgress = new ConcurrentHashMap<>();
58
59 /**
60 * Constructor for creating a job, to get a specific tile from cache
61 * @param listener Tile loader listener
62 * @param tile to be fetched from cache
63 * @param cache object
64 * @param options for job (such as http headers, timeouts etc.)
65 * @param downloadExecutor that will be executing the jobs
66 */
67
68 public TMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
69 ICacheAccess<String, BufferedImageCacheEntry> cache,
70 TileJobOptions options,
71 ThreadPoolExecutor downloadExecutor) {
72 super(cache, options, downloadExecutor);
73 this.tile = tile;
74 this.options = options;
75 if (listener != null) {
76 String deduplicationKey = getCacheKey();
77 synchronized (inProgress) {
78 inProgress.computeIfAbsent(deduplicationKey, k -> new HashSet<>()).add(listener);
79 }
80 }
81 }
82
83 @Override
84 public String getCacheKey() {
85 if (tile != null) {
86 TileSource tileSource = tile.getTileSource();
87 return Optional.ofNullable(tileSource.getName()).orElse("").replace(':', '_') + ':'
88 + tileSource.getTileId(tile.getZoom(), tile.getXtile(), tile.getYtile());
89 }
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() throws IOException {
104 if (url == null) {
105 synchronized (this) {
106 if (url == null) {
107 String sUrl = tile.getUrl();
108 if (!"".equals(sUrl)) {
109 url = new URL(sUrl);
110 }
111 }
112 }
113 }
114 return url;
115 }
116
117 @Override
118 public boolean isObjectLoadable() {
119 if (cacheData != null) {
120 byte[] content = cacheData.getContent();
121 try {
122 return content.length > 0 || cacheData.getImage() != null || isNoTileAtZoom();
123 } catch (IOException e) {
124 Logging.logWithStackTrace(Logging.LEVEL_WARN, e, "JCS TMS - error loading from cache for tile {0}: {1}",
125 new Object[] {tile.getKey(), e.getMessage()}
126 );
127 }
128 }
129 return false;
130 }
131
132 @Override
133 protected boolean isResponseLoadable(Map<String, List<String>> headers, int statusCode, byte[] content) {
134 attributes.setMetadata(tile.getTileSource().getMetadata(headers));
135 if (tile.getTileSource().isNoTileAtZoom(headers, statusCode, content)) {
136 attributes.setNoTileAtZoom(true);
137 return false; // do no try to load data from no-tile at zoom, cache empty object instead
138 }
139 return super.isResponseLoadable(headers, statusCode, content);
140 }
141
142 @Override
143 protected boolean cacheAsEmpty() {
144 return isNoTileAtZoom() || super.cacheAsEmpty();
145 }
146
147 @Override
148 public void submit(boolean force) {
149 tile.initLoading();
150 try {
151 super.submit(this, force);
152 } catch (IOException | IllegalArgumentException e) {
153 // if we fail to submit the job, mark tile as loaded and set error message
154 Logging.log(Logging.LEVEL_WARN, e);
155 tile.finishLoading();
156 tile.setError(e.getMessage());
157 }
158 }
159
160 @Override
161 public void loadingFinished(CacheEntry object, CacheEntryAttributes attributes, LoadResult result) {
162 this.attributes = attributes; // as we might get notification from other object than our selfs, pass attributes along
163 Set<TileLoaderListener> listeners;
164 synchronized (inProgress) {
165 listeners = inProgress.remove(getCacheKey());
166 }
167 boolean status = result.equals(LoadResult.SUCCESS);
168
169 try {
170 tile.finishLoading(); // whatever happened set that loading has finished
171 // set tile metadata
172 if (this.attributes != null) {
173 for (Entry<String, String> e: this.attributes.getMetadata().entrySet()) {
174 tile.putValue(e.getKey(), e.getValue());
175 }
176 }
177
178 switch(result) {
179 case SUCCESS:
180 handleNoTileAtZoom();
181 if (attributes != null) {
182 int httpStatusCode = attributes.getResponseCode();
183 if (httpStatusCode >= 400 && !isNoTileAtZoom()) {
184 if (attributes.getErrorMessage() == null) {
185 tile.setError(tr("HTTP error {0} when loading tiles", httpStatusCode));
186 } else {
187 tile.setError(tr("Error downloading tiles: {0}", attributes.getErrorMessage()));
188 }
189 status = false;
190 }
191 }
192 status &= tryLoadTileImage(object); //try to keep returned image as background
193 break;
194 case FAILURE:
195 tile.setError("Problem loading tile");
196 tryLoadTileImage(object);
197 break;
198 case CANCELED:
199 tile.loadingCanceled();
200 // do nothing
201 }
202
203 // always check, if there is some listener interested in fact, that tile has finished loading
204 if (listeners != null) { // listeners might be null, if some other thread notified already about success
205 for (TileLoaderListener l: listeners) {
206 l.tileLoadingFinished(tile, status);
207 }
208 }
209 } catch (IOException e) {
210 Logging.warn("JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
211 tile.setError(e);
212 tile.setLoaded(false);
213 if (listeners != null) { // listeners might be null, if some other thread notified already about success
214 for (TileLoaderListener l: listeners) {
215 l.tileLoadingFinished(tile, false);
216 }
217 }
218 }
219 }
220
221 /**
222 * For TMS use BaseURL as settings discovery, so for different paths, we will have different settings (useful for developer servers)
223 *
224 * @return base URL of TMS or server url as defined in super class
225 */
226 @Override
227 protected String getServerKey() {
228 TileSource ts = tile.getSource();
229 if (ts instanceof AbstractTMSTileSource) {
230 return ((AbstractTMSTileSource) ts).getBaseUrl();
231 }
232 return super.getServerKey();
233 }
234
235 @Override
236 protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
237 return new BufferedImageCacheEntry(content);
238 }
239
240 @Override
241 public void submit() {
242 submit(false);
243 }
244
245 @Override
246 protected CacheEntryAttributes parseHeaders(HttpClient.Response urlConn) {
247 CacheEntryAttributes ret = super.parseHeaders(urlConn);
248 // keep the expiration time between MINIMUM_EXPIRES and MAXIMUM_EXPIRES, so we will cache the tiles
249 // at least for some short period of time, but not too long
250 if (ret.getExpirationTime() < now + Math.max(MINIMUM_EXPIRES.get(), options.getMinimumExpiryTime())) {
251 ret.setExpirationTime(now + MINIMUM_EXPIRES.get());
252 }
253 if (ret.getExpirationTime() > now + Math.max(MAXIMUM_EXPIRES.get(), options.getMinimumExpiryTime())) {
254 ret.setExpirationTime(now + MAXIMUM_EXPIRES.get());
255 }
256 return ret;
257 }
258
259 private boolean handleNoTileAtZoom() {
260 if (isNoTileAtZoom()) {
261 Logging.debug("JCS TMS - Tile valid, but no file, as no tiles at this level {0}", tile);
262 tile.setError("No tile at this zoom level");
263 tile.putValue("tile-info", "no-tile");
264 return true;
265 }
266 return false;
267 }
268
269 private boolean isNoTileAtZoom() {
270 if (attributes == null) {
271 Logging.warn("Cache attributes are null");
272 }
273 return attributes != null && attributes.isNoTileAtZoom();
274 }
275
276 private boolean tryLoadTileImage(CacheEntry object) throws IOException {
277 if (object != null) {
278 byte[] content = object.getContent();
279 if (content.length > 0) {
280 try (ByteArrayInputStream in = new ByteArrayInputStream(content)) {
281 tile.loadImage(in);
282 if (tile.getImage() == null) {
283 String s = new String(content, StandardCharsets.UTF_8);
284 Matcher m = SERVICE_EXCEPTION_PATTERN.matcher(s);
285 if (m.matches()) {
286 String message = Utils.strip(m.group(1));
287 tile.setError(message);
288 Logging.error(message);
289 Logging.debug(s);
290 } else {
291 tile.setError(tr("Could not load image from tile server"));
292 }
293 return false;
294 }
295 } catch (UnsatisfiedLinkError | SecurityException e) {
296 throw new IOException(e);
297 }
298 }
299 }
300 return true;
301 }
302}
Note: See TracBrowser for help on using the repository browser.