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