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