1
2
3
4
5
6 package org.tailormap.api.persistence.helper;
7
8 import static org.tailormap.api.persistence.json.GeoServiceProtocol.QUANTIZEDMESH;
9 import static org.tailormap.api.persistence.json.GeoServiceProtocol.TILES3D;
10 import static org.tailormap.api.persistence.json.GeoServiceProtocol.XYZ;
11
12 import java.io.IOException;
13 import java.lang.invoke.MethodHandles;
14 import java.net.URI;
15 import java.net.URISyntaxException;
16 import java.nio.charset.StandardCharsets;
17 import java.time.Instant;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Objects;
22 import java.util.Optional;
23 import java.util.Set;
24 import java.util.function.Predicate;
25 import java.util.stream.Collectors;
26 import org.apache.commons.lang3.StringUtils;
27 import org.geotools.api.data.ServiceInfo;
28 import org.geotools.data.ows.AbstractOpenWebService;
29 import org.geotools.data.ows.Capabilities;
30 import org.geotools.data.ows.OperationType;
31 import org.geotools.http.HTTPClientFinder;
32 import org.geotools.ows.wms.Layer;
33 import org.geotools.ows.wms.WMSCapabilities;
34 import org.geotools.ows.wms.WebMapServer;
35 import org.geotools.ows.wmts.WebMapTileServer;
36 import org.geotools.ows.wmts.model.WMTSLayer;
37 import org.geotools.xml.DocumentFactory;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40 import org.springframework.beans.factory.annotation.Autowired;
41 import org.springframework.http.MediaType;
42 import org.springframework.stereotype.Service;
43 import org.springframework.web.util.UriComponentsBuilder;
44 import org.tailormap.api.configuration.TailormapConfig;
45 import org.tailormap.api.geotools.ResponseTeeingHTTPClient;
46 import org.tailormap.api.geotools.WMSServiceExceptionUtil;
47 import org.tailormap.api.persistence.GeoService;
48 import org.tailormap.api.persistence.json.GeoServiceLayer;
49 import org.tailormap.api.persistence.json.ServiceAuthentication;
50 import org.tailormap.api.persistence.json.TMServiceCapabilitiesRequest;
51 import org.tailormap.api.persistence.json.TMServiceCapabilitiesRequestGetFeatureInfo;
52 import org.tailormap.api.persistence.json.TMServiceCapabilitiesRequestGetMap;
53 import org.tailormap.api.persistence.json.TMServiceCaps;
54 import org.tailormap.api.persistence.json.TMServiceCapsCapabilities;
55 import org.tailormap.api.persistence.json.TMServiceInfo;
56 import org.tailormap.api.persistence.json.WMSStyle;
57 import org.tailormap.api.viewer.model.Service.ServerTypeEnum;
58
59 @Service
60 public class GeoServiceHelper {
61
62 private static final Logger logger =
63 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
64 private final TailormapConfig tailormapConfig;
65
66 @Autowired
67 public GeoServiceHelper(TailormapConfig tailormapConfig) {
68 this.tailormapConfig = tailormapConfig;
69 }
70
71 public static ServerTypeEnum guessServerTypeFromUrl(String url) {
72
73 if (StringUtils.isBlank(url)) {
74 return org.tailormap.api.viewer.model.Service.ServerTypeEnum.GENERIC;
75 }
76 if (url.contains("/arcgis/")) {
77 return org.tailormap.api.viewer.model.Service.ServerTypeEnum.GENERIC;
78 }
79 if (url.contains("/geoserver/")) {
80 return org.tailormap.api.viewer.model.Service.ServerTypeEnum.GEOSERVER;
81 }
82 if (url.contains("/mapserv")) {
83 return org.tailormap.api.viewer.model.Service.ServerTypeEnum.MAPSERVER;
84 }
85 return org.tailormap.api.viewer.model.Service.ServerTypeEnum.GENERIC;
86 }
87
88 public static String getWmsRequest(String uri) {
89 return getWmsRequest(uri == null ? null : URI.create(uri));
90 }
91
92
93
94
95
96
97
98 public static String getWmsRequest(URI uri) {
99 if (uri == null || uri.getQuery() == null) {
100 return null;
101 }
102 return UriComponentsBuilder.fromUri(uri).build().getQueryParams().entrySet().stream()
103 .filter(entry -> "request".equalsIgnoreCase(entry.getKey()))
104 .map(entry -> entry.getValue().getFirst())
105 .findFirst()
106 .orElse(null);
107 }
108
109 public void loadServiceCapabilities(GeoService geoService) throws Exception {
110
111 if (geoService.getProtocol() == XYZ) {
112 setXyzCapabilities(geoService);
113 return;
114 }
115
116 if (geoService.getProtocol() == TILES3D) {
117 set3DTilesCapabilities(geoService);
118 return;
119 }
120
121 if (geoService.getProtocol() == QUANTIZEDMESH) {
122 setQuantizedMeshCapabilities(geoService);
123 return;
124 }
125
126 ResponseTeeingHTTPClient client = new ResponseTeeingHTTPClient(
127 HTTPClientFinder.createClient(), null, Set.of("Access-Control-Allow-Origin"));
128
129 ServiceAuthentication auth = geoService.getAuthentication();
130 if (auth != null && auth.getMethod() == ServiceAuthentication.MethodEnum.PASSWORD) {
131 client.setUser(auth.getUsername());
132 client.setPassword(auth.getPassword());
133 }
134
135 client.setReadTimeout(this.tailormapConfig.getTimeout());
136 client.setConnectTimeout(this.tailormapConfig.getTimeout());
137 client.setTryGzip(true);
138
139 logger.info(
140 "Get capabilities for {} {} from URL {}",
141 geoService.getProtocol(),
142 geoService.getId() == null ? "(new)" : "id " + geoService.getId(),
143 geoService.getUrl());
144
145
146
147 switch (geoService.getProtocol()) {
148 case WMS -> loadWMSCapabilities(geoService, client);
149 case WMTS -> loadWMTSCapabilities(geoService, client);
150 default ->
151 throw new UnsupportedOperationException(
152 "Unsupported geo service protocol: " + geoService.getProtocol());
153 }
154
155 if (geoService.getTitle() == null) {
156 geoService.setTitle(Optional.ofNullable(geoService.getServiceCapabilities())
157 .map(TMServiceCaps::getServiceInfo)
158 .map(TMServiceInfo::getTitle)
159 .orElse(null));
160 }
161
162 if (logger.isDebugEnabled()) {
163 logger.debug("Loaded service layers: {}", geoService.getLayers());
164 } else {
165 logger.info(
166 "Loaded service layers: {}",
167 geoService.getLayers().stream()
168 .filter(Predicate.not(GeoServiceLayer::getVirtual))
169 .map(GeoServiceLayer::getName)
170 .collect(Collectors.toList()));
171 }
172 }
173
174 private static void setXyzCapabilities(GeoService geoService) {
175 geoService.setLayers(List.of(new GeoServiceLayer()
176 .id("0")
177 .root(true)
178 .name("xyz")
179 .title(geoService.getTitle())
180 .crs(Set.of(geoService.getSettings().getXyzCrs()))
181 .virtual(false)
182 .queryable(false)));
183 }
184
185 private static void set3DTilesCapabilities(GeoService geoService) {
186 geoService.setLayers(List.of(new GeoServiceLayer()
187 .id("0")
188 .root(true)
189 .name("tiles3d")
190 .title(geoService.getTitle())
191 .virtual(false)
192 .queryable(false)));
193 }
194
195 private static void setQuantizedMeshCapabilities(GeoService geoService) {
196 geoService.setLayers(List.of(new GeoServiceLayer()
197 .id("0")
198 .root(true)
199 .name("quantizedmesh")
200 .title(geoService.getTitle())
201 .virtual(false)
202 .queryable(false)));
203 }
204
205 private void setServiceInfo(
206 GeoService geoService,
207 ResponseTeeingHTTPClient client,
208 AbstractOpenWebService<? extends Capabilities, Layer> ows) {
209 geoService.setCapabilities(client.getLatestResponseCopy());
210 geoService.setCapabilitiesContentType(MediaType.APPLICATION_XML_VALUE);
211 geoService.setCapabilitiesFetched(Instant.now());
212
213 ServiceInfo info = ows.getInfo();
214
215 TMServiceCaps caps = new TMServiceCaps();
216 geoService.setServiceCapabilities(caps);
217
218 caps.setCorsAllowOrigin(client.getLatestResponse().getResponseHeader("Access-Control-Allow-Origin"));
219
220 if (info != null) {
221 if (StringUtils.isBlank(geoService.getTitle())) {
222 geoService.setTitle(info.getTitle());
223 }
224
225 caps.serviceInfo(new TMServiceInfo()
226 .keywords(info.getKeywords())
227 .description(info.getDescription())
228 .title(info.getTitle())
229 .publisher(info.getPublisher())
230 .schema(info.getSchema())
231 .source(info.getSource()));
232
233 geoService.setAdvertisedUrl(info.getSource().toString());
234 } else if (ows.getCapabilities() != null && ows.getCapabilities().getService() != null) {
235 org.geotools.data.ows.Service service = ows.getCapabilities().getService();
236
237 if (StringUtils.isBlank(geoService.getTitle())) {
238 geoService.setTitle(service.getTitle());
239 }
240 caps.setServiceInfo(new TMServiceInfo().keywords(Set.copyOf(List.of(service.getKeywordList()))));
241 }
242 }
243
244 private GeoServiceLayer toGeoServiceLayer(Layer l, List<? extends Layer> layers) {
245 return new GeoServiceLayer()
246 .id(String.valueOf(layers.indexOf(l)))
247 .name(l.getName())
248 .root(l.getParent() == null)
249 .title(l.getTitle())
250 .maxScale(Double.isNaN(l.getScaleDenominatorMax()) ? null : l.getScaleDenominatorMax())
251 .minScale(Double.isNaN(l.getScaleDenominatorMin()) ? null : l.getScaleDenominatorMin())
252 .virtual(l.getName() == null)
253 .crs(l.getSrs())
254 .latLonBoundingBox(GeoToolsHelper.boundsFromCRSEnvelope(l.getLatLonBoundingBox()))
255 .styles(l.getStyles().stream()
256 .map(gtStyle -> {
257 WMSStyle style = new WMSStyle()
258 .name(gtStyle.getName())
259 .title(Optional.ofNullable(gtStyle.getTitle())
260 .map(Objects::toString)
261 .orElse(null))
262 .abstractText(Optional.ofNullable(gtStyle.getAbstract())
263 .map(Objects::toString)
264 .orElse(null));
265 try {
266 List<?> legendUrls = gtStyle.getLegendURLs();
267
268 if (legendUrls != null && !legendUrls.isEmpty() && legendUrls.getFirst() != null) {
269 style.legendUrl(new URI((String) legendUrls.getFirst()));
270 }
271 } catch (URISyntaxException ignored) {
272
273
274 }
275 return style;
276 })
277 .collect(Collectors.toList()))
278 .queryable(l.isQueryable())
279 .abstractText(l.get_abstract())
280 .keywords(l.getKeywords() == null ? Set.of() : Set.copyOf(List.of(l.getKeywords())))
281 .children(l.getLayerChildren().stream()
282 .map(layers::indexOf)
283 .map(String::valueOf)
284 .collect(Collectors.toList()));
285 }
286
287 private void addLayerRecursive(
288 GeoService geoService, List<? extends Layer> layers, Layer layer, Set<String> parentCrs) {
289 GeoServiceLayer geoServiceLayer = toGeoServiceLayer(layer, layers);
290
291
292
293 geoServiceLayer.getCrs().removeAll(parentCrs);
294 geoService.getLayers().add(geoServiceLayer);
295 for (Layer l : layer.getLayerChildren()) {
296 addLayerRecursive(geoService, layers, l, layer.getSrs());
297 }
298 }
299
300 private void loadWMSCapabilities(GeoService geoService, ResponseTeeingHTTPClient client) throws Exception {
301 WebMapServer wms;
302 try {
303 wms = new WebMapServer(
304 new URI(geoService.getUrl()).toURL(),
305 client,
306 Map.of(
307
308 DocumentFactory.ENABLE_DTD, true));
309 } catch (ClassCastException | IllegalStateException e) {
310
311
312
313
314
315
316
317 String contentType = client.getLatestResponse().getContentType();
318 if (contentType != null && contentType.contains("text/xml")) {
319 String wmsException =
320 WMSServiceExceptionUtil.tryGetServiceExceptionMessage(client.getLatestResponseCopy());
321 throw new Exception("Error loading WMS capabilities: "
322 + (wmsException != null
323 ? wmsException
324 : new String(client.getLatestResponseCopy(), StandardCharsets.UTF_8)));
325 } else {
326 throw e;
327 }
328 } catch (IOException e) {
329
330
331
332 if (e.getMessage().contains("Server returned HTTP response code: 401 for URL:")) {
333 throw new Exception(
334 "Error loading WMS, got 401 unauthorized response (credentials may be required or invalid)");
335 } else {
336 throw e;
337 }
338 }
339
340 OperationType getMap = wms.getCapabilities().getRequest().getGetMap();
341 OperationType getFeatureInfo = wms.getCapabilities().getRequest().getGetFeatureInfo();
342
343 if (getMap == null) {
344 throw new Exception("Service does not support GetMap");
345 }
346
347 setServiceInfo(geoService, client, wms);
348
349 WMSCapabilities wmsCapabilities = wms.getCapabilities();
350
351
352
353 geoService
354 .getServiceCapabilities()
355 .capabilities(new TMServiceCapsCapabilities()
356 .version(wmsCapabilities.getVersion())
357 .updateSequence(wmsCapabilities.getUpdateSequence())
358 .abstractText(wmsCapabilities.getService().get_abstract())
359 .request(new TMServiceCapabilitiesRequest()
360 .getMap(new TMServiceCapabilitiesRequestGetMap()
361 .formats(Set.copyOf(getMap.getFormats())))
362 .getFeatureInfo(
363 getFeatureInfo == null
364 ? null
365 : new TMServiceCapabilitiesRequestGetFeatureInfo()
366 .formats(Set.copyOf(getFeatureInfo.getFormats())))
367 .describeLayer(
368 wms.getCapabilities().getRequest().getDescribeLayer() != null)));
369
370 if (logger.isDebugEnabled()) {
371 logger.debug("Loaded capabilities, service capabilities: {}", geoService.getServiceCapabilities());
372 } else {
373 logger.info(
374 "Loaded capabilities from \"{}\", title: \"{}\"",
375 geoService.getUrl(),
376 geoService.getServiceCapabilities() != null
377 && geoService.getServiceCapabilities().getServiceInfo() != null
378 ? geoService
379 .getServiceCapabilities()
380 .getServiceInfo()
381 .getTitle()
382 : "(none)");
383 }
384 geoService.setLayers(new ArrayList<>());
385 addLayerRecursive(
386 geoService,
387 wms.getCapabilities().getLayerList(),
388 wms.getCapabilities().getLayer(),
389 Set.of());
390 }
391
392 private void loadWMTSCapabilities(GeoService geoService, ResponseTeeingHTTPClient client) throws Exception {
393 WebMapTileServer wmts = new WebMapTileServer(new URI(geoService.getUrl()).toURL(), client);
394 setServiceInfo(geoService, client, wmts);
395
396
397
398 List<WMTSLayer> layers = wmts.getCapabilities().getLayerList();
399 geoService.setLayers(
400 layers.stream().map(l -> toGeoServiceLayer(l, layers)).collect(Collectors.toList()));
401 }
402
403
404
405
406
407
408
409
410
411
412
413 public static URI getLayerLegendUrlFromStyles(GeoService service, GeoServiceLayer serviceLayer) {
414 if (serviceLayer.getRoot()) {
415
416
417 return serviceLayer.getStyles().stream()
418 .findFirst()
419 .map(WMSStyle::getLegendUrl)
420 .orElse(null);
421 }
422
423 final List<WMSStyle> allOurLayersStyles = serviceLayer.getStyles();
424 if (allOurLayersStyles.size() == 1) {
425 return allOurLayersStyles.getFirst().getLegendUrl();
426 }
427
428 service.getLayers().stream()
429 .filter(layer -> !layer.equals(serviceLayer))
430 .forEach(layer -> allOurLayersStyles.removeAll(layer.getStyles()));
431
432 return allOurLayersStyles.stream()
433 .findFirst()
434 .map(WMSStyle::getLegendUrl)
435 .orElse(null);
436 }
437 }