### Eclipse Workspace Patch 1.0
#P josm
diff --git src/org/openstreetmap/josm/data/imagery/GetCapabilitiesParseHelper.java src/org/openstreetmap/josm/data/imagery/GetCapabilitiesParseHelper.java
index 4988e9b..00a7bc2 100644
|
|
|
|
| 3 | 3 | |
| 4 | 4 | import java.io.InputStream; |
| 5 | 5 | import java.net.MalformedURLException; |
| 6 | | import java.net.URL; |
| | 6 | import java.net.URI; |
| | 7 | import java.net.URISyntaxException; |
| 7 | 8 | import java.util.Arrays; |
| 8 | 9 | import java.util.Locale; |
| 9 | 10 | import java.util.function.BiPredicate; |
| … |
… |
|
| 92 | 93 | */ |
| 93 | 94 | public static void moveReaderToEndCurrentTag(XMLStreamReader reader) throws XMLStreamException { |
| 94 | 95 | int level = 0; |
| 95 | | QName tag = reader.getName(); |
| | 96 | final QName tag = reader.getName(); |
| 96 | 97 | for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { |
| 97 | 98 | if (XMLStreamReader.START_ELEMENT == event) { |
| 98 | 99 | level += 1; |
| … |
… |
|
| 117 | 118 | * @throws XMLStreamException if any XML stream error occurs |
| 118 | 119 | */ |
| 119 | 120 | public static String getElementTextWithSubtags(XMLStreamReader reader) throws XMLStreamException { |
| 120 | | StringBuilder ret = new StringBuilder(); |
| | 121 | final StringBuilder ret = new StringBuilder(); |
| 121 | 122 | int level = 0; |
| 122 | | QName tag = reader.getName(); |
| | 123 | final QName tag = reader.getName(); |
| 123 | 124 | for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { |
| 124 | 125 | if (XMLStreamReader.START_ELEMENT == event) { |
| 125 | 126 | if (level > 0) { |
| … |
… |
|
| 167 | 168 | */ |
| 168 | 169 | public static boolean moveReaderToTag(XMLStreamReader reader, |
| 169 | 170 | BiPredicate<QName, QName> equalsFunc, QName... tags) throws XMLStreamException { |
| 170 | | QName stopTag = reader.getName(); |
| | 171 | final QName stopTag = reader.getName(); |
| 171 | 172 | int currentLevel = 0; |
| 172 | 173 | QName searchTag = tags[currentLevel]; |
| 173 | 174 | QName parentTag = null; |
| … |
… |
|
| 214 | 215 | * @throws XMLStreamException See {@link XMLStreamReader} |
| 215 | 216 | */ |
| 216 | 217 | public static TransferMode getTransferMode(XMLStreamReader reader) throws XMLStreamException { |
| 217 | | QName getQname = QN_OWS_GET; |
| | 218 | final QName getQname = QN_OWS_GET; |
| 218 | 219 | |
| 219 | 220 | Utils.ensure(getQname.equals(reader.getName()), "WMTS Parser state invalid. Expected element %s, got %s", |
| 220 | 221 | getQname, reader.getName()); |
| … |
… |
|
| 238 | 239 | * @throws MalformedURLException in case of malformed URL |
| 239 | 240 | * @since 10993 |
| 240 | 241 | */ |
| 241 | | public static String normalizeCapabilitiesUrl(String url) throws MalformedURLException { |
| 242 | | URL inUrl = new URL(url); |
| 243 | | URL ret = new URL(inUrl.getProtocol(), inUrl.getHost(), inUrl.getPort(), inUrl.getFile()); |
| 244 | | return ret.toExternalForm(); |
| | 242 | public static String normalizeCapabilitiesUrl(String url) throws MalformedURLException { |
| | 243 | if (url == null || url.isBlank()) { |
| | 244 | throw new MalformedURLException("URL string is null or blank"); |
| | 245 | } |
| | 246 | try { |
| | 247 | final URI inUri = new URI(url); |
| | 248 | final URI retUri = new URI(inUri.getScheme(), null, inUri.getHost(), inUri.getPort(), inUri.getPath(), inUri.getQuery(), null); |
| | 249 | return retUri.toURL().toExternalForm(); |
| | 250 | } catch (URISyntaxException e) { |
| | 251 | throw new MalformedURLException("Invalid URL syntax: " + e.getMessage()); |
| | 252 | } |
| 245 | 253 | } |
| 246 | 254 | |
| 247 | 255 | /** |
diff --git src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
index 03a71e2..e3e55e2 100644
|
|
|
|
| 6 | 6 | import java.io.ByteArrayInputStream; |
| 7 | 7 | import java.io.IOException; |
| 8 | 8 | import java.net.SocketTimeoutException; |
| | 9 | import java.net.URI; |
| | 10 | import java.net.URISyntaxException; |
| 9 | 11 | import java.net.URL; |
| 10 | 12 | import java.nio.charset.StandardCharsets; |
| 11 | 13 | import java.util.HashSet; |
| … |
… |
|
| 85 | 87 | |
| 86 | 88 | @Override |
| 87 | 89 | public String getCacheKey() { |
| 88 | | if (tile != null) { |
| 89 | | TileSource tileSource = tile.getTileSource(); |
| 90 | | return Optional.ofNullable(tileSource.getName()).orElse("").replace(ICache.NAME_COMPONENT_DELIMITER, "_") |
| 91 | | + ICache.NAME_COMPONENT_DELIMITER |
| 92 | | + tileSource.getTileId(tile.getZoom(), tile.getXtile(), tile.getYtile()); |
| 93 | | } |
| 94 | | return null; |
| | 90 | if (tile == null) { |
| | 91 | return null; |
| | 92 | } |
| | 93 | final TileSource tileSource = tile.getTileSource(); |
| | 94 | return Optional.ofNullable(tileSource.getName()).orElse("").replace(ICache.NAME_COMPONENT_DELIMITER, "_") |
| | 95 | + ICache.NAME_COMPONENT_DELIMITER |
| | 96 | + tileSource.getTileId(tile.getZoom(), tile.getXtile(), tile.getYtile()); |
| 95 | 97 | } |
| 96 | 98 | |
| 97 | 99 | /* |
| 98 | | * this doesn't needs to be synchronized, as it's not that costly to keep only one execution |
| 99 | | * in parallel, but URL creation and Tile.getUrl() are costly and are not needed when fetching |
| 100 | | * data from cache, that's why URL creation is postponed until it's needed |
| | 100 | * This doesn't need to be synchronized for the entire method, as it's not that costly |
| | 101 | * to keep only one execution in parallel. However, URL creation and tile.getUrl() |
| | 102 | * are costly and are not needed when fetching data from the cache. That's why |
| | 103 | * URL creation is postponed until it's actually needed. |
| 101 | 104 | * |
| 102 | | * We need to have static url value for TileLoaderJob, as for some TileSources we might get different |
| 103 | | * URL's each call we made (servers switching), and URL's are used below as a key for duplicate detection |
| 104 | | * |
| | 105 | * We need to have a static/stable URL value for TileLoaderJob, as for some TileSources |
| | 106 | * we might get different URLs on each call (due to server switching), and URLs are |
| | 107 | * used below as a key for duplicate detection. |
| 105 | 108 | */ |
| 106 | 109 | @Override |
| 107 | 110 | public URL getUrl() throws IOException { |
| 108 | 111 | if (url == null) { |
| 109 | 112 | synchronized (this) { |
| 110 | 113 | if (url == null) { |
| 111 | | String sUrl = tile.getUrl(); |
| 112 | | if (!"".equals(sUrl)) { |
| 113 | | url = new URL(sUrl); |
| | 114 | final String sUrl = tile.getUrl(); |
| | 115 | if (sUrl != null && !sUrl.isBlank()) { |
| | 116 | try { |
| | 117 | url = new URI(sUrl).toURL(); |
| | 118 | } catch (URISyntaxException e) { |
| | 119 | throw new IOException("Invalid URL format: " + sUrl, e); |
| | 120 | } |
| 114 | 121 | } |
| 115 | 122 | } |
| 116 | 123 | } |
| … |
… |
|
| 121 | 128 | @Override |
| 122 | 129 | public boolean isObjectLoadable() { |
| 123 | 130 | if (cacheData != null) { |
| 124 | | byte[] content = cacheData.getContent(); |
| | 131 | final byte[] content = cacheData.getContent(); |
| 125 | 132 | try { |
| 126 | 133 | return content.length > 0 || cacheData.getImage() != null || isNoTileAtZoom(); |
| 127 | 134 | } catch (IOException e) { |
| … |
… |
|
| 139 | 146 | attributes.setNoTileAtZoom(true); |
| 140 | 147 | return false; // do no try to load data from no-tile at zoom, cache empty object instead |
| 141 | 148 | } |
| 142 | | if (isNotImage(headers, statusCode)) { |
| 143 | | String message = detectErrorMessage(new String(content, StandardCharsets.UTF_8)); |
| 144 | | if (!Utils.isEmpty(message)) { |
| 145 | | tile.setError(message); |
| 146 | | } |
| 147 | | return false; |
| 148 | | } |
| 149 | | return super.isResponseLoadable(headers, statusCode, content); |
| | 149 | if (!isNotImage(headers, statusCode)) { |
| | 150 | return super.isResponseLoadable(headers, statusCode, content); |
| | 151 | } |
| | 152 | final String message = detectErrorMessage(new String(content, StandardCharsets.UTF_8)); |
| | 153 | if (!Utils.isEmpty(message)) { |
| | 154 | tile.setError(message); |
| | 155 | } |
| | 156 | return false; |
| 150 | 157 | } |
| 151 | 158 | |
| 152 | 159 | private boolean isNotImage(Map<String, List<String>> headers, int statusCode) { |
| 153 | 160 | if (statusCode == 200 && headers.containsKey("Content-Type") && !headers.get("Content-Type").isEmpty()) { |
| 154 | | String contentType = headers.get("Content-Type").stream().findAny().orElse("").toLowerCase(Locale.ROOT); |
| | 161 | final String contentType = headers.get("Content-Type").stream().findAny().orElse("").toLowerCase(Locale.ROOT); |
| 155 | 162 | if (!contentType.startsWith("image") |
| 156 | 163 | && !contentType.startsWith("application/octet-stream") |
| 157 | 164 | && !MVTFile.MIMETYPE.contains(contentType)) { |
| … |
… |
|
| 187 | 194 | @Override |
| 188 | 195 | public void loadingFinished(CacheEntry object, CacheEntryAttributes attributes, LoadResult result) { |
| 189 | 196 | this.attributes = attributes; // as we might get notification from other object than our selfs, pass attributes along |
| 190 | | Set<TileLoaderListener> listeners = inProgress.remove(getCacheKey()); |
| | 197 | final Set<TileLoaderListener> listeners = inProgress.remove(getCacheKey()); |
| 191 | 198 | boolean status = result == LoadResult.SUCCESS; |
| 192 | 199 | |
| 193 | 200 | try { |
| … |
… |
|
| 203 | 210 | case SUCCESS: |
| 204 | 211 | handleNoTileAtZoom(); |
| 205 | 212 | if (attributes != null) { |
| 206 | | int httpStatusCode = attributes.getResponseCode(); |
| | 213 | final int httpStatusCode = attributes.getResponseCode(); |
| 207 | 214 | if (httpStatusCode >= 400 && !isNoTileAtZoom()) { |
| 208 | 215 | status = false; |
| 209 | 216 | handleError(attributes); |
| … |
… |
|
| 246 | 253 | return; |
| 247 | 254 | } |
| 248 | 255 | if (attributes != null) { |
| 249 | | int httpStatusCode = attributes.getResponseCode(); |
| | 256 | final int httpStatusCode = attributes.getResponseCode(); |
| 250 | 257 | if (attributes.getErrorMessage() == null) { |
| 251 | 258 | tile.setError(tr("HTTP error {0} when loading tiles", httpStatusCode)); |
| 252 | 259 | } else { |
| … |
… |
|
| 273 | 280 | */ |
| 274 | 281 | @Override |
| 275 | 282 | protected String getServerKey() { |
| 276 | | TileSource ts = tile.getSource(); |
| | 283 | final TileSource ts = tile.getSource(); |
| 277 | 284 | if (ts instanceof AbstractTMSTileSource) { |
| 278 | 285 | return ((AbstractTMSTileSource) ts).getBaseUrl(); |
| 279 | 286 | } |
| … |
… |
|
| 292 | 299 | |
| 293 | 300 | @Override |
| 294 | 301 | protected CacheEntryAttributes parseHeaders(HttpClient.Response urlConn) { |
| 295 | | CacheEntryAttributes ret = super.parseHeaders(urlConn); |
| | 302 | final CacheEntryAttributes ret = super.parseHeaders(urlConn); |
| 296 | 303 | // keep the expiration time between MINIMUM_EXPIRES and MAXIMUM_EXPIRES, so we will cache the tiles |
| 297 | 304 | // at least for some short period of time, but not too long |
| 298 | | long minimumExpiryTime = TimeUnit.SECONDS.toMillis(options.getMinimumExpiryTime()); |
| 299 | | long nowPlusMin = now + Math.max(MINIMUM_EXPIRES.get(), minimumExpiryTime); |
| | 305 | final long minimumExpiryTime = TimeUnit.SECONDS.toMillis(options.getMinimumExpiryTime()); |
| | 306 | final long nowPlusMin = now + Math.max(MINIMUM_EXPIRES.get(), minimumExpiryTime); |
| 300 | 307 | if (ret.getExpirationTime() < nowPlusMin) { |
| 301 | 308 | ret.setExpirationTime(nowPlusMin); |
| 302 | 309 | } |
| 303 | | long nowPlusMax = now + Math.max(MAXIMUM_EXPIRES.get(), minimumExpiryTime); |
| | 310 | final long nowPlusMax = now + Math.max(MAXIMUM_EXPIRES.get(), minimumExpiryTime); |
| 304 | 311 | if (ret.getExpirationTime() > nowPlusMax) { |
| 305 | 312 | ret.setExpirationTime(nowPlusMax); |
| 306 | 313 | } |
| … |
… |
|
| 326 | 333 | |
| 327 | 334 | private boolean tryLoadTileImage(CacheEntry object) throws IOException { |
| 328 | 335 | if (object != null) { |
| 329 | | byte[] content = object.getContent(); |
| | 336 | final byte[] content = object.getContent(); |
| 330 | 337 | if (content.length > 0 || tile instanceof VectorTile) { |
| 331 | 338 | try (ByteArrayInputStream in = new ByteArrayInputStream(content)) { |
| 332 | 339 | tile.loadImage(in); |
| 333 | 340 | if ((!(tile instanceof VectorTile) && tile.getImage() == null) |
| 334 | 341 | || ((tile instanceof VectorTile) && !tile.isLoaded())) { |
| 335 | | String s = new String(content, StandardCharsets.UTF_8); |
| 336 | | Matcher m = SERVICE_EXCEPTION_PATTERN.matcher(s); |
| | 342 | final String s = new String(content, StandardCharsets.UTF_8); |
| | 343 | final Matcher m = SERVICE_EXCEPTION_PATTERN.matcher(s); |
| 337 | 344 | if (m.matches()) { |
| 338 | | String message = Utils.strip(m.group(1)); |
| | 345 | final String message = Utils.strip(m.group(1)); |
| 339 | 346 | tile.setError(message); |
| 340 | 347 | Logging.error(message); |
| 341 | 348 | Logging.debug(s); |
| … |
… |
|
| 354 | 361 | |
| 355 | 362 | @Override |
| 356 | 363 | public String detectErrorMessage(String data) { |
| 357 | | Matcher xml = SERVICE_EXCEPTION_PATTERN.matcher(data); |
| 358 | | Matcher json = JSON_PATTERN.matcher(data); |
| | 364 | final Matcher xml = SERVICE_EXCEPTION_PATTERN.matcher(data); |
| | 365 | final Matcher json = JSON_PATTERN.matcher(data); |
| 359 | 366 | return xml.matches() ? removeCdata(Utils.strip(xml.group(1))) |
| 360 | 367 | : json.matches() ? Utils.strip(json.group(1)) |
| 361 | 368 | : super.detectErrorMessage(data); |
| 362 | 369 | } |
| 363 | 370 | |
| 364 | 371 | private static String removeCdata(String msg) { |
| 365 | | Matcher m = CDATA_PATTERN.matcher(msg); |
| | 372 | final Matcher m = CDATA_PATTERN.matcher(msg); |
| 366 | 373 | return m.matches() ? Utils.strip(m.group(1)) : msg; |
| 367 | 374 | } |
| 368 | 375 | } |