1
2
3
4
5
6 package org.tailormap.api.persistence;
7
8 import com.fasterxml.jackson.annotation.JsonIgnore;
9 import io.hypersistence.tsid.TSID;
10 import jakarta.persistence.Basic;
11 import jakarta.persistence.Column;
12 import jakarta.persistence.Entity;
13 import jakarta.persistence.EntityListeners;
14 import jakarta.persistence.EnumType;
15 import jakarta.persistence.Enumerated;
16 import jakarta.persistence.FetchType;
17 import jakarta.persistence.Id;
18 import jakarta.persistence.PrePersist;
19 import jakarta.persistence.Transient;
20 import jakarta.persistence.Version;
21 import jakarta.validation.constraints.NotNull;
22 import java.lang.invoke.MethodHandles;
23 import java.nio.charset.StandardCharsets;
24 import java.time.Instant;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29 import java.util.Optional;
30 import org.apache.commons.lang3.StringUtils;
31 import org.hibernate.annotations.JdbcTypeCode;
32 import org.hibernate.envers.Audited;
33 import org.hibernate.envers.NotAudited;
34 import org.hibernate.type.SqlTypes;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37 import org.springframework.data.jpa.domain.support.AuditingEntityListener;
38 import org.springframework.lang.NonNull;
39 import org.springframework.util.LinkedMultiValueMap;
40 import org.springframework.util.MultiValueMap;
41 import org.springframework.web.util.UriComponentsBuilder;
42 import org.tailormap.api.persistence.helper.GeoServiceHelper;
43 import org.tailormap.api.persistence.json.AuthorizationRule;
44 import org.tailormap.api.persistence.json.GeoServiceDefaultLayerSettings;
45 import org.tailormap.api.persistence.json.GeoServiceLayer;
46 import org.tailormap.api.persistence.json.GeoServiceLayerSettings;
47 import org.tailormap.api.persistence.json.GeoServiceProtocol;
48 import org.tailormap.api.persistence.json.GeoServiceSettings;
49 import org.tailormap.api.persistence.json.ServiceAuthentication;
50 import org.tailormap.api.persistence.json.TMServiceCaps;
51 import org.tailormap.api.persistence.listener.EntityEventPublisher;
52 import org.tailormap.api.repository.FeatureSourceRepository;
53 import org.tailormap.api.util.TMStringUtils;
54 import org.tailormap.api.viewer.model.Service;
55
56 @Audited
57 @Entity
58 @EntityListeners({EntityEventPublisher.class, AuditingEntityListener.class})
59 public class GeoService extends AuditMetadata {
60 private static final Logger logger =
61 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
62
63 private static final List<String> REMOVE_PARAMS = List.of("REQUEST");
64
65 @Id
66 private String id;
67
68 @Version
69 private Long version;
70
71 @Column(columnDefinition = "text")
72 private String notes;
73
74 @NotNull @Enumerated(EnumType.STRING)
75 private GeoServiceProtocol protocol;
76
77
78
79
80
81
82
83 @NotNull @Column(length = 2048)
84 private String url;
85
86 @Transient
87 private boolean refreshCapabilities;
88
89
90
91
92
93 @JdbcTypeCode(SqlTypes.JSON)
94 @Column(columnDefinition = "jsonb")
95 private ServiceAuthentication authentication;
96
97
98
99
100
101 @NotAudited
102 @Basic(fetch = FetchType.LAZY)
103 @JsonIgnore
104 private byte[] capabilities;
105
106
107 private String capabilitiesContentType;
108
109
110 private Instant capabilitiesFetched;
111
112
113 @NotNull @Column(length = 2048)
114 private String title;
115
116
117
118
119
120 @Column(length = 2048)
121 private String advertisedUrl;
122
123 @JdbcTypeCode(SqlTypes.JSON)
124 @Column(columnDefinition = "jsonb")
125 private TMServiceCaps serviceCapabilities;
126
127 @JdbcTypeCode(SqlTypes.JSON)
128 @Column(columnDefinition = "jsonb")
129 @NotNull private List<AuthorizationRule> authorizationRules = new ArrayList<>();
130
131 @JdbcTypeCode(SqlTypes.JSON)
132 @Column(columnDefinition = "jsonb")
133 @NotNull private List<GeoServiceLayer> layers = new ArrayList<>();
134
135 private boolean published;
136
137
138
139
140
141 @JdbcTypeCode(SqlTypes.JSON)
142 @Column(columnDefinition = "jsonb")
143 @NotNull private GeoServiceSettings settings = new GeoServiceSettings();
144
145
146 public String getId() {
147 return id;
148 }
149
150 public GeoService setId(String id) {
151 this.id = id;
152 return this;
153 }
154
155 public Long getVersion() {
156 return version;
157 }
158
159 public GeoService setVersion(Long version) {
160 this.version = version;
161 return this;
162 }
163
164 public String getNotes() {
165 return notes;
166 }
167
168 public GeoService setNotes(String adminComments) {
169 this.notes = adminComments;
170 return this;
171 }
172
173 public GeoServiceProtocol getProtocol() {
174 return protocol;
175 }
176
177 public GeoService setProtocol(GeoServiceProtocol protocol) {
178 this.protocol = protocol;
179 return this;
180 }
181
182 public String getUrl() {
183 return url;
184 }
185
186
187 public GeoService setUrl(String url) {
188 this.url = sanitiseUrl(url);
189 return this;
190 }
191
192 public boolean isRefreshCapabilities() {
193 return refreshCapabilities;
194 }
195
196 public void setRefreshCapabilities(boolean refreshCapabilities) {
197 this.refreshCapabilities = refreshCapabilities;
198 }
199
200 public ServiceAuthentication getAuthentication() {
201 return authentication;
202 }
203
204 public GeoService setAuthentication(ServiceAuthentication authentication) {
205 this.authentication = authentication;
206 return this;
207 }
208
209 public byte[] getCapabilities() {
210 return capabilities;
211 }
212
213 public GeoService setCapabilities(byte[] capabilities) {
214 this.capabilities = capabilities;
215 return this;
216 }
217
218 public String getCapabilitiesContentType() {
219 return capabilitiesContentType;
220 }
221
222 public GeoService setCapabilitiesContentType(String capabilitiesContentType) {
223 this.capabilitiesContentType = capabilitiesContentType;
224 return this;
225 }
226
227 public Instant getCapabilitiesFetched() {
228 return capabilitiesFetched;
229 }
230
231 public GeoService setCapabilitiesFetched(Instant capabilitiesFetched) {
232 this.capabilitiesFetched = capabilitiesFetched;
233 return this;
234 }
235
236 public String getTitle() {
237 return title;
238 }
239
240 public GeoService setTitle(String title) {
241 this.title = title;
242 return this;
243 }
244
245 public String getAdvertisedUrl() {
246 return advertisedUrl;
247 }
248
249 public GeoService setAdvertisedUrl(String advertisedUrl) {
250 this.advertisedUrl = advertisedUrl;
251 return this;
252 }
253
254 public TMServiceCaps getServiceCapabilities() {
255 return serviceCapabilities;
256 }
257
258 public GeoService setServiceCapabilities(TMServiceCaps serviceCapabilities) {
259 this.serviceCapabilities = serviceCapabilities;
260 return this;
261 }
262
263 public List<AuthorizationRule> getAuthorizationRules() {
264 return authorizationRules;
265 }
266
267 public GeoService setAuthorizationRules(List<AuthorizationRule> authorizationRules) {
268 this.authorizationRules = authorizationRules;
269 return this;
270 }
271
272 public List<GeoServiceLayer> getLayers() {
273 return layers;
274 }
275
276 public GeoService setLayers(List<GeoServiceLayer> layers) {
277 this.layers = layers;
278 return this;
279 }
280
281 public boolean isPublished() {
282 return published;
283 }
284
285 public GeoService setPublished(boolean published) {
286 this.published = published;
287 return this;
288 }
289
290 public GeoServiceSettings getSettings() {
291 return settings;
292 }
293
294 public GeoService setSettings(GeoServiceSettings settings) {
295 this.settings = settings;
296 return this;
297 }
298
299
300
301 @PrePersist
302 public void assignId() {
303 if (StringUtils.isBlank(getId())) {
304
305
306
307 setId(TSID.fast().toString());
308 }
309 }
310
311
312 public org.tailormap.api.viewer.model.Service.ServerTypeEnum getResolvedServerType() {
313 if (settings.getServerType() == GeoServiceSettings.ServerTypeEnum.AUTO) {
314 return GeoServiceHelper.guessServerTypeFromUrl(getUrl());
315 } else {
316 return Service.ServerTypeEnum.fromValue(settings.getServerType().getValue());
317 }
318 }
319
320 public Service toJsonPojo(GeoServiceHelper geoServiceHelper) {
321 Service s = new Service()
322 .id(this.id)
323 .title(this.title)
324 .url(Boolean.TRUE.equals(this.getSettings().getUseProxy()) ? null : this.url)
325 .protocol(this.protocol)
326 .serverType(getResolvedServerType());
327
328 if (this.protocol == GeoServiceProtocol.WMTS) {
329
330
331 s.capabilities(new String(getCapabilities(), StandardCharsets.UTF_8));
332 }
333
334 return s;
335 }
336
337 public GeoServiceLayer findLayer(String name) {
338 return getLayers().stream()
339 .filter(sl -> name.equals(sl.getName()))
340 .findFirst()
341 .orElse(null);
342 }
343
344 public GeoServiceLayerSettings getLayerSettings(String layerName) {
345 return getSettings().getLayerSettings().get(layerName);
346 }
347
348 @NonNull public String getTitleWithSettingsOverrides(String layerName) {
349
350 String title = Optional.ofNullable(getLayerSettings(layerName))
351 .map(GeoServiceLayerSettings::getTitle)
352 .map(TMStringUtils::nullIfEmpty)
353 .orElse(null);
354
355
356 if (title == null) {
357 title = Optional.ofNullable(findLayer(layerName))
358 .map(GeoServiceLayer::getTitle)
359 .map(TMStringUtils::nullIfEmpty)
360 .orElse(null);
361 }
362
363
364
365
366 if (title == null) {
367 title = layerName;
368 }
369
370 return title;
371 }
372
373 public TMFeatureType findFeatureTypeForLayer(
374 GeoServiceLayer layer, FeatureSourceRepository featureSourceRepository) {
375
376 GeoServiceDefaultLayerSettings defaultLayerSettings = getSettings().getDefaultLayerSettings();
377 GeoServiceLayerSettings layerSettings = getLayerSettings(layer.getName());
378
379 Long featureSourceId = null;
380 String featureTypeName;
381
382 if (layerSettings != null && layerSettings.getFeatureType() != null) {
383 featureTypeName = Optional.ofNullable(layerSettings.getFeatureType().getFeatureTypeName())
384 .orElse(layer.getName());
385 featureSourceId = layerSettings.getFeatureType().getFeatureSourceId();
386 } else {
387 featureTypeName = layer.getName();
388 }
389
390 if (featureSourceId == null && defaultLayerSettings != null && defaultLayerSettings.getFeatureType() != null) {
391 featureSourceId = defaultLayerSettings.getFeatureType().getFeatureSourceId();
392 }
393
394 if (featureTypeName == null) {
395 return null;
396 }
397
398 TMFeatureSource tmfs = null;
399 TMFeatureType tmft = null;
400
401 if (featureSourceId == null) {
402 List<TMFeatureSource> linkedSources = featureSourceRepository.findByLinkedServiceId(getId());
403 for (TMFeatureSource linkedFs : linkedSources) {
404 tmft = linkedFs.findFeatureTypeByName(featureTypeName);
405 if (tmft != null) {
406 tmfs = linkedFs;
407 break;
408 }
409 }
410 } else {
411 tmfs = featureSourceRepository.findById(featureSourceId).orElse(null);
412 if (tmfs != null) {
413 tmft = tmfs.findFeatureTypeByName(featureTypeName);
414 }
415 }
416
417 if (tmfs == null) {
418 return null;
419 }
420
421 if (tmft == null) {
422 String[] split = featureTypeName.split(":", 2);
423 if (split.length == 2) {
424 String shortFeatureTypeName = split[1];
425 tmft = tmfs.findFeatureTypeByName(shortFeatureTypeName);
426 if (tmft != null) {
427 logger.debug(
428 "Did not find feature type with full name \"{}\", using \"{}\" of feature source {}",
429 featureTypeName,
430 shortFeatureTypeName,
431 tmfs);
432 }
433 }
434 }
435 return tmft;
436 }
437
438
439
440
441
442
443
444 private String sanitiseUrl(String url) {
445 if (url != null && url.contains("?")) {
446 MultiValueMap<String, String> sanitisedParams = new LinkedMultiValueMap<>();
447 UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(url);
448 MultiValueMap<String, String> requestParams =
449 uri.build().getQueryParams();
450 for (Map.Entry<String, List<String>> param : requestParams.entrySet()) {
451 if (!REMOVE_PARAMS.contains(param.getKey().toUpperCase(Locale.ROOT))) {
452 sanitisedParams.put(param.getKey(), param.getValue());
453 }
454 }
455
456 url = uri.replaceQueryParams(sanitisedParams).build().toUriString();
457 if (url.endsWith("?")) {
458 url = url.substring(0, url.length() - 1);
459 }
460 }
461 return url;
462 }
463
464 public GeoServiceLayer getParentLayer(String layerId) {
465 for (GeoServiceLayer layer : this.getLayers()) {
466 if (layer.getChildren().contains(layerId)) {
467 return layer;
468 }
469 }
470 return null;
471 }
472 }