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