View Javadoc
1   /*
2    * Copyright (C) 2023 B3Partners B.V.
3    *
4    * SPDX-License-Identifier: MIT
5    */
6   package org.tailormap.api.configuration.dev;
7   
8   import static org.tailormap.api.persistence.Configuration.HOME_PAGE;
9   import static org.tailormap.api.persistence.Configuration.PORTAL_MENU;
10  import static org.tailormap.api.persistence.json.GeoServiceProtocol.QUANTIZEDMESH;
11  import static org.tailormap.api.persistence.json.GeoServiceProtocol.TILES3D;
12  import static org.tailormap.api.persistence.json.GeoServiceProtocol.WMS;
13  import static org.tailormap.api.persistence.json.GeoServiceProtocol.WMTS;
14  import static org.tailormap.api.persistence.json.GeoServiceProtocol.XYZ;
15  import static org.tailormap.api.persistence.json.HiddenLayerFunctionalityEnum.ATTRIBUTE_LIST;
16  import static org.tailormap.api.persistence.json.HiddenLayerFunctionalityEnum.EXPORT;
17  import static org.tailormap.api.persistence.json.HiddenLayerFunctionalityEnum.FEATURE_INFO;
18  import static org.tailormap.api.security.AuthorisationService.ACCESS_TYPE_VIEW;
19  
20  import com.fasterxml.jackson.core.JsonProcessingException;
21  import com.fasterxml.jackson.databind.ObjectMapper;
22  import java.io.IOException;
23  import java.lang.invoke.MethodHandles;
24  import java.net.URI;
25  import java.nio.charset.StandardCharsets;
26  import java.sql.SQLException;
27  import java.time.OffsetDateTime;
28  import java.time.ZoneId;
29  import java.util.ArrayList;
30  import java.util.Collection;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.NoSuchElementException;
34  import java.util.Optional;
35  import java.util.Properties;
36  import java.util.Set;
37  import java.util.UUID;
38  import org.apache.solr.client.solrj.SolrServerException;
39  import org.quartz.SchedulerException;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  import org.springframework.beans.factory.annotation.Value;
43  import org.springframework.boot.SpringApplication;
44  import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
45  import org.springframework.boot.context.event.ApplicationReadyEvent;
46  import org.springframework.context.ApplicationContext;
47  import org.springframework.context.event.EventListener;
48  import org.springframework.core.io.ClassPathResource;
49  import org.springframework.jdbc.core.simple.JdbcClient;
50  import org.springframework.transaction.annotation.Transactional;
51  import org.springframework.util.PropertyPlaceholderHelper;
52  import org.tailormap.api.admin.model.TaskSchedule;
53  import org.tailormap.api.geotools.featuresources.AttachmentsHelper;
54  import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
55  import org.tailormap.api.geotools.featuresources.JDBCFeatureSourceHelper;
56  import org.tailormap.api.geotools.featuresources.WFSFeatureSourceHelper;
57  import org.tailormap.api.persistence.Application;
58  import org.tailormap.api.persistence.Catalog;
59  import org.tailormap.api.persistence.Configuration;
60  import org.tailormap.api.persistence.GeoService;
61  import org.tailormap.api.persistence.Group;
62  import org.tailormap.api.persistence.Page;
63  import org.tailormap.api.persistence.SearchIndex;
64  import org.tailormap.api.persistence.TMFeatureSource;
65  import org.tailormap.api.persistence.TMFeatureType;
66  import org.tailormap.api.persistence.Upload;
67  import org.tailormap.api.persistence.User;
68  import org.tailormap.api.persistence.helper.GeoServiceHelper;
69  import org.tailormap.api.persistence.json.AppContent;
70  import org.tailormap.api.persistence.json.AppLayerSettings;
71  import org.tailormap.api.persistence.json.AppSettings;
72  import org.tailormap.api.persistence.json.AppTreeLayerNode;
73  import org.tailormap.api.persistence.json.AppTreeLevelNode;
74  import org.tailormap.api.persistence.json.AppTreeNode;
75  import org.tailormap.api.persistence.json.AppUiSettings;
76  import org.tailormap.api.persistence.json.AttachmentAttributeType;
77  import org.tailormap.api.persistence.json.AttributeSettings;
78  import org.tailormap.api.persistence.json.AttributeValueSettings;
79  import org.tailormap.api.persistence.json.AuthorizationRule;
80  import org.tailormap.api.persistence.json.AuthorizationRuleDecision;
81  import org.tailormap.api.persistence.json.Bounds;
82  import org.tailormap.api.persistence.json.CatalogNode;
83  import org.tailormap.api.persistence.json.FeatureTypeRef;
84  import org.tailormap.api.persistence.json.FeatureTypeTemplate;
85  import org.tailormap.api.persistence.json.Filter;
86  import org.tailormap.api.persistence.json.FilterEditConfiguration;
87  import org.tailormap.api.persistence.json.FilterGroup;
88  import org.tailormap.api.persistence.json.GeoServiceDefaultLayerSettings;
89  import org.tailormap.api.persistence.json.GeoServiceLayerSettings;
90  import org.tailormap.api.persistence.json.GeoServiceSettings;
91  import org.tailormap.api.persistence.json.JDBCConnectionProperties;
92  import org.tailormap.api.persistence.json.MenuItem;
93  import org.tailormap.api.persistence.json.PageTile;
94  import org.tailormap.api.persistence.json.ServiceAuthentication;
95  import org.tailormap.api.persistence.json.TailormapObjectRef;
96  import org.tailormap.api.persistence.json.TileLayerHiDpiMode;
97  import org.tailormap.api.persistence.json.WMSStyle;
98  import org.tailormap.api.repository.ApplicationRepository;
99  import org.tailormap.api.repository.CatalogRepository;
100 import org.tailormap.api.repository.ConfigurationRepository;
101 import org.tailormap.api.repository.FeatureSourceRepository;
102 import org.tailormap.api.repository.GeoServiceRepository;
103 import org.tailormap.api.repository.GroupRepository;
104 import org.tailormap.api.repository.PageRepository;
105 import org.tailormap.api.repository.SearchIndexRepository;
106 import org.tailormap.api.repository.UploadRepository;
107 import org.tailormap.api.repository.UserRepository;
108 import org.tailormap.api.scheduling.IndexTask;
109 import org.tailormap.api.scheduling.TMJobDataMap;
110 import org.tailormap.api.scheduling.Task;
111 import org.tailormap.api.scheduling.TaskManagerService;
112 import org.tailormap.api.scheduling.TaskType;
113 import org.tailormap.api.security.InternalAdminAuthentication;
114 import org.tailormap.api.solr.SolrHelper;
115 import org.tailormap.api.solr.SolrService;
116 import org.tailormap.api.viewer.model.AppStyling;
117 import org.tailormap.api.viewer.model.Component;
118 import org.tailormap.api.viewer.model.ComponentConfig;
119 
120 /**
121  * Populates entities to add services and applications to demo functionality, support development and use in integration
122  * tests with a common set of test data. See README.md for usage details.
123  */
124 @org.springframework.context.annotation.Configuration
125 @ConditionalOnProperty(name = "tailormap-api.database.populate-testdata", havingValue = "true")
126 public class PopulateTestData {
127 
128   private static final Logger logger =
129       LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
130   private final ApplicationContext appContext;
131   private final UserRepository userRepository;
132   private final GroupRepository groupRepository;
133   private final CatalogRepository catalogRepository;
134   private final GeoServiceRepository geoServiceRepository;
135   private final GeoServiceHelper geoServiceHelper;
136   private final SolrService solrService;
137   private final TaskManagerService taskManagerService;
138   private final FeatureSourceRepository featureSourceRepository;
139   private final ApplicationRepository applicationRepository;
140   private final ConfigurationRepository configurationRepository;
141   private final SearchIndexRepository searchIndexRepository;
142   private final FeatureSourceFactoryHelper featureSourceFactoryHelper;
143   private final UploadRepository uploadRepository;
144   private final PageRepository pageRepository;
145   private final JdbcClient jdbcClient;
146 
147   @Value("${spatial.dbs.connect:false}")
148   private boolean connectToSpatialDbs;
149 
150   @Value("#{'${tailormap-api.database.populate-testdata.categories}'.split(',')}")
151   private Set<String> categories;
152 
153   @Value("${spatial.dbs.localhost:true}")
154   private boolean connectToSpatialDbsAtLocalhost;
155 
156   @Value("${tailormap-api.database.populate-testdata.admin-hashed-password}")
157   private String adminHashedPassword;
158 
159   @Value("${tailormap-api.database.populate-testdata.exit:false}")
160   private boolean exit;
161 
162   @Value("${MAP5_URL:#{null}}")
163   private String map5url;
164 
165   private final String tiles3dProxyUsername = "b3p";
166 
167   // Password is for proxying a public service for testing purposes, not really secret
168   private final String tiles3dProxyPassword = "cOxPlJFWmtndFk0eVg5VdL";
169 
170   @Value("${tailormap-api.solr-batch-size:1000}")
171   private int solrBatchSize;
172 
173   @Value("${tailormap-api.solr-geometry-validation-rule:repairBuffer0}")
174   private String solrGeometryValidationRule;
175 
176   private static final String PROVINCIE_FEATURE_TYPE_NAME = "bestuurlijkegebieden:Provinciegebied";
177 
178   public PopulateTestData(
179       ApplicationContext appContext,
180       UserRepository userRepository,
181       GroupRepository groupRepository,
182       CatalogRepository catalogRepository,
183       GeoServiceRepository geoServiceRepository,
184       GeoServiceHelper geoServiceHelper,
185       SolrService solrService,
186       TaskManagerService taskManagerService,
187       FeatureSourceRepository featureSourceRepository,
188       ApplicationRepository applicationRepository,
189       ConfigurationRepository configurationRepository,
190       FeatureSourceFactoryHelper featureSourceFactoryHelper,
191       SearchIndexRepository searchIndexRepository,
192       UploadRepository uploadRepository,
193       PageRepository pageRepository,
194       JdbcClient jdbcClient) {
195     this.appContext = appContext;
196     this.userRepository = userRepository;
197     this.groupRepository = groupRepository;
198     this.catalogRepository = catalogRepository;
199     this.geoServiceRepository = geoServiceRepository;
200     this.geoServiceHelper = geoServiceHelper;
201     this.solrService = solrService;
202     this.taskManagerService = taskManagerService;
203     this.featureSourceRepository = featureSourceRepository;
204     this.applicationRepository = applicationRepository;
205     this.configurationRepository = configurationRepository;
206     this.featureSourceFactoryHelper = featureSourceFactoryHelper;
207     this.searchIndexRepository = searchIndexRepository;
208     this.uploadRepository = uploadRepository;
209     this.pageRepository = pageRepository;
210     this.jdbcClient = jdbcClient;
211   }
212 
213   @EventListener(ApplicationReadyEvent.class)
214   @Transactional
215   public void populate() throws Exception {
216     InternalAdminAuthentication.setInSecurityContext();
217     try {
218       // Used in conjunction with tailormap-api.database.clean=true so the database has been cleaned
219       // and the latest schema re-created
220       createTestUsersAndGroups();
221       createConfigurationTestData();
222       if (categories.contains("catalog")) {
223         createCatalogTestData();
224       }
225       if (categories.contains("apps")) {
226         createAppTestData();
227       }
228       if (categories.contains("search-index")) {
229         try {
230           createSolrIndex();
231         } catch (Exception e) {
232           logger.error("Exception creating Solr Index for testdata (continuing)", e);
233         }
234       }
235       if (categories.contains("search-index")) {
236         createSearchIndexTasks();
237       }
238       if (categories.contains("pages")) {
239         createPages();
240       }
241       logger.info("Test entities created");
242       if (categories.contains("drawing")) {
243         insertTestDrawing();
244         logger.info("Test drawing created");
245         try {
246           insertDrawingStyle();
247           logger.info("Test drawing style created");
248         } catch (Exception e) {
249           logger.error("Exception creating drawing style", e);
250         }
251       }
252     } finally {
253       InternalAdminAuthentication.clearSecurityContextAuthentication();
254     }
255     if (exit) {
256       // Exit after transaction is completed - for 'mvn verify' to populate testdata before
257       // integration tests
258       new Thread(() -> {
259             try {
260               logger.info("Exiting in 10 seconds");
261               Thread.sleep(10000);
262             } catch (InterruptedException ignored) {
263               // Ignore
264             }
265             SpringApplication.exit(appContext, () -> 0);
266             System.exit(0);
267           })
268           .start();
269     }
270   }
271 
272   public void createTestUsersAndGroups() throws NoSuchElementException {
273     Group groupFoo = new Group().setName("test-foo").setDescription("Used for integration tests.");
274     groupRepository.save(groupFoo);
275 
276     Group groupBar = new Group().setName("test-bar").setDescription("Used for integration tests.");
277     groupBar.addOrUpdateAdminProperty("group-property", true, true);
278     groupBar.addOrUpdateAdminProperty("group-private-property", 999.9, false);
279     groupRepository.save(groupBar);
280 
281     Group groupBaz = new Group().setName("test-baz").setDescription("Used for integration tests.");
282     groupRepository.save(groupBaz);
283 
284     // Normal user
285     User u = new User().setUsername("user").setPassword("{noop}user").setEmail("user@example.com");
286     u.getGroups().addAll(List.of(groupFoo, groupBar, groupBaz));
287     userRepository.save(u);
288 
289     // Foo only user
290     u = new User()
291         .setUsername("foo")
292         .setPassword("{noop}foo")
293         .setEmail("foo@example.com")
294         .setName("Foo only user");
295     u.getGroups().add(groupFoo);
296     userRepository.save(u);
297 
298     u = new User()
299         .setUsername("disabled")
300         .setPassword("{noop}disabled")
301         .setEmail("disabled@example.com")
302         .setEnabled(false);
303     userRepository.save(u);
304 
305     u = new User()
306         .setUsername("expired")
307         .setPassword("{noop}expired")
308         .setEmail("expired@example.com")
309         .setValidUntil(
310             OffsetDateTime.now(ZoneId.systemDefault()).minusDays(1).toZonedDateTime());
311     userRepository.save(u);
312 
313     // Superuser with all access
314     u = new User().setUsername("tm-admin").setPassword(adminHashedPassword);
315     u.addOrUpdateAdminProperty("some-property", "some-value", true);
316     u.addOrUpdateAdminProperty("admin-property", "private-value", false);
317     u.getGroups().add(groupRepository.findById(Group.ADMIN).orElseThrow());
318     u.getGroups().add(groupBar);
319     userRepository.save(u);
320   }
321 
322   private final List<AuthorizationRule> ruleAnonymousRead = List.of(new AuthorizationRule()
323       .groupName(Group.ANONYMOUS)
324       .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW)));
325 
326   private final List<AuthorizationRule> ruleLoggedIn = List.of(new AuthorizationRule()
327       .groupName(Group.AUTHENTICATED)
328       .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW)));
329 
330   @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
331   private void createCatalogTestData() throws Exception {
332     Catalog catalog = catalogRepository.findById(Catalog.MAIN).orElseThrow();
333     CatalogNode rootCatalogNode = catalog.getNodes().getFirst();
334     CatalogNode catalogNode = new CatalogNode().id("test").title("Test services");
335     rootCatalogNode.addChildrenItem(catalogNode.getId());
336     catalog.getNodes().add(catalogNode);
337 
338     String osmAttribution = "© [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors";
339 
340     Bounds rdTileGridExtent =
341         new Bounds().minx(-285401.92).maxx(595401.92).miny(22598.08).maxy(903401.92);
342 
343     Upload legend = new Upload()
344         .setCategory(Upload.CATEGORY_LEGEND)
345         .setFilename("gemeentegebied-legend.png")
346         .setMimeType("image/png")
347         .setContent(new ClassPathResource("test/gemeentegebied-legend.png").getContentAsByteArray())
348         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
349     uploadRepository.save(legend);
350 
351     Collection<GeoService> services = List.of(
352         new GeoService()
353             .setId("demo")
354             .setProtocol(WMS)
355             .setTitle("Demo")
356             .setPublished(true)
357             .setAuthorizationRules(ruleAnonymousRead)
358             .setUrl("https://demo.tailormap.com/geoserver/geodata/ows?SERVICE=WMS"),
359         new GeoService()
360             .setId("osm")
361             .setProtocol(XYZ)
362             .setTitle("OSM")
363             .setUrl("https://tile.openstreetmap.org/{z}/{x}/{y}.png")
364             .setAuthorizationRules(ruleAnonymousRead)
365             .setSettings(new GeoServiceSettings()
366                 .xyzCrs("EPSG:3857")
367                 .layerSettings(Map.of(
368                     "xyz",
369                     new GeoServiceLayerSettings()
370                         .attribution(osmAttribution)
371                         .maxZoom(19)))),
372         // Layer settings configured later, using the same settings for this one and proxied one
373         new GeoService()
374             .setId("snapshot-geoserver")
375             .setProtocol(WMS)
376             .setTitle("Test GeoServer")
377             .setUrl("https://snapshot.tailormap.nl/geoserver/wms")
378             .setAuthorizationRules(ruleAnonymousRead)
379             .setPublished(true),
380         new GeoService()
381             .setId("filtered-snapshot-geoserver")
382             .setProtocol(WMS)
383             .setTitle("Test GeoServer (with authorization rules)")
384             .setUrl("https://snapshot.tailormap.nl/geoserver/wms")
385             .setAuthorizationRules(List.of(
386                 new AuthorizationRule()
387                     .groupName("test-foo")
388                     .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW)),
389                 new AuthorizationRule()
390                     .groupName("test-baz")
391                     .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW))))
392             .setSettings(new GeoServiceSettings()
393                 .layerSettings(Map.of(
394                     "BGT",
395                     new GeoServiceLayerSettings()
396                         .addAuthorizationRulesItem(new AuthorizationRule()
397                             .groupName("test-foo")
398                             .decisions(Map.of(
399                                 ACCESS_TYPE_VIEW, AuthorizationRuleDecision.DENY)))
400                         .addAuthorizationRulesItem(new AuthorizationRule()
401                             .groupName("test-baz")
402                             .decisions(Map.of(
403                                 ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW))))))
404             .setPublished(true),
405         new GeoService()
406             .setId("snapshot-geoserver-proxied")
407             .setProtocol(WMS)
408             .setTitle("Test GeoServer (proxied)")
409             .setUrl("https://snapshot.tailormap.nl/geoserver/wms")
410             .setAuthorizationRules(ruleAnonymousRead)
411             .setSettings(new GeoServiceSettings().useProxy(true)),
412         new GeoService()
413             .setId("openbasiskaart")
414             .setProtocol(WMTS)
415             .setTitle("Openbasiskaart")
416             .setUrl("https://www.openbasiskaart.nl/mapcache/wmts")
417             .setAuthorizationRules(ruleAnonymousRead)
418             .setSettings(new GeoServiceSettings()
419                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings().attribution(osmAttribution))
420                 .layerSettings(Map.of(
421                     "osm",
422                     new GeoServiceLayerSettings()
423                         .title("Openbasiskaart")
424                         .hiDpiDisabled(false)
425                         .hiDpiMode(TileLayerHiDpiMode.SUBSTITUTELAYERSHOWNEXTZOOMLEVEL)
426                         .hiDpiSubstituteLayer("osm-hq")))),
427         new GeoService()
428             .setId("openbasiskaart-proxied")
429             .setProtocol(WMTS)
430             .setTitle("Openbasiskaart (proxied)")
431             .setUrl("https://www.openbasiskaart.nl/mapcache/wmts")
432             .setAuthorizationRules(ruleAnonymousRead)
433             // The service actually doesn't require authentication, but also doesn't mind it
434             // Just for testing
435             .setAuthentication(new ServiceAuthentication()
436                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
437                 .username("test")
438                 .password("test"))
439             .setSettings(new GeoServiceSettings()
440                 .useProxy(true)
441                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings().attribution(osmAttribution))
442                 .layerSettings(Map.of(
443                     "osm",
444                     new GeoServiceLayerSettings()
445                         .hiDpiDisabled(false)
446                         .hiDpiMode(TileLayerHiDpiMode.SUBSTITUTELAYERSHOWNEXTZOOMLEVEL)
447                         .hiDpiSubstituteLayer("osm-hq")))),
448         new GeoService()
449             .setId("openbasiskaart-tms")
450             .setProtocol(XYZ)
451             .setTitle("Openbasiskaart (TMS)")
452             .setUrl("https://openbasiskaart.nl/mapcache/tms/1.0.0/osm@rd/{z}/{x}/{-y}.png")
453             .setAuthorizationRules(ruleAnonymousRead)
454             .setSettings(
455                 new GeoServiceSettings()
456                     .xyzCrs("EPSG:28992")
457                     .defaultLayerSettings(
458                         new GeoServiceDefaultLayerSettings().attribution(osmAttribution))
459                     .layerSettings(
460                         Map.of(
461                             "xyz",
462                             new GeoServiceLayerSettings()
463                                 .maxZoom(15)
464                                 .tileGridExtent(rdTileGridExtent)
465                                 .hiDpiDisabled(false)
466                                 .hiDpiMode(
467                                     TileLayerHiDpiMode
468                                         .SUBSTITUTELAYERTILEPIXELRATIOONLY)
469                                 .hiDpiSubstituteLayer(
470                                     "https://openbasiskaart.nl/mapcache/tms/1.0.0/osm-hq@rd-hq/{z}/{x}/{-y}.png")))),
471         new GeoService()
472             .setId("pdok-hwh-luchtfotorgb")
473             .setProtocol(WMTS)
474             .setTitle("PDOK HWH luchtfoto")
475             .setUrl("https://service.pdok.nl/hwh/luchtfotorgb/wmts/v1_0")
476             .setAuthorizationRules(ruleAnonymousRead)
477             .setPublished(true)
478             .setSettings(new GeoServiceSettings()
479                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings()
480                     .attribution("© [Beeldmateriaal.nl](https://beeldmateriaal.nl)")
481                     .hiDpiDisabled(false))
482                 .putLayerSettingsItem(
483                     "Actueel_orthoHR", new GeoServiceLayerSettings().title("Luchtfoto"))),
484         new GeoService()
485             .setId("b3p-mapproxy-luchtfoto")
486             .setProtocol(XYZ)
487             .setTitle("Luchtfoto (TMS)")
488             .setUrl("https://mapproxy.b3p.nl/tms/1.0.0/luchtfoto/EPSG28992/{z}/{x}/{-y}.jpeg")
489             .setAuthorizationRules(ruleAnonymousRead)
490             .setPublished(true)
491             .setSettings(new GeoServiceSettings()
492                 .xyzCrs("EPSG:28992")
493                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings()
494                     .attribution("© [Beeldmateriaal.nl](https://beeldmateriaal.nl)")
495                     .hiDpiDisabled(false))
496                 .layerSettings(Map.of(
497                     "xyz",
498                     new GeoServiceLayerSettings()
499                         .maxZoom(14)
500                         .tileGridExtent(rdTileGridExtent)
501                         .hiDpiMode(TileLayerHiDpiMode.SHOWNEXTZOOMLEVEL)))),
502         new GeoService()
503             .setId("at-basemap")
504             .setProtocol(WMTS)
505             .setTitle("basemap.at")
506             .setUrl("https://mapsneu.wien.gv.at/basemapneu/1.0.0/WMTSCapabilities.xml")
507             .setAuthorizationRules(ruleAnonymousRead)
508             .setPublished(true)
509             .setSettings(new GeoServiceSettings()
510                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings()
511                     .attribution("© [basemap.at](https://basemap.at)")
512                     .hiDpiDisabled(true))
513                 .layerSettings(Map.of(
514                     "geolandbasemap",
515                     new GeoServiceLayerSettings()
516                         .title("Basemap")
517                         .hiDpiDisabled(false)
518                         .hiDpiMode(TileLayerHiDpiMode.SUBSTITUTELAYERTILEPIXELRATIOONLY)
519                         .hiDpiSubstituteLayer("bmaphidpi"),
520                     "bmaporthofoto30cm",
521                     new GeoServiceLayerSettings()
522                         .title("Orthophoto")
523                         .hiDpiDisabled(false)))),
524         new GeoService()
525             .setId("pdok-kadaster-bestuurlijkegebieden")
526             .setProtocol(WMS)
527             .setUrl("https://service.pdok.nl/kadaster/brk-bestuurlijke-gebieden/wms/v1_0?service=WMS")
528             .setAuthorizationRules(ruleAnonymousRead)
529             .setSettings(new GeoServiceSettings()
530                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings()
531                     .description("This layer shows an administrative boundary."))
532                 // No attribution required: service is CC0
533                 .serverType(GeoServiceSettings.ServerTypeEnum.MAPSERVER)
534                 .useProxy(true)
535                 .putLayerSettingsItem(
536                     "Gemeentegebied",
537                     new GeoServiceLayerSettings()
538                         .legendImageId(legend.getId().toString())))
539             .setPublished(true)
540             .setTitle("PDOK Kadaster bestuurlijke gebieden"),
541         new GeoService()
542             .setId("bestuurlijkegebieden-proxied")
543             .setProtocol(WMS)
544             .setUrl("https://service.pdok.nl/kadaster/brk-bestuurlijke-gebieden/wms/v1_0?service=WMS")
545             .setAuthorizationRules(ruleAnonymousRead)
546             // The service actually doesn't require authentication, but also doesn't mind it
547             // Just for testing that proxied services with auth are not available in public
548             // apps (even when logged in), in any controllers (map, proxy, features)
549             .setAuthentication(new ServiceAuthentication()
550                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
551                 .username("test")
552                 .password("test"))
553             .setSettings(new GeoServiceSettings()
554                 // No attribution required: service is CC0
555                 .serverType(GeoServiceSettings.ServerTypeEnum.MAPSERVER)
556                 .useProxy(true))
557             .setPublished(true)
558             .setTitle("Bestuurlijke gebieden (proxied met auth)"),
559         new GeoService()
560             .setId("3dbag_utrecht")
561             .setProtocol(TILES3D)
562             .setUrl("https://3dtilesnederland.nl/tiles/1.0/implicit/nederland/344.json")
563             .setTitle("3D BAG Utrecht")
564             .setPublished(true)
565             .setAuthorizationRules(ruleAnonymousRead),
566         new GeoService()
567             .setId("ahn_terrain_model")
568             .setProtocol(QUANTIZEDMESH)
569             .setUrl(
570                 "https://api.pdok.nl/kadaster/3d-basisvoorziening/ogc/v1/collections/digitaalterreinmodel")
571             .setTitle("AHN Terrain Model")
572             .setPublished(true)
573             .setAuthorizationRules(ruleAnonymousRead),
574         new GeoService()
575             .setId("3d_basisvoorziening_gebouwen_proxy")
576             .setProtocol(TILES3D)
577             .setUrl(
578                 "https://api.pdok.nl/kadaster/3d-basisvoorziening/ogc/v1_0/collections/gebouwen/3dtiles")
579             .setTitle("3D Basisvoorziening Gebouwen Proxy")
580             .setPublished(true)
581             .setAuthorizationRules(ruleAnonymousRead)
582             .setSettings(new GeoServiceSettings().useProxy(true)),
583         new GeoService()
584             .setId("3d_utrecht_proxied_auth")
585             .setProtocol(TILES3D)
586             .setUrl("https://snapshot.tailormap.nl/tiles3d-proxy/3dtiles")
587             .setTitle("3D Utrecht Proxied with Authorization")
588             .setPublished(true)
589             .setAuthorizationRules(ruleAnonymousRead)
590             .setAuthentication(new ServiceAuthentication()
591                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
592                 .username(tiles3dProxyUsername)
593                 .password(tiles3dProxyPassword))
594             .setSettings(new GeoServiceSettings().useProxy(true))
595         // TODO MapServer WMS "https://wms.geonorge.no/skwms1/wms.adm_enheter_historisk"
596         );
597 
598     if (map5url != null) {
599       GeoServiceLayerSettings osmAttr = new GeoServiceLayerSettings().attribution(osmAttribution);
600       GeoServiceLayerSettings map5Attr = new GeoServiceLayerSettings()
601           .attribution("Kaarten: [Map5.nl](https://map5.nl), data: " + osmAttribution);
602       services = new ArrayList<>(services);
603       services.add(new GeoService()
604           .setId("map5")
605           .setProtocol(WMTS)
606           .setTitle("Map5")
607           .setUrl(map5url)
608           .setAuthorizationRules(ruleAnonymousRead)
609           .setSettings(new GeoServiceSettings()
610               .defaultLayerSettings(new GeoServiceDefaultLayerSettings().hiDpiDisabled(true))
611               .layerSettings(Map.of(
612                   "openlufo",
613                   new GeoServiceLayerSettings()
614                       .attribution("© [Beeldmateriaal.nl](https://beeldmateriaal.nl), "
615                           + osmAttribution),
616                   "luforoadslabels",
617                   osmAttr,
618                   "map5topo",
619                   new GeoServiceLayerSettings()
620                       .attribution(map5Attr.getAttribution())
621                       .hiDpiDisabled(false)
622                       .hiDpiMode(TileLayerHiDpiMode.SUBSTITUTELAYERSHOWNEXTZOOMLEVEL)
623                       .hiDpiSubstituteLayer("map5topo_hq"),
624                   "map5topo_gray",
625                   map5Attr,
626                   "map5topo_simple",
627                   map5Attr,
628                   "map5topo_simple_gray",
629                   map5Attr,
630                   "opensimpletopo",
631                   osmAttr,
632                   "opensimpletopo_gray",
633                   osmAttr,
634                   "opentopo",
635                   osmAttr,
636                   "opentopo_gray",
637                   osmAttr))));
638     }
639 
640     for (GeoService geoService : services) {
641       try {
642         geoServiceHelper.loadServiceCapabilities(geoService);
643       } catch (Exception e) {
644         logger.error(
645             "Error loading capabilities for {} service URL {}: {}, {}",
646             geoService.getProtocol().getValue(),
647             geoService.getUrl(),
648             e.getClass(),
649             e.getMessage());
650       }
651 
652       geoServiceRepository.save(geoService);
653       catalogNode.addItemsItem(new TailormapObjectRef()
654           .kind(TailormapObjectRef.KindEnum.GEO_SERVICE)
655           .id(geoService.getId()));
656     }
657 
658     CatalogNode wfsFeatureSourceCatalogNode =
659         new CatalogNode().id("wfs_feature_sources").title("WFS feature sources");
660     rootCatalogNode.addChildrenItem(wfsFeatureSourceCatalogNode.getId());
661     catalog.getNodes().add(wfsFeatureSourceCatalogNode);
662 
663     services.stream().filter(s -> s.getProtocol() == WMS).forEach(s -> {
664       geoServiceHelper.findAndSaveRelatedWFS(s);
665       List<TMFeatureSource> linkedSources = featureSourceRepository.findByLinkedServiceId(s.getId());
666       for (TMFeatureSource linkedSource : linkedSources) {
667         wfsFeatureSourceCatalogNode.addItemsItem(new TailormapObjectRef()
668             .kind(TailormapObjectRef.KindEnum.FEATURE_SOURCE)
669             .id(linkedSource.getId().toString()));
670       }
671     });
672 
673     String geodataPassword = "980f1c8A-25933b2";
674 
675     Map<String, TMFeatureSource> featureSources = Map.of(
676         "postgis",
677         new TMFeatureSource()
678             .setProtocol(TMFeatureSource.Protocol.JDBC)
679             .setTitle("PostGIS")
680             .setJdbcConnection(new JDBCConnectionProperties()
681                 .dbtype(JDBCConnectionProperties.DbtypeEnum.POSTGIS)
682                 .host(connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "postgis")
683                 .port(connectToSpatialDbsAtLocalhost ? 54322 : 5432)
684                 .database("geodata")
685                 .schema("public")
686                 .additionalProperties(Map.of("connectionOptions", "?ApplicationName=tailormap-api")))
687             .setAuthentication(new ServiceAuthentication()
688                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
689                 .username("geodata")
690                 .password(geodataPassword)),
691         "postgis_osm",
692         new TMFeatureSource()
693             .setProtocol(TMFeatureSource.Protocol.JDBC)
694             .setTitle("PostGIS OSM")
695             .setJdbcConnection(new JDBCConnectionProperties()
696                 .dbtype(JDBCConnectionProperties.DbtypeEnum.POSTGIS)
697                 .host(connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "postgis")
698                 .port(connectToSpatialDbsAtLocalhost ? 54322 : 5432)
699                 .database("geodata")
700                 .schema("osm")
701                 .additionalProperties(Map.of("connectionOptions", "?ApplicationName=tailormap-api")))
702             .setAuthentication(new ServiceAuthentication()
703                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
704                 .username("geodata")
705                 .password(geodataPassword)),
706         "oracle",
707         new TMFeatureSource()
708             .setProtocol(TMFeatureSource.Protocol.JDBC)
709             .setTitle("Oracle")
710             .setJdbcConnection(new JDBCConnectionProperties()
711                 .dbtype(JDBCConnectionProperties.DbtypeEnum.ORACLE)
712                 .host(connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "oracle")
713                 .database("/FREEPDB1")
714                 .schema("GEODATA")
715                 .additionalProperties(Map.of("connectionOptions", "?oracle.jdbc.J2EE13Compliant=true")))
716             .setAuthentication(new ServiceAuthentication()
717                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
718                 .username("geodata")
719                 .password(geodataPassword)),
720         "sqlserver",
721         new TMFeatureSource()
722             .setProtocol(TMFeatureSource.Protocol.JDBC)
723             .setTitle("MS SQL Server")
724             .setJdbcConnection(new JDBCConnectionProperties()
725                 .dbtype(JDBCConnectionProperties.DbtypeEnum.SQLSERVER)
726                 .host(connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "sqlserver")
727                 .database("geodata")
728                 .schema("dbo")
729                 .additionalProperties(Map.of("connectionOptions", ";encrypt=false")))
730             .setAuthentication(new ServiceAuthentication()
731                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
732                 .username("geodata")
733                 .password(geodataPassword)),
734         "pdok-kadaster-bestuurlijkegebieden",
735         new TMFeatureSource()
736             .setProtocol(TMFeatureSource.Protocol.WFS)
737             .setUrl(
738                 "https://service.pdok.nl/kadaster/brk-bestuurlijke-gebieden/wfs/v1_0?service=WFS&VERSION=2.0.0")
739             .setTitle("Bestuurlijke gebieden")
740             .setNotes(
741                 "Overzicht van de bestuurlijke indeling van Nederland in gemeenten en provincies alsmede de rijksgrens. Gegevens zijn afgeleid uit de Basisregistratie Kadaster (BRK)."));
742     featureSourceRepository.saveAll(featureSources.values());
743 
744     new WFSFeatureSourceHelper().loadCapabilities(featureSources.get("pdok-kadaster-bestuurlijkegebieden"));
745     geoServiceRepository.findById("pdok-kadaster-bestuurlijkegebieden").ifPresent(geoService -> {
746       geoService
747           .getSettings()
748           .getLayerSettings()
749           .put(
750               "Provinciegebied",
751               new GeoServiceLayerSettings()
752                   .description("The administrative boundary of Dutch Provinces, connected to a WFS.")
753                   .featureType(new FeatureTypeRef()
754                       .featureSourceId(featureSources
755                           .get("pdok-kadaster-bestuurlijkegebieden")
756                           .getId())
757                       .featureTypeName(PROVINCIE_FEATURE_TYPE_NAME))
758                   .title("Provinciegebied (WFS)"));
759       geoServiceRepository.save(geoService);
760     });
761 
762     geoServiceRepository.findById("bestuurlijkegebieden-proxied").ifPresent(geoService -> {
763       geoService
764           .getSettings()
765           .getLayerSettings()
766           .put(
767               "Provinciegebied",
768               new GeoServiceLayerSettings()
769                   .featureType(new FeatureTypeRef()
770                       .featureSourceId(featureSources
771                           .get("pdok-kadaster-bestuurlijkegebieden")
772                           .getId())
773                       .featureTypeName(PROVINCIE_FEATURE_TYPE_NAME))
774                   .title("Provinciegebied (WFS, proxied met auth)"));
775       geoServiceRepository.save(geoService);
776     });
777 
778     CatalogNode featureSourceCatalogNode =
779         new CatalogNode().id("feature_sources").title("Test feature sources");
780     rootCatalogNode.addChildrenItem(featureSourceCatalogNode.getId());
781     catalog.getNodes().add(featureSourceCatalogNode);
782 
783     for (TMFeatureSource featureSource : featureSources.values()) {
784       featureSourceCatalogNode.addItemsItem(new TailormapObjectRef()
785           .kind(TailormapObjectRef.KindEnum.FEATURE_SOURCE)
786           .id(featureSource.getId().toString()));
787     }
788     catalogRepository.save(catalog);
789 
790     if (connectToSpatialDbs) {
791       featureSources.values().forEach(fs -> {
792         try {
793           if (fs.getProtocol() == TMFeatureSource.Protocol.JDBC) {
794             new JDBCFeatureSourceHelper().loadCapabilities(fs);
795           } else if (fs.getProtocol() == TMFeatureSource.Protocol.WFS) {
796             new WFSFeatureSourceHelper().loadCapabilities(fs);
797           }
798         } catch (Exception e) {
799           logger.error("Error loading capabilities for feature source {}", fs.getTitle(), e);
800         }
801       });
802 
803       services.stream()
804           // Set layer settings for both the proxied and non-proxied one, but don't overwrite the
805           // authorization rules for the "filtered-snapshot-geoserver" service
806           .filter(s -> s.getId().startsWith("snapshot-geoserver"))
807           .forEach(s -> s.getSettings()
808               .layerSettings(Map.of(
809                   "postgis:begroeidterreindeel",
810                   new GeoServiceLayerSettings()
811                       .description("""
812 This layer shows data from https://www.postgis.net/
813 
814 https://postgis.net/brand.svg""")
815                       .featureType(new FeatureTypeRef()
816                           .featureSourceId(featureSources
817                               .get("postgis")
818                               .getId())
819                           .featureTypeName("begroeidterreindeel")),
820                   "postgis:bak",
821                   new GeoServiceLayerSettings()
822                       .featureType(new FeatureTypeRef()
823                           .featureSourceId(featureSources
824                               .get("postgis")
825                               .getId())
826                           .featureTypeName("bak")),
827                   "postgis:kadastraal_perceel",
828                   new GeoServiceLayerSettings()
829                       .description("cadastral parcel label points")
830                       .featureType(new FeatureTypeRef()
831                           .featureSourceId(featureSources
832                               .get("postgis")
833                               .getId())
834                           .featureTypeName("kadastraal_perceel")),
835                   "sqlserver:wegdeel",
836                   new GeoServiceLayerSettings()
837                       .attribution(
838                           "CC BY 4.0 [BGT/Kadaster](https://www.nationaalgeoregister.nl/geonetwork/srv/api/records/2cb4769c-b56e-48fa-8685-c48f61b9a319)")
839                       .description("""
840 This layer shows data from [MS SQL Server](https://learn.microsoft.com/en-us/sql/relational-databases/spatial/spatial-data-sql-server).
841 
842 https://social.technet.microsoft.com/wiki/cfs-filesystemfile.ashx/__key/communityserver-components-imagefileviewer/communityserver-wikis-components-files-00-00-00-00-05/1884.SQL_5F00_h_5F00_rgb.png_2D00_550x0.png""")
843                       .featureType(new FeatureTypeRef()
844                           .featureSourceId(featureSources
845                               .get("sqlserver")
846                               .getId())
847                           .featureTypeName("wegdeel")),
848                   "oracle:WATERDEEL",
849                   new GeoServiceLayerSettings()
850                       .description("This layer shows data from Oracle Spatial.")
851                       .featureType(new FeatureTypeRef()
852                           .featureSourceId(featureSources
853                               .get("oracle")
854                               .getId())
855                           .featureTypeName("WATERDEEL")),
856                   "postgis:osm_polygon",
857                   new GeoServiceLayerSettings()
858                       .description("This layer shows OSM data from postgis.")
859                       .featureType(new FeatureTypeRef()
860                           .featureSourceId(featureSources
861                               .get("postgis_osm")
862                               .getId())
863                           .featureTypeName("osm_polygon")))));
864     }
865 
866     featureSources.get("pdok-kadaster-bestuurlijkegebieden").getFeatureTypes().stream()
867         .filter(ft -> ft.getName().equals(PROVINCIE_FEATURE_TYPE_NAME))
868         .findFirst()
869         .ifPresent(ft -> {
870           ft.getSettings().addHideAttributesItem("identificatie");
871           ft.getSettings().addHideAttributesItem("ligtInLandCode");
872           ft.getSettings().addHideAttributesItem("fuuid");
873           ft.getSettings().putAttributeSettingsItem("naam", new AttributeSettings().title("Naam"));
874           ft.getSettings()
875               .setTemplate(new FeatureTypeTemplate()
876                   .templateLanguage("simple")
877                   .markupLanguage("markdown")
878                   .template("""
879 ### Provincie
880 Deze provincie heet **{{naam}}** en ligt in _{{ligtInLandNaam}}_.
881 
882 | Attribuut | Waarde             |
883 | --------- | ------------------ |
884 | `code`    | {{code}}           |
885 | `naam`    | {{naam}}           |
886 | `ligt in` | {{ligtInLandNaam}} |"""));
887         });
888 
889     featureSources.get("postgis").getFeatureTypes().stream()
890         .filter(ft -> ft.getName().equals("begroeidterreindeel"))
891         .findFirst()
892         .ifPresent(ft -> {
893           ft.getSettings().addHideAttributesItem("terminationdate");
894           ft.getSettings().addHideAttributesItem("geom_kruinlijn");
895           ft.getSettings().putAttributeSettingsItem("gmlid", new AttributeSettings().title("GML ID"));
896           ft.getSettings()
897               .putAttributeSettingsItem("identificatie", new AttributeSettings().title("Identificatie"));
898           ft.getSettings()
899               .putAttributeSettingsItem(
900                   "tijdstipregistratie", new AttributeSettings().title("Registratie"));
901           ft.getSettings()
902               .putAttributeSettingsItem(
903                   "eindregistratie", new AttributeSettings().title("Eind registratie"));
904           ft.getSettings().putAttributeSettingsItem("class", new AttributeSettings().title("Klasse"));
905           ft.getSettings()
906               .putAttributeSettingsItem("bronhouder", new AttributeSettings().title("Bronhouder"));
907           ft.getSettings()
908               .putAttributeSettingsItem("inonderzoek", new AttributeSettings().title("In onderzoek"));
909           ft.getSettings()
910               .putAttributeSettingsItem(
911                   "relatievehoogteligging", new AttributeSettings().title("Relatieve hoogteligging"));
912           ft.getSettings()
913               .putAttributeSettingsItem("bgt_status", new AttributeSettings().title("BGT status"));
914           ft.getSettings()
915               .putAttributeSettingsItem("plus_status", new AttributeSettings().title("Plus-status"));
916           ft.getSettings()
917               .putAttributeSettingsItem(
918                   "plus_fysiekvoorkomen", new AttributeSettings().title("Plus-fysiek voorkomen"));
919           ft.getSettings()
920               .putAttributeSettingsItem(
921                   "begroeidterreindeeloptalud", new AttributeSettings().title("Op talud"));
922           ft.getSettings().addAttributeOrderItem("identificatie");
923           ft.getSettings().addAttributeOrderItem("bronhouder");
924           ft.getSettings().addAttributeOrderItem("class");
925           ft.getSettings()
926               .addAttachmentAttributesItem(new AttachmentAttributeType()
927                   .attributeName("bijlage")
928                   .maxAttachmentSize(4_000_000L)
929                   .mimeType("image/jpeg, image/svg+xml, .png, image/*"));
930           try {
931             AttachmentsHelper.createAttachmentTableForFeatureType(ft);
932           } catch (IOException | SQLException e) {
933             throw new RuntimeException("Failed to create attachments table", e);
934           }
935           ft.getSettings().addEditableAttributesItem("identificatie");
936           ft.getSettings().addEditableAttributesItem("bronhouder");
937           ft.getSettings().addEditableAttributesItem("class");
938           ft.getSettings().addEditableAttributesItem("gmlid");
939           ft.getSettings().addEditableAttributesItem("lv_publicatiedatum");
940           ft.getSettings().addEditableAttributesItem("creationdate");
941           ft.getSettings().addEditableAttributesItem("tijdstipregistratie");
942           ft.getSettings().addEditableAttributesItem("eindregistratie");
943           ft.getSettings().addEditableAttributesItem("terminationdate");
944           ft.getSettings().addEditableAttributesItem("inonderzoek");
945           ft.getSettings().addEditableAttributesItem("relatievehoogteligging");
946           ft.getSettings().addEditableAttributesItem("bgt_status");
947           ft.getSettings().addEditableAttributesItem("plus_status");
948           ft.getSettings().addEditableAttributesItem("plus_fysiekvoorkomen");
949           ft.getSettings().addEditableAttributesItem("begroeidterreindeeloptalud");
950         });
951 
952     featureSources.get("postgis").getFeatureTypes().stream()
953         .filter(ft -> ft.getName().equals("bak"))
954         .findFirst()
955         .ifPresent(ft -> {
956           ft.getSettings().addHideAttributesItem("gmlid");
957           ft.getSettings().addHideAttributesItem("lv_publicatiedatum");
958           ft.getSettings().addHideAttributesItem("creationdate");
959           ft.getSettings().addHideAttributesItem("tijdstipregistratie");
960           ft.getSettings().addHideAttributesItem("eindregistratie");
961           ft.getSettings().addHideAttributesItem("terminationdate");
962           ft.getSettings().addHideAttributesItem("inonderzoek");
963           ft.getSettings().addHideAttributesItem("relatievehoogteligging");
964           ft.getSettings().addHideAttributesItem("bgt_status");
965           ft.getSettings().addHideAttributesItem("plus_status");
966           ft.getSettings().addHideAttributesItem("function_");
967           ft.getSettings().addHideAttributesItem("plus_type");
968         });
969 
970     featureSources.get("oracle").getFeatureTypes().stream()
971         .filter(ft -> ft.getName().equals("WATERDEEL"))
972         .findFirst()
973         .ifPresent(ft -> {
974           ft.getSettings()
975               .addAttachmentAttributesItem(new AttachmentAttributeType()
976                   .attributeName("bijlage")
977                   .maxAttachmentSize(4_000_000L)
978                   .mimeType("image/jpeg, image/svg+xml, .png, image/*"));
979           try {
980             AttachmentsHelper.createAttachmentTableForFeatureType(ft);
981           } catch (IOException | SQLException e) {
982             throw new RuntimeException("Failed to create attachments table", e);
983           }
984           ft.getSettings().addEditableAttributesItem("GMLID");
985           ft.getSettings().addEditableAttributesItem("IDENTIFICATIE");
986           ft.getSettings().addEditableAttributesItem("LV_PUBLICATIEDATUM");
987           ft.getSettings().addEditableAttributesItem("CREATIONDATE");
988           ft.getSettings().addEditableAttributesItem("TIJDSTIPREGISTRATIE");
989           ft.getSettings().addEditableAttributesItem("EINDREGISTRATIE");
990           ft.getSettings().addEditableAttributesItem("TERMINATIONDATE");
991           ft.getSettings().addEditableAttributesItem("BRONHOUDER");
992           ft.getSettings().addEditableAttributesItem("INONDERZOEK");
993           ft.getSettings().addEditableAttributesItem("RELATIEVEHOOGTELIGGING");
994           ft.getSettings().addEditableAttributesItem("BGT_STATUS");
995           ft.getSettings().addEditableAttributesItem("PLUS_STATUS");
996           ft.getSettings().addEditableAttributesItem("CLASS");
997           ft.getSettings().addEditableAttributesItem("PLUS_TYPE");
998         });
999     featureSources.get("sqlserver").getFeatureTypes().stream()
1000         .filter(ft -> ft.getName().equals("wegdeel"))
1001         .findFirst()
1002         .ifPresent(ft -> {
1003           ft.getSettings()
1004               .addAttachmentAttributesItem(new AttachmentAttributeType()
1005                   .attributeName("bijlage")
1006                   .maxAttachmentSize(4_000_000L)
1007                   .mimeType("image/jpeg, image/svg+xml, .png, image/*"));
1008           try {
1009             AttachmentsHelper.createAttachmentTableForFeatureType(ft);
1010           } catch (IOException | SQLException e) {
1011             throw new RuntimeException("Failed to create attachments table", e);
1012           }
1013           ft.getSettings().addEditableAttributesItem("gmlid");
1014           ft.getSettings().addEditableAttributesItem("identificatie");
1015           ft.getSettings().addEditableAttributesItem("lv_publicatiedatum");
1016           ft.getSettings().addEditableAttributesItem("creationdate");
1017           ft.getSettings().addEditableAttributesItem("tijdstipregistratie");
1018           ft.getSettings().addEditableAttributesItem("eindregistratie");
1019           ft.getSettings().addEditableAttributesItem("terminationdate");
1020           ft.getSettings().addEditableAttributesItem("bronhouder");
1021           ft.getSettings().addEditableAttributesItem("inonderzoek");
1022           ft.getSettings().addEditableAttributesItem("relatievehoogteligging");
1023           ft.getSettings().addEditableAttributesItem("bgt_status");
1024           ft.getSettings().addEditableAttributesItem("plus_status");
1025           ft.getSettings().addEditableAttributesItem("function_");
1026           ft.getSettings().addEditableAttributesItem("plus_functiewegdeel");
1027           ft.getSettings().addEditableAttributesItem("plus_fysiekvoorkomenwegdeel");
1028           ft.getSettings().addEditableAttributesItem("surfacematerial");
1029           ft.getSettings().addEditableAttributesItem("wegdeeloptalud");
1030         });
1031 
1032     featureSources.get("postgis").getFeatureTypes().stream()
1033         .filter(ft -> ft.getName().equals("kadastraal_perceel"))
1034         .findFirst()
1035         .ifPresent(ft -> {
1036           ft.getSettings()
1037               .addAttachmentAttributesItem(new AttachmentAttributeType()
1038                   .attributeName("bijlage")
1039                   .maxAttachmentSize(4_000_000L)
1040                   .mimeType("image/jpeg, image/svg+xml, .png, image/*"));
1041           try {
1042             AttachmentsHelper.createAttachmentTableForFeatureType(ft);
1043           } catch (IOException | SQLException e) {
1044             throw new RuntimeException("Failed to create attachments table for kadastraal_perceel", e);
1045           }
1046           // hide primary key
1047           ft.getSettings().addHideAttributesItem("gml_id");
1048           // make some attributes editable
1049           ft.getSettings().addEditableAttributesItem("begin_geldigheid");
1050           ft.getSettings().addEditableAttributesItem("tijdstip_registratie");
1051         });
1052 
1053     featureSources.get("postgis_osm").getFeatureTypes().stream()
1054         .filter(ft -> ft.getName().equals("osm_polygon"))
1055         .findFirst()
1056         .ifPresent(ft -> {
1057           ft.getSettings().addEditableAttributesItem("osm_id");
1058           ft.getSettings().addEditableAttributesItem("building");
1059           ft.getSettings().addEditableAttributesItem("z_order");
1060         });
1061   }
1062 
1063   public void createAppTestData() throws Exception {
1064     Upload logo = new Upload()
1065         .setCategory(Upload.CATEGORY_APP_LOGO)
1066         .setFilename("gradient.svg")
1067         .setMimeType("image/svg+xml")
1068         .setContent(new ClassPathResource("test/gradient-logo.svg").getContentAsByteArray())
1069         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
1070     uploadRepository.save(logo);
1071 
1072     List<AppTreeNode> baseNodes = List.of(
1073         new AppTreeLayerNode()
1074             .objectType("AppTreeLayerNode")
1075             .id("lyr:openbasiskaart:osm")
1076             .serviceId("openbasiskaart")
1077             .layerName("osm")
1078             .visible(true),
1079         new AppTreeLayerNode()
1080             .objectType("AppTreeLayerNode")
1081             .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR")
1082             .serviceId("pdok-hwh-luchtfotorgb")
1083             .layerName("Actueel_orthoHR")
1084             .visible(false));
1085 
1086     Application app = new Application()
1087         .setName("default")
1088         .setTitle("Tailormap demo")
1089         .setCrs("EPSG:28992")
1090         .setAuthorizationRules(ruleAnonymousRead)
1091         .setComponents(List.of(
1092             new Component()
1093                 .type("SIMPLE_SEARCH")
1094                 .config(new ComponentConfig()
1095                     .enabled(true)
1096                     .putAdditionalProperty("municipalities", List.of("0344"))),
1097             new Component().type("EDIT").config(new ComponentConfig().enabled(true)),
1098             new Component()
1099                 .type("TOC")
1100                 .config(new ComponentConfig()
1101                     .enabled(true)
1102                     .putAdditionalProperty("showEditLayerIcon", true)),
1103             new Component()
1104                 .type("COORDINATE_LINK_WINDOW")
1105                 .config(new ComponentConfig()
1106                     .enabled(true)
1107                     .putAdditionalProperty(
1108                         "urls",
1109                         List.of(
1110                             Map.of(
1111                                 "id",
1112                                 "google-maps",
1113                                 "url",
1114                                 "https://www.google.com/maps/@[lat],[lon],18z",
1115                                 "alias",
1116                                 "Google Maps",
1117                                 "projection",
1118                                 "EPSG:4326"),
1119                             Map.of(
1120                                 "id",
1121                                 "tm-demo",
1122                                 "url",
1123                                 "https://demo.tailormap.com/#@[X],[Y],18",
1124                                 "alias",
1125                                 "Tailormap demo",
1126                                 "projection",
1127                                 "EPSG:28992"))))))
1128         .setContentRoot(new AppContent()
1129             .addBaseLayerNodesItem(new AppTreeLevelNode()
1130                 .objectType("AppTreeLevelNode")
1131                 .id("root-base-layers")
1132                 .root(true)
1133                 .title("Base layers")
1134                 .childrenIds(List.of(
1135                     "lyr:openbasiskaart:osm",
1136                     "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR",
1137                     "lyr:openbasiskaart-proxied:osm",
1138                     "lyr:openbasiskaart-tms:xyz",
1139                     "lyr:b3p-mapproxy-luchtfoto:xyz")))
1140             .addBaseLayerNodesItem(
1141                 // This layer from a secured proxied service should not be proxyable in a
1142                 // public app, see test_wms_secured_proxy_not_in_public_app() testcase
1143                 new AppTreeLayerNode()
1144                     .objectType("AppTreeLayerNode")
1145                     .id("lyr:openbasiskaart-proxied:osm")
1146                     .serviceId("openbasiskaart-proxied")
1147                     .layerName("osm")
1148                     .visible(false))
1149             .addBaseLayerNodesItem(new AppTreeLayerNode()
1150                 .objectType("AppTreeLayerNode")
1151                 .id("lyr:openbasiskaart-tms:xyz")
1152                 .serviceId("openbasiskaart-tms")
1153                 .layerName("xyz")
1154                 .visible(false))
1155             .addBaseLayerNodesItem(new AppTreeLayerNode()
1156                 .objectType("AppTreeLayerNode")
1157                 .id("lyr:b3p-mapproxy-luchtfoto:xyz")
1158                 .serviceId("b3p-mapproxy-luchtfoto")
1159                 .layerName("xyz")
1160                 .visible(false))
1161             .addLayerNodesItem(new AppTreeLevelNode()
1162                 .objectType("AppTreeLevelNode")
1163                 .id("root")
1164                 .root(true)
1165                 .title("Layers")
1166                 .childrenIds(List.of(
1167                     "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1168                     "lyr:bestuurlijkegebieden-proxied:Provinciegebied",
1169                     "lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied",
1170                     "lyr:snapshot-geoserver:postgis:begroeidterreindeel",
1171                     "lyr:snapshot-geoserver:postgis:bak",
1172                     "lyr:snapshot-geoserver:postgis:kadastraal_perceel",
1173                     "lyr:snapshot-geoserver:sqlserver:wegdeel",
1174                     "lyr:snapshot-geoserver:oracle:WATERDEEL",
1175                     "lyr:snapshot-geoserver:BGT",
1176                     "lvl:proxied",
1177                     "lvl:osm",
1178                     "lvl:archeo")))
1179             .addLayerNodesItem(new AppTreeLayerNode()
1180                 .objectType("AppTreeLayerNode")
1181                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied")
1182                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1183                 .layerName("Provinciegebied")
1184                 .visible(true))
1185             // This is a layer from proxied service with auth that should also not be
1186             // visible, but it has a feature source attached, should also be denied for
1187             // features access and not be included in TOC
1188             .addLayerNodesItem(new AppTreeLayerNode()
1189                 .objectType("AppTreeLayerNode")
1190                 .id("lyr:bestuurlijkegebieden-proxied:Provinciegebied")
1191                 .serviceId("bestuurlijkegebieden-proxied")
1192                 .layerName("Provinciegebied")
1193                 .visible(false))
1194             .addLayerNodesItem(new AppTreeLayerNode()
1195                 .objectType("AppTreeLayerNode")
1196                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied")
1197                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1198                 .layerName("Gemeentegebied")
1199                 .visible(true))
1200             .addLayerNodesItem(new AppTreeLayerNode()
1201                 .objectType("AppTreeLayerNode")
1202                 .id("lyr:snapshot-geoserver:postgis:begroeidterreindeel")
1203                 .serviceId("snapshot-geoserver")
1204                 .layerName("postgis:begroeidterreindeel")
1205                 .visible(true))
1206             .addLayerNodesItem(new AppTreeLayerNode()
1207                 .objectType("AppTreeLayerNode")
1208                 .id("lyr:snapshot-geoserver:postgis:bak")
1209                 .serviceId("snapshot-geoserver")
1210                 .layerName("postgis:bak")
1211                 .visible(false))
1212             .addLayerNodesItem(new AppTreeLayerNode()
1213                 .objectType("AppTreeLayerNode")
1214                 .id("lyr:snapshot-geoserver:postgis:kadastraal_perceel")
1215                 .serviceId("snapshot-geoserver")
1216                 .layerName("postgis:kadastraal_perceel")
1217                 .visible(false))
1218             .addLayerNodesItem(new AppTreeLayerNode()
1219                 .objectType("AppTreeLayerNode")
1220                 .id("lyr:snapshot-geoserver:sqlserver:wegdeel")
1221                 .serviceId("snapshot-geoserver")
1222                 .layerName("sqlserver:wegdeel")
1223                 .visible(true))
1224             .addLayerNodesItem(new AppTreeLayerNode()
1225                 .objectType("AppTreeLayerNode")
1226                 .id("lyr:snapshot-geoserver:oracle:WATERDEEL")
1227                 .serviceId("snapshot-geoserver")
1228                 .layerName("oracle:WATERDEEL")
1229                 .visible(true))
1230             .addLayerNodesItem(new AppTreeLayerNode()
1231                 .objectType("AppTreeLayerNode")
1232                 .id("lyr:snapshot-geoserver:BGT")
1233                 .serviceId("snapshot-geoserver")
1234                 .layerName("BGT")
1235                 .visible(false))
1236             .addLayerNodesItem(new AppTreeLevelNode()
1237                 .objectType("AppTreeLevelNode")
1238                 .id("lvl:proxied")
1239                 .title("Proxied")
1240                 .childrenIds(List.of("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")))
1241             .addLayerNodesItem(new AppTreeLayerNode()
1242                 .objectType("AppTreeLayerNode")
1243                 .id("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")
1244                 .serviceId("snapshot-geoserver-proxied")
1245                 .layerName("postgis:begroeidterreindeel")
1246                 .visible(false))
1247             .addLayerNodesItem(new AppTreeLevelNode()
1248                 .objectType("AppTreeLevelNode")
1249                 .id("lvl:osm")
1250                 .title("OSM")
1251                 .childrenIds(List.of("lyr:snapshot-geoserver:postgis:osm_polygon")))
1252             .addLayerNodesItem(new AppTreeLayerNode()
1253                 .objectType("AppTreeLayerNode")
1254                 .id("lyr:snapshot-geoserver:postgis:osm_polygon")
1255                 .serviceId("snapshot-geoserver")
1256                 .layerName("postgis:osm_polygon")
1257                 .visible(false))
1258             .addLayerNodesItem(new AppTreeLevelNode()
1259                 .objectType("AppTreeLevelNode")
1260                 .id("lvl:archeo")
1261                 .title("Archeology")
1262                 .childrenIds(List.of("lyr:demo:geomorfologie")))
1263             .addLayerNodesItem(new AppTreeLayerNode()
1264                 .objectType("AppTreeLayerNode")
1265                 .id("lyr:demo:geomorfologie")
1266                 .serviceId("demo")
1267                 .layerName("geomorfologie")
1268                 .visible(true)))
1269         .setStyling(new AppStyling().logo(logo.getId().toString()))
1270         .setSettings(new AppSettings()
1271             .putLayerSettingsItem("lyr:openbasiskaart:osm", new AppLayerSettings().title("Openbasiskaart"))
1272             .putLayerSettingsItem(
1273                 "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR", new AppLayerSettings().title("Luchtfoto"))
1274             .putLayerSettingsItem(
1275                 "lyr:openbasiskaart-proxied:osm",
1276                 new AppLayerSettings().title("Openbasiskaart (proxied)"))
1277             .putLayerSettingsItem(
1278                 "lyr:snapshot-geoserver:oracle:WATERDEEL",
1279                 new AppLayerSettings()
1280                     .opacity(50)
1281                     .title("Waterdeel overridden title")
1282                     .editable(true)
1283                     .description("This is the layer description from the app layer setting.")
1284                     .attribution(
1285                         "CC BY 4.0 [BGT/Kadaster](https://www.nationaalgeoregister.nl/geonetwork/srv/api/records/2cb4769c-b56e-48fa-8685-c48f61b9a319)"))
1286             .putLayerSettingsItem(
1287                 "lyr:snapshot-geoserver:postgis:osm_polygon",
1288                 new AppLayerSettings()
1289                     .description("OpenStreetMap polygon data in EPSG:3857")
1290                     .opacity(60)
1291                     .editable(true)
1292                     .title("OSM Polygon (EPSG:3857)")
1293                     .attribution(
1294                         "© [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors"))
1295             .putLayerSettingsItem(
1296                 "lyr:snapshot-geoserver:postgis:begroeidterreindeel",
1297                 new AppLayerSettings()
1298                     .editable(true)
1299                     .addHideAttributesItem("begroeidterreindeeloptalud")
1300                     .addReadOnlyAttributesItem("eindregistratie")
1301                     .selectedStyles(
1302                         List.of(
1303                             new WMSStyle(
1304                                 "begroeidterreindeel",
1305                                 "Visualisatie van de begroeide terreindelen",
1306                                 "Deze stylesheet bevat de regels voor de visualisatie van het objecttype Begroeid Terreindeel",
1307                                 URI.create(
1308                                     "https://snapshot.tailormap.nl/geoserver/ows?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image%2Fpng&width=20&height=20&layer=test%3Apostgis_begroeidterreindeel")),
1309                             new WMSStyle(
1310                                 "purple_polygon",
1311                                 "purple_polygon",
1312                                 null,
1313                                 URI.create(
1314                                     "https://snapshot.tailormap.nl/geoserver/ows?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image%2Fpng&width=20&height=20&layer=test%3Apostgis_begroeidterreindeel&style=purple_polygon")))))
1315             .putLayerSettingsItem(
1316                 "lyr:snapshot-geoserver:postgis:kadastraal_perceel",
1317                 new AppLayerSettings().editable(true).addReadOnlyAttributesItem("aanduiding"))
1318             .putLayerSettingsItem(
1319                 "lyr:snapshot-geoserver:sqlserver:wegdeel", new AppLayerSettings().editable(true))
1320             .putLayerSettingsItem(
1321                 "lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel",
1322                 new AppLayerSettings().editable(false))
1323             .putLayerSettingsItem(
1324                 "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1325                 new AppLayerSettings()
1326                     .hiddenFunctionality(Set.of(FEATURE_INFO, ATTRIBUTE_LIST, EXPORT)))
1327             .addFilterGroupsItem(new FilterGroup()
1328                 .id("filtergroup1")
1329                 .source("PRESET")
1330                 .type(FilterGroup.TypeEnum.ATTRIBUTE)
1331                 .layerIds(List.of("lyr:snapshot-geoserver:postgis:begroeidterreindeel"))
1332                 .operator(FilterGroup.OperatorEnum.AND)
1333                 .addFiltersItem(new Filter()
1334                     .id("filter1")
1335                     .type(Filter.TypeEnum.ATTRIBUTE)
1336                     .condition(Filter.ConditionEnum.BEFORE)
1337                     .addValueItem("2025-06-05")
1338                     .attribute("creationdate")
1339                     .attributeType(Filter.AttributeTypeEnum.DATE))
1340                 .addFiltersItem(new Filter()
1341                     .id("filter2")
1342                     .type(Filter.TypeEnum.ATTRIBUTE)
1343                     .condition(Filter.ConditionEnum.UNIQUE_VALUES)
1344                     .addValueItem("bodembedekkers")
1345                     .addValueItem("bosplantsoen")
1346                     .addValueItem("gras- en kruidachtigen")
1347                     .attribute("plus_fysiekvoorkomen")
1348                     .attributeType(Filter.AttributeTypeEnum.STRING)
1349                     .editConfiguration(new FilterEditConfiguration()
1350                         .filterTool(FilterEditConfiguration.FilterToolEnum.CHECKBOX)
1351                         .attributeValuesSettings(List.of(
1352                             new AttributeValueSettings()
1353                                 .value("bodembedekkers")
1354                                 .initiallySelected(true)
1355                                 .selectable(true)
1356                                 .alias("Bodembedekkers"),
1357                             new AttributeValueSettings()
1358                                 .value("bosplantsoen")
1359                                 .initiallySelected(true)
1360                                 .selectable(true)
1361                                 .alias("Bosplantsoen"),
1362                             new AttributeValueSettings()
1363                                 .value("gras- en kruidachtigen")
1364                                 .initiallySelected(true)
1365                                 .selectable(true)
1366                                 .alias("Gras- en kruidachtigen"),
1367                             new AttributeValueSettings()
1368                                 .value("griend en hakhout")
1369                                 .initiallySelected(false)
1370                                 .selectable(true),
1371                             new AttributeValueSettings()
1372                                 .value("heesters")
1373                                 .initiallySelected(false)
1374                                 .selectable(true),
1375                             new AttributeValueSettings()
1376                                 .value("planten")
1377                                 .initiallySelected(false)
1378                                 .selectable(true),
1379                             new AttributeValueSettings()
1380                                 .value("struikrozen")
1381                                 .initiallySelected(false)
1382                                 .selectable(true),
1383                             new AttributeValueSettings()
1384                                 .value("waardeOnbekend")
1385                                 .initiallySelected(false)
1386                                 .selectable(true))))))
1387             .addFilterGroupsItem(new FilterGroup()
1388                 .id("filtergroup2")
1389                 .source("PRESET")
1390                 .type(FilterGroup.TypeEnum.ATTRIBUTE)
1391                 .layerIds(List.of("lyr:snapshot-geoserver:postgis:kadastraal_perceel"))
1392                 .operator(FilterGroup.OperatorEnum.AND)
1393                 .addFiltersItem(new Filter()
1394                     .id("filter3")
1395                     .type(Filter.TypeEnum.ATTRIBUTE)
1396                     .condition(Filter.ConditionEnum.u)
1397                     .addValueItem("1")
1398                     .addValueItem("12419")
1399                     .attribute("perceelnummer")
1400                     .attributeType(Filter.AttributeTypeEnum.DOUBLE)
1401                     .editConfiguration(new FilterEditConfiguration()
1402                         .filterTool(FilterEditConfiguration.FilterToolEnum.SLIDER)
1403                         .initialLowerValue(1d)
1404                         .initialUpperValue(12419d)
1405                         .minimumValue(1d)
1406                         .maximumValue(12419d)))));
1407 
1408     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1409     app.setInitialExtent(
1410         new Bounds().minx(130011d).miny(458031d).maxx(132703d).maxy(459995d));
1411     app.setMaxExtent(new Bounds().minx(-285401d).miny(22598d).maxx(595401d).maxy(903401d));
1412 
1413     if (map5url != null) {
1414       AppTreeLevelNode root =
1415           (AppTreeLevelNode) app.getContentRoot().getBaseLayerNodes().getFirst();
1416       List<String> childrenIds = new ArrayList<>(root.getChildrenIds());
1417       childrenIds.add("lyr:map5:map5topo");
1418       childrenIds.add("lyr:map5:map5topo_simple");
1419       childrenIds.add("lvl:luchtfoto-labels");
1420       root.setChildrenIds(childrenIds);
1421       app.getSettings()
1422           .putLayerSettingsItem("lyr:map5:map5topo", new AppLayerSettings().title("Map5"))
1423           .putLayerSettingsItem("lyr:map5:map5topo_simple", new AppLayerSettings().title("Map5 simple"));
1424       app.getContentRoot()
1425           .addBaseLayerNodesItem(new AppTreeLayerNode()
1426               .objectType("AppTreeLayerNode")
1427               .id("lyr:map5:map5topo")
1428               .serviceId("map5")
1429               .layerName("map5topo")
1430               .visible(false))
1431           .addBaseLayerNodesItem(new AppTreeLayerNode()
1432               .objectType("AppTreeLayerNode")
1433               .id("lyr:map5:map5topo_simple")
1434               .serviceId("map5")
1435               .layerName("map5topo_simple")
1436               .visible(false))
1437           .addBaseLayerNodesItem(new AppTreeLevelNode()
1438               .objectType("AppTreeLevelNode")
1439               .id("lvl:luchtfoto-labels")
1440               .title("Luchtfoto met labels")
1441               .addChildrenIdsItem("lyr:map5:luforoadslabels")
1442               .addChildrenIdsItem("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR2"))
1443           .addBaseLayerNodesItem(new AppTreeLayerNode()
1444               .objectType("AppTreeLayerNode")
1445               .id("lyr:map5:luforoadslabels")
1446               .serviceId("map5")
1447               .layerName("luforoadslabels")
1448               .visible(false))
1449           .addBaseLayerNodesItem(new AppTreeLayerNode()
1450               .objectType("AppTreeLayerNode")
1451               .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR2")
1452               .serviceId("pdok-hwh-luchtfotorgb")
1453               .layerName("Actueel_orthoHR")
1454               .visible(false));
1455     }
1456 
1457     applicationRepository.save(app);
1458 
1459     app = new Application()
1460         .setName("base")
1461         .setTitle("Service base app")
1462         .setCrs("EPSG:28992")
1463         .setAuthorizationRules(ruleAnonymousRead)
1464         .setContentRoot(new AppContent()
1465             .addBaseLayerNodesItem(new AppTreeLevelNode()
1466                 .objectType("AppTreeLevelNode")
1467                 .id("root-base-layers")
1468                 .root(true)
1469                 .title("Base layers")
1470                 .childrenIds(List.of(
1471                     "lyr:openbasiskaart:osm", "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR"))));
1472     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1473     applicationRepository.save(app);
1474 
1475     app = new Application()
1476         .setName("secured")
1477         .setTitle("secured app")
1478         .setCrs("EPSG:28992")
1479         .setAuthorizationRules(ruleLoggedIn)
1480         .setContentRoot(new AppContent()
1481             .addBaseLayerNodesItem(new AppTreeLevelNode()
1482                 .objectType("AppTreeLevelNode")
1483                 .id("root-base-layers")
1484                 .root(true)
1485                 .title("Base layers")
1486                 .childrenIds(List.of(
1487                     "lyr:openbasiskaart:osm",
1488                     "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR",
1489                     "lyr:openbasiskaart-proxied:osm")))
1490             .addBaseLayerNodesItem(new AppTreeLayerNode()
1491                 .objectType("AppTreeLayerNode")
1492                 .id("lyr:openbasiskaart-proxied:osm")
1493                 .serviceId("openbasiskaart-proxied")
1494                 .layerName("osm")
1495                 .visible(false))
1496             .addLayerNodesItem(new AppTreeLevelNode()
1497                 .objectType("AppTreeLevelNode")
1498                 .id("root")
1499                 .root(true)
1500                 .title("Layers")
1501                 .childrenIds(List.of(
1502                     "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1503                     "lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied",
1504                     "lvl:proxied")))
1505             .addLayerNodesItem(new AppTreeLayerNode()
1506                 .objectType("AppTreeLayerNode")
1507                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied")
1508                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1509                 .layerName("Gemeentegebied")
1510                 .visible(true))
1511             .addLayerNodesItem(new AppTreeLayerNode()
1512                 .objectType("AppTreeLayerNode")
1513                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied")
1514                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1515                 .layerName("Provinciegebied")
1516                 .visible(false))
1517             .addLayerNodesItem(new AppTreeLevelNode()
1518                 .objectType("AppTreeLevelNode")
1519                 .id("lvl:proxied")
1520                 .title("Proxied")
1521                 .childrenIds(List.of("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")))
1522             .addLayerNodesItem(new AppTreeLayerNode()
1523                 .objectType("AppTreeLayerNode")
1524                 .id("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")
1525                 .serviceId("snapshot-geoserver-proxied")
1526                 .layerName("postgis:begroeidterreindeel")
1527                 .visible(false)))
1528         .setSettings(
1529             new AppSettings()
1530                 .putLayerSettingsItem(
1531                     "lyr:openbasiskaart-proxied:osm",
1532                     new AppLayerSettings().title("Openbasiskaart (proxied)"))
1533                 .putLayerSettingsItem(
1534                     "lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel",
1535                     new AppLayerSettings()
1536                         .description("This layer should render using purple polygons")
1537                         .selectedStyles(
1538                             List.of(
1539                                 new WMSStyle(
1540                                     "purple_polygon",
1541                                     "purple_polygon",
1542                                     null,
1543                                     URI.create(
1544                                         "https://snapshot.tailormap.nl/geoserver/ows?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image%2Fpng&width=20&height=20&layer=test%3Apostgis_begroeidterreindeel&style=purple_polygon"))))));
1545 
1546     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1547     applicationRepository.save(app);
1548 
1549     app = new Application()
1550         .setName("secured-auth")
1551         .setTitle("secured (with authorizations)")
1552         .setCrs("EPSG:28992")
1553         .setAuthorizationRules(List.of(
1554             new AuthorizationRule()
1555                 .groupName("test-foo")
1556                 .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW)),
1557             new AuthorizationRule()
1558                 .groupName("test-bar")
1559                 .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW))))
1560         .setContentRoot(new AppContent()
1561             .addLayerNodesItem(new AppTreeLevelNode()
1562                 .objectType("AppTreeLevelNode")
1563                 .id("root")
1564                 .root(true)
1565                 .title("Layers")
1566                 .childrenIds(List.of("lvl:needs-auth", "lvl:public")))
1567             .addLayerNodesItem(new AppTreeLevelNode()
1568                 .objectType("AppTreeLevelNode")
1569                 .id("lvl:public")
1570                 .title("Public")
1571                 .childrenIds(List.of("lyr:snapshot-geoserver:BGT")))
1572             .addLayerNodesItem(new AppTreeLevelNode()
1573                 .objectType("AppTreeLevelNode")
1574                 .id("lvl:needs-auth")
1575                 .title("Needs auth")
1576                 .childrenIds(List.of(
1577                     "lyr:filtered-snapshot-geoserver:BGT",
1578                     "lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")))
1579             .addLayerNodesItem(new AppTreeLayerNode()
1580                 .objectType("AppTreeLayerNode")
1581                 .id("lyr:filtered-snapshot-geoserver:BGT")
1582                 .serviceId("filtered-snapshot-geoserver")
1583                 .layerName("BGT")
1584                 .visible(true))
1585             .addLayerNodesItem(new AppTreeLayerNode()
1586                 .objectType("AppTreeLayerNode")
1587                 .id("lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")
1588                 .serviceId("filtered-snapshot-geoserver")
1589                 .layerName("postgis:begroeidterreindeel")
1590                 .visible(true))
1591             .addLayerNodesItem(new AppTreeLayerNode()
1592                 .objectType("AppTreeLayerNode")
1593                 .id("lyr:snapshot-geoserver:BGT")
1594                 .serviceId("snapshot-geoserver")
1595                 .layerName("BGT")
1596                 .visible(true)));
1597 
1598     applicationRepository.save(app);
1599 
1600     app = new Application()
1601         .setName("austria")
1602         .setCrs("EPSG:3857")
1603         .setAuthorizationRules(ruleAnonymousRead)
1604         .setTitle("Austria")
1605         .setInitialExtent(
1606             new Bounds().minx(987982d).miny(5799551d).maxx(1963423d).maxy(6320708d))
1607         .setMaxExtent(
1608             new Bounds().minx(206516d).miny(5095461d).maxx(3146930d).maxy(7096232d))
1609         .setContentRoot(new AppContent()
1610             .addBaseLayerNodesItem(new AppTreeLevelNode()
1611                 .objectType("AppTreeLevelNode")
1612                 .id("root-base-layers")
1613                 .root(true)
1614                 .title("Base layers")
1615                 .childrenIds(List.of(
1616                     "lyr:at-basemap:geolandbasemap",
1617                     "lyr:at-basemap:orthofoto",
1618                     "lvl:orthofoto-labels",
1619                     "lyr:osm:xyz")))
1620             .addBaseLayerNodesItem(new AppTreeLayerNode()
1621                 .objectType("AppTreeLayerNode")
1622                 .id("lyr:at-basemap:geolandbasemap")
1623                 .serviceId("at-basemap")
1624                 .layerName("geolandbasemap")
1625                 .visible(true))
1626             .addBaseLayerNodesItem(new AppTreeLayerNode()
1627                 .objectType("AppTreeLayerNode")
1628                 .id("lyr:at-basemap:orthofoto")
1629                 .serviceId("at-basemap")
1630                 .layerName("bmaporthofoto30cm")
1631                 .visible(false))
1632             .addBaseLayerNodesItem(new AppTreeLevelNode()
1633                 .objectType("AppTreeLevelNode")
1634                 .id("lvl:orthofoto-labels")
1635                 .title("Orthophoto with labels")
1636                 .childrenIds(List.of("lyr:at-basemap:bmapoverlay", "lyr:at-basemap:orthofoto_2")))
1637             .addBaseLayerNodesItem(new AppTreeLayerNode()
1638                 .objectType("AppTreeLayerNode")
1639                 .id("lyr:at-basemap:bmapoverlay")
1640                 .serviceId("at-basemap")
1641                 .layerName("bmapoverlay")
1642                 .visible(false))
1643             .addBaseLayerNodesItem(new AppTreeLayerNode()
1644                 .objectType("AppTreeLayerNode")
1645                 .id("lyr:at-basemap:orthofoto_2")
1646                 .serviceId("at-basemap")
1647                 .layerName("bmaporthofoto30cm")
1648                 .visible(false))
1649             .addBaseLayerNodesItem(new AppTreeLayerNode()
1650                 .objectType("AppTreeLayerNode")
1651                 .id("lyr:osm:xyz")
1652                 .serviceId("osm")
1653                 .layerName("xyz")
1654                 .visible(false)));
1655 
1656     applicationRepository.save(app);
1657 
1658     app = new Application()
1659         .setName("3d_utrecht")
1660         .setCrs("EPSG:3857")
1661         .setAuthorizationRules(ruleLoggedIn)
1662         .setTitle("3D Utrecht")
1663         .setInitialExtent(
1664             new Bounds().minx(558390d).miny(6818485d).maxx(566751d).maxy(6824036d))
1665         .setMaxExtent(
1666             new Bounds().minx(91467d).miny(6496479d).maxx(1037043d).maxy(7147453d))
1667         .setSettings(new AppSettings().uiSettings(new AppUiSettings().enable3D(true)))
1668         .setContentRoot(new AppContent()
1669             .addBaseLayerNodesItem(new AppTreeLevelNode()
1670                 .objectType("AppTreeLevelNode")
1671                 .id("root-base-layers")
1672                 .root(true)
1673                 .title("Base layers")
1674                 .childrenIds(List.of("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR", "lyr:osm:xyz")))
1675             .addBaseLayerNodesItem(new AppTreeLayerNode()
1676                 .objectType("AppTreeLayerNode")
1677                 .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR")
1678                 .serviceId("pdok-hwh-luchtfotorgb")
1679                 .layerName("Actueel_orthoHR")
1680                 .visible(true))
1681             .addBaseLayerNodesItem(new AppTreeLayerNode()
1682                 .objectType("AppTreeLayerNode")
1683                 .id("lyr:osm:xyz")
1684                 .serviceId("osm")
1685                 .layerName("xyz")
1686                 .visible(false))
1687             .addLayerNodesItem(new AppTreeLevelNode()
1688                 .objectType("AppTreeLevelNode")
1689                 .id("root")
1690                 .root(true)
1691                 .title("Layers")
1692                 .childrenIds(List.of(
1693                     "lyr:3dbag_utrecht:tiles3d",
1694                     "lyr:snapshot-geoserver:postgis:begroeidterreindeel",
1695                     "lyr:3d_basisvoorziening_gebouwen_proxy:tiles3d",
1696                     "lyr:3d_utrecht_proxied_auth:tiles3d")))
1697             .addLayerNodesItem(new AppTreeLayerNode()
1698                 .objectType("AppTreeLayerNode")
1699                 .id("lyr:3dbag_utrecht:tiles3d")
1700                 .serviceId("3dbag_utrecht")
1701                 .layerName("tiles3d")
1702                 .visible(true))
1703             .addLayerNodesItem(new AppTreeLayerNode()
1704                 .objectType("AppTreeLayerNode")
1705                 .id("lyr:3d_basisvoorziening_gebouwen_proxy:tiles3d")
1706                 .serviceId("3d_basisvoorziening_gebouwen_proxy")
1707                 .layerName("tiles3d")
1708                 .visible(false))
1709             .addLayerNodesItem(new AppTreeLayerNode()
1710                 .objectType("AppTreeLayerNode")
1711                 .id("lyr:3d_utrecht_proxied_auth:tiles3d")
1712                 .serviceId("3d_utrecht_proxied_auth")
1713                 .layerName("tiles3d")
1714                 .visible(false))
1715             .addLayerNodesItem(new AppTreeLayerNode()
1716                 .objectType("AppTreeLayerNode")
1717                 .id("lyr:snapshot-geoserver:postgis:begroeidterreindeel")
1718                 .serviceId("snapshot-geoserver")
1719                 .layerName("postgis:begroeidterreindeel")
1720                 .visible(true))
1721             .addTerrainLayerNodesItem(new AppTreeLevelNode()
1722                 .objectType("AppTreeLevelNode")
1723                 .id("root-terrain-layers")
1724                 .root(true)
1725                 .title("Terrain Layers")
1726                 .childrenIds(List.of("lyr:ahn_terrain_model:quantizedmesh")))
1727             .addTerrainLayerNodesItem(new AppTreeLayerNode()
1728                 .objectType("AppTreeLayerNode")
1729                 .id("lyr:ahn_terrain_model:quantizedmesh")
1730                 .serviceId("ahn_terrain_model")
1731                 .layerName("quantizedmesh")
1732                 .visible(false)));
1733 
1734     applicationRepository.save(app);
1735 
1736     app = new Application()
1737         .setName("public-with-auth")
1738         .setTitle("Public app with one restricted layer in a group")
1739         .setCrs("EPSG:28992")
1740         .setAuthorizationRules(ruleAnonymousRead)
1741         .setInitialExtent(
1742             new Bounds().minx(130011d).miny(458031d).maxx(132703d).maxy(459995d))
1743         .setMaxExtent(
1744             new Bounds().minx(-285401d).miny(22598d).maxx(595401d).maxy(903401d))
1745         .setStyling(new AppStyling().logo(logo.getId().toString()))
1746         .setContentRoot(new AppContent()
1747             .addBaseLayerNodesItem(new AppTreeLevelNode()
1748                 .objectType("AppTreeLevelNode")
1749                 .id("root")
1750                 .root(true)
1751                 .title("Basemaps")
1752                 .childrenIds(List.of("lyr:openbasiskaart:osm")))
1753             .addBaseLayerNodesItem(new AppTreeLayerNode()
1754                 .objectType("AppTreeLayerNode")
1755                 .id("lyr:openbasiskaart:osm")
1756                 .serviceId("openbasiskaart")
1757                 .layerName("osm")
1758                 .visible(true))
1759             .addLayerNodesItem(new AppTreeLevelNode()
1760                 .objectType("AppTreeLevelNode")
1761                 .id("root")
1762                 .root(true)
1763                 .title("Application layers")
1764                 .childrenIds(List.of(
1765                     "lyr:snapshot-geoserver:postgis:kadastraal_perceel", "xpfhl34VmghkU12nP9Jer")))
1766             .addLayerNodesItem(new AppTreeLayerNode()
1767                 .objectType("AppTreeLayerNode")
1768                 .id("lyr:snapshot-geoserver:postgis:kadastraal_perceel")
1769                 .serviceId("snapshot-geoserver")
1770                 .layerName("postgis:kadastraal_perceel")
1771                 .visible(true))
1772             .addLayerNodesItem(new AppTreeLevelNode()
1773                 .id("xpfhl34VmghkU12nP9Jer")
1774                 .root(false)
1775                 .title("restricted")
1776                 .objectType("AppTreeLevelNode")
1777                 .childrenIds(List.of("lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")))
1778             .addLayerNodesItem(new AppTreeLayerNode()
1779                 .objectType("AppTreeLayerNode")
1780                 .id("lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")
1781                 .visible(true)
1782                 .serviceId("filtered-snapshot-geoserver")
1783                 .layerName("postgis:begroeidterreindeel")))
1784         .setSettings(new AppSettings());
1785 
1786     applicationRepository.save(app);
1787 
1788     Configuration config = new Configuration();
1789     config.setKey(Configuration.DEFAULT_APP);
1790     config.setValue("default");
1791     configurationRepository.save(config);
1792     config = new Configuration();
1793     config.setKey(Configuration.DEFAULT_BASE_APP);
1794     config.setValue("base");
1795     configurationRepository.save(config);
1796   }
1797 
1798   private void createConfigurationTestData() throws JsonProcessingException {
1799     Configuration config = new Configuration();
1800     config.setKey("test");
1801     config.setAvailableForViewer(true);
1802     config.setValue("test value");
1803     config.setJsonValue(new ObjectMapper().readTree("{ \"someProperty\": 1, \"nestedObject\": { \"num\": 42 } }"));
1804     configurationRepository.save(config);
1805   }
1806 
1807   @Transactional
1808   public void createSolrIndex() throws Exception {
1809     if (connectToSpatialDbs) {
1810       // flush() the repo because we need to make sure feature type testdata is fully stored
1811       // before creating the Solr index (which requires access to the feature type settings)
1812       featureSourceRepository.flush();
1813 
1814       logger.info("Creating Solr index");
1815       @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
1816       final String solrUrl = "http://" + (connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "solr") + ":8983/solr/";
1817       this.solrService.setSolrUrl(solrUrl);
1818       SolrHelper solrHelper = new SolrHelper(this.solrService.getSolrClientForIndexing())
1819           .withBatchSize(solrBatchSize)
1820           .withGeometryValidationRule(solrGeometryValidationRule);
1821       GeoService geoService =
1822           geoServiceRepository.findById("snapshot-geoserver").orElseThrow();
1823       Application defaultApp = applicationRepository.findByName("default");
1824 
1825       TMFeatureType begroeidterreindeelFT = geoService.findFeatureTypeForLayer(
1826           geoService.findLayer("postgis:begroeidterreindeel"), featureSourceRepository);
1827 
1828       TMFeatureType wegdeelFT = geoService.findFeatureTypeForLayer(
1829           geoService.findLayer("sqlserver:wegdeel"), featureSourceRepository);
1830 
1831       TMFeatureType kadastraalPerceelFT = geoService.findFeatureTypeForLayer(
1832           geoService.findLayer("postgis:kadastraal_perceel"), featureSourceRepository);
1833 
1834       try (solrHelper) {
1835         SearchIndex begroeidterreindeelIndex = null;
1836         if (begroeidterreindeelFT != null) {
1837           begroeidterreindeelIndex = new SearchIndex()
1838               .setName("Begroeidterreindeel")
1839               .setFeatureTypeId(begroeidterreindeelFT.getId())
1840               .setSearchFieldsUsed(List.of("class", "plus_fysiekvoorkomen", "bronhouder"))
1841               .setSearchDisplayFieldsUsed(List.of("class", "plus_fysiekvoorkomen"));
1842           begroeidterreindeelIndex = searchIndexRepository.save(begroeidterreindeelIndex);
1843           begroeidterreindeelIndex = solrHelper.addFeatureTypeIndex(
1844               begroeidterreindeelIndex,
1845               begroeidterreindeelFT,
1846               featureSourceFactoryHelper,
1847               searchIndexRepository);
1848           begroeidterreindeelIndex = searchIndexRepository.save(begroeidterreindeelIndex);
1849         }
1850 
1851         SearchIndex kadastraalPerceelIndex = null;
1852         if (kadastraalPerceelFT != null) {
1853           kadastraalPerceelIndex = new SearchIndex()
1854               .setName("kadastraal_perceel")
1855               .setFeatureTypeId(kadastraalPerceelFT.getId())
1856               .setSearchFieldsUsed(List.of("aanduiding"))
1857               .setSearchDisplayFieldsUsed(List.of("aanduiding"));
1858           kadastraalPerceelIndex = searchIndexRepository.save(kadastraalPerceelIndex);
1859           kadastraalPerceelIndex = solrHelper.addFeatureTypeIndex(
1860               kadastraalPerceelIndex,
1861               kadastraalPerceelFT,
1862               featureSourceFactoryHelper,
1863               searchIndexRepository);
1864           kadastraalPerceelIndex = searchIndexRepository.save(kadastraalPerceelIndex);
1865         }
1866 
1867         SearchIndex wegdeelIndex = null;
1868         if (wegdeelFT != null) {
1869           wegdeelIndex = new SearchIndex()
1870               .setName("Wegdeel")
1871               .setFeatureTypeId(wegdeelFT.getId())
1872               .setSearchFieldsUsed(List.of(
1873                   "function_", "plus_fysiekvoorkomenwegdeel", "surfacematerial", "bronhouder"))
1874               .setSearchDisplayFieldsUsed(List.of("function_", "plus_fysiekvoorkomenwegdeel"));
1875           wegdeelIndex = searchIndexRepository.save(wegdeelIndex);
1876           wegdeelIndex = solrHelper.addFeatureTypeIndex(
1877               wegdeelIndex, wegdeelFT, featureSourceFactoryHelper, searchIndexRepository);
1878           wegdeelIndex = searchIndexRepository.save(wegdeelIndex);
1879         }
1880 
1881         featureSourceRepository
1882             .getByTitle("PostGIS")
1883             .flatMap(fs -> fs.getFeatureTypes().stream()
1884                 .filter(ft -> ft.getName().equals("bak"))
1885                 .findFirst())
1886             .ifPresent(ft -> {
1887               SearchIndex bak = new SearchIndex()
1888                   .setName("bak")
1889                   .setFeatureTypeId(ft.getId())
1890                   .setSearchFieldsUsed(List.of("gmlid", "identificatie", "plus_type"))
1891                   .setSearchDisplayFieldsUsed(List.of("gmlid", "plus_type", "bronhouder"));
1892               searchIndexRepository.save(bak);
1893               try {
1894                 bak = solrHelper.addFeatureTypeIndex(
1895                     bak, ft, featureSourceFactoryHelper, searchIndexRepository);
1896                 searchIndexRepository.save(bak);
1897               } catch (IOException | SolrServerException e) {
1898                 throw new RuntimeException(e);
1899               }
1900             });
1901 
1902         // creating a solr index of config this will/should fail because there is no primary key in
1903         // the FT
1904         featureSourceRepository
1905             .getByTitle("PostGIS OSM")
1906             .flatMap(fs -> fs.getFeatureTypes().stream()
1907                 .filter(ft -> ft.getName().equals("osm_roads"))
1908                 .findFirst())
1909             .ifPresent(ft -> {
1910               SearchIndex osm_no_pk = new SearchIndex()
1911                   .setName("osm_no_pk")
1912                   .setFeatureTypeId(ft.getId())
1913                   .setSearchFieldsUsed(List.of("landuse", "osm_id", "natural", "boundary"))
1914                   .setSearchDisplayFieldsUsed(
1915                       List.of("landuse", "osm_id", "natural", "amenity", "boundary"));
1916               searchIndexRepository.save(osm_no_pk);
1917             });
1918 
1919         AppTreeLayerNode begroeidTerreindeelLayerNode = defaultApp
1920             .getAllAppTreeLayerNode()
1921             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:postgis:begroeidterreindeel"))
1922             .findFirst()
1923             .orElse(null);
1924 
1925         if (begroeidTerreindeelLayerNode != null && begroeidterreindeelIndex != null) {
1926           defaultApp
1927               .getAppLayerSettings(begroeidTerreindeelLayerNode)
1928               .setSearchIndexId(begroeidterreindeelIndex.getId());
1929         }
1930 
1931         AppTreeLayerNode kadastraalPerceelLayerNode = defaultApp
1932             .getAllAppTreeLayerNode()
1933             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:postgis:kadastraal_perceel"))
1934             .findFirst()
1935             .orElse(null);
1936 
1937         if (kadastraalPerceelLayerNode != null && kadastraalPerceelIndex != null) {
1938           defaultApp
1939               .getAppLayerSettings(kadastraalPerceelLayerNode)
1940               .setSearchIndexId(kadastraalPerceelIndex.getId());
1941         }
1942 
1943         AppTreeLayerNode wegdeel = defaultApp
1944             .getAllAppTreeLayerNode()
1945             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:sqlserver:wegdeel"))
1946             .findFirst()
1947             .orElse(null);
1948 
1949         if (wegdeel != null && wegdeelIndex != null) {
1950           defaultApp.getAppLayerSettings(wegdeel).setSearchIndexId(wegdeelIndex.getId());
1951         }
1952 
1953         applicationRepository.save(defaultApp);
1954       }
1955     }
1956   }
1957 
1958   private void createSearchIndexTasks() {
1959     logger.info("Creating search index tasks");
1960     List.of("Begroeidterreindeel", "kadastraal_perceel")
1961         .forEach(name -> searchIndexRepository.findByName(name).ifPresent(index -> {
1962           index.setSchedule(new TaskSchedule()
1963               /* hour */
1964               .cronExpression("0 0 0/1 1/1 * ? *")
1965               // /* 15 min */
1966               // .cronExpression("0 0/15 * 1/1 * ? *")
1967               .description("Update Solr index \"" + name + "\" every hour"));
1968           try {
1969             final UUID uuid = taskManagerService.createTask(
1970                 IndexTask.class,
1971                 new TMJobDataMap(Map.of(
1972                     Task.TYPE_KEY,
1973                     TaskType.INDEX,
1974                     Task.DESCRIPTION_KEY,
1975                     index.getSchedule().getDescription(),
1976                     IndexTask.INDEX_KEY,
1977                     index.getId().toString(),
1978                     Task.PRIORITY_KEY,
1979                     10)),
1980                 index.getSchedule().getCronExpression());
1981 
1982             index.getSchedule().setUuid(uuid);
1983             searchIndexRepository.save(index);
1984 
1985             logger.info("Created task to update Solr index with key: {}", uuid);
1986           } catch (SchedulerException e) {
1987             logger.error("Error creating scheduled solr index task", e);
1988           }
1989         }));
1990   }
1991 
1992   private void createPages() throws IOException {
1993     Upload logo = new Upload()
1994         .setCategory(Upload.CATEGORY_PORTAL_IMAGE)
1995         .setFilename("gradient.svg")
1996         .setMimeType("image/svg+xml")
1997         .setContent(new ClassPathResource("test/gradient-logo.svg").getContentAsByteArray())
1998         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
1999     uploadRepository.save(logo);
2000 
2001     Page loggedIn = new Page();
2002     loggedIn.setAuthorizationRules(ruleLoggedIn);
2003     loggedIn.setName("loggedIn");
2004     loggedIn.setType("page");
2005     loggedIn.setContent("About Tailormap");
2006     loggedIn.setContent("""
2007 # About Tailormap
2008 This is a page for logged in users.
2009 """);
2010     pageRepository.save(loggedIn);
2011 
2012     Page about = new Page();
2013     about.setAuthorizationRules(ruleAnonymousRead);
2014     about.setName("about");
2015     about.setType("page");
2016     about.setContent("About Tailormap");
2017     about.setContent("""
2018 # About Tailormap
2019 
2020 This is a page about *Tailormap*. It doesn't say much yet.
2021 """);
2022     pageRepository.save(about);
2023 
2024     Page page = new Page();
2025     page.setAuthorizationRules(ruleAnonymousRead);
2026     page.setName("home");
2027     page.setType("page");
2028     page.setTitle("Tailormap - Home");
2029     page.setContent("""
2030 # Welcome to Tailormap!
2031 
2032 This page is only visible when you implement a frontend to display pages, or get it (including a simple CMS)
2033 from [B3Partners](https://www.b3partners.nl)!
2034 """);
2035     page.setClassName(null);
2036     page.setTiles(List.of(
2037         new PageTile()
2038             .authorizationRules(ruleAnonymousRead)
2039             .id(UUID.randomUUID().toString())
2040             .title("Default app")
2041             .tileType(PageTile.TileTypeEnum.APPLICATION)
2042             .applicationId(Optional.ofNullable(applicationRepository.findByName("default"))
2043                 .map(Application::getId)
2044                 .orElse(null))
2045             .image(logo.getId().toString())
2046             .content("*Default app* tile content")
2047             .filterRequireAuthorization(false)
2048             .openInNewWindow(false),
2049         new PageTile()
2050             .authorizationRules(ruleAnonymousRead)
2051             .id(UUID.randomUUID().toString())
2052             .title("Secured app")
2053             .tileType(PageTile.TileTypeEnum.APPLICATION)
2054             .applicationId(Optional.ofNullable(applicationRepository.findByName("secured"))
2055                 .map(Application::getId)
2056                 .orElse(null))
2057             .filterRequireAuthorization(true)
2058             .content("Secure app, only shown if user has authorization")
2059             .openInNewWindow(false),
2060         new PageTile()
2061             .authorizationRules(ruleAnonymousRead)
2062             .id(UUID.randomUUID().toString())
2063             .title("Secured app (unfiltered)")
2064             .tileType(PageTile.TileTypeEnum.APPLICATION)
2065             .applicationId(Optional.ofNullable(applicationRepository.findByName("secured"))
2066                 .map(Application::getId)
2067                 .orElse(null))
2068             .filterRequireAuthorization(false)
2069             .content("Secure app, tile shown to everyone")
2070             .openInNewWindow(false),
2071         new PageTile()
2072             .authorizationRules(ruleAnonymousRead)
2073             .id(UUID.randomUUID().toString())
2074             .title("About")
2075             .tileType(PageTile.TileTypeEnum.PAGE)
2076             .pageId(about.getId())
2077             .openInNewWindow(false),
2078         new PageTile()
2079             .authorizationRules(ruleAnonymousRead)
2080             .id(UUID.randomUUID().toString())
2081             .title("B3Partners")
2082             .tileType(PageTile.TileTypeEnum.URL)
2083             .url("https://www.b3partners.nl/")
2084             .openInNewWindow(true),
2085         new PageTile()
2086             .authorizationRules(ruleLoggedIn)
2087             .id(UUID.randomUUID().toString())
2088             .title("Github repository")
2089             .tileType(PageTile.TileTypeEnum.URL)
2090             .url("https://github.com/Tailormap/tailormap-viewer")
2091             .openInNewWindow(true),
2092         new PageTile()
2093             .authorizationRules(ruleAnonymousRead)
2094             .id(UUID.randomUUID().toString())
2095             .tileType(PageTile.TileTypeEnum.PAGE)
2096             .title("Secured page")
2097             .pageId(loggedIn.getId())
2098             .filterRequireAuthorization(true)
2099             .openInNewWindow(true),
2100         new PageTile()
2101             .authorizationRules(ruleAnonymousRead)
2102             .id(UUID.randomUUID().toString())
2103             .tileType(PageTile.TileTypeEnum.PAGE)
2104             .title("Secured page (unfiltered)")
2105             .pageId(loggedIn.getId())
2106             .filterRequireAuthorization(false)
2107             .openInNewWindow(true)));
2108     pageRepository.save(page);
2109 
2110     Configuration c = new Configuration();
2111     c.setKey(HOME_PAGE);
2112     c.setValue(page.getId().toString());
2113     configurationRepository.save(c);
2114 
2115     List<MenuItem> globalMenuItems = List.of(
2116         new MenuItem().pageId(about.getId()).label("About").openInNewWindow(false),
2117         new MenuItem()
2118             .label("B3Partners website")
2119             .url("https://www.b3partners.nl/")
2120             .openInNewWindow(true)
2121             .exclusiveOnPageId(about.getId()));
2122     c = new Configuration();
2123     c.setKey(PORTAL_MENU);
2124     c.setJsonValue(new ObjectMapper().valueToTree(globalMenuItems));
2125     configurationRepository.save(c);
2126   }
2127 
2128   private void insertTestDrawing() {
2129     // note that the drawing uuid is hardcoded and used in the DrawingControllerIntegrationTest
2130     try {
2131       this.jdbcClient.sql("""
2132 INSERT INTO data.drawing (id,name,description,domain_data,"access",created_by,created_at,updated_by,updated_at,srid,"version") VALUES
2133 ('38faa008-013e-49d4-9528-8f58c94d8791'::uuid,'Testcase','A private access drawing that is inserted as part of the testdata','{"items": 1, "domain": "test drawings"}','private','tm-admin','2025-02-27 17:53:36.095164+01','tm-admin','2025-02-27 17:54:19.384961+01',28992,1);
2134 """).update();
2135 
2136       this.jdbcClient.sql("""
2137 INSERT INTO data.drawing_feature (drawing_id,id,geometry,properties) VALUES
2138 ('38faa008-013e-49d4-9528-8f58c94d8791'::uuid, '394e0d4a-b374-4d00-94c8-1758ea70e9d9'::uuid, 'SRID=28992;POLYGON ((131982.01 458929.27, 131957.31 458755.45, 132072.39 458736.69, 132099.06 458910.51, 131982.01 458929.27))'::public.geometry, '{"type": "POLYGON", "style": {"label": "", "marker": "circle", "fillColor": "rgb(117, 117, 117)", "labelSize": 15, "labelColor": "rgb(0, 0, 0)", "markerSize": 5, "fillOpacity": 30, "strokeColor": "rgb(98, 54, 255)", "strokeWidth": 3, "strokeOpacity": 100, "markerRotation": 0, "markerFillColor": "rgb(98, 54, 255)", "labelOutlineColor": "rgb(189, 189, 189)", "markerStrokeColor": "rgb(98, 54, 255)", "markerStrokeWidth": 1}}'::jsonb),
2139 ('38faa008-013e-49d4-9528-8f58c94d8791'::uuid, '89fc320c-41a3-4635-8d98-39d822339692'::uuid, 'SRID=28992;POINT (131897.55 458971.24)'::public.geometry, '{"type": "CIRCLE", "style": {"label": "CIRCLE", "marker": "circle", "fillColor": "rgb(117, 117, 117)", "labelSize": 15, "labelColor": "rgb(0, 0, 0)", "markerSize": 5, "fillOpacity": 30, "strokeColor": "rgb(98, 54, 255)", "strokeWidth": 3, "strokeOpacity": 100, "markerRotation": 0, "markerFillColor": "rgb(98, 54, 255)", "labelOutlineColor": "rgb(189, 189, 189)", "markerStrokeColor": "rgb(98, 54, 255)", "markerStrokeWidth": 1}, "radius": 14.989999999990687}'::jsonb),
2140 ('38faa008-013e-49d4-9528-8f58c94d8791'::uuid, '5d6252cc-25b6-4a43-9053-0779b86895a7'::uuid, 'SRID=28992;LINESTRING (131920.76 458754.96, 131923.73 458783.1, 131930.15 458793.97, 131945.95 458912.98, 131967.68 458946.06)'::public.geometry, '{"type": "LINE", "style": {"label": "", "marker": "circle", "fillColor": "rgb(255, 82, 82)", "labelSize": 15, "labelColor": "rgb(0, 0, 0)", "markerSize": 5, "fillOpacity": 30, "strokeColor": "rgb(255, 82, 82)", "strokeWidth": 13, "strokeOpacity": 100, "markerRotation": 0, "markerFillColor": "rgb(98, 54, 255)", "labelOutlineColor": "rgb(189, 189, 189)", "markerStrokeColor": "rgb(98, 54, 255)", "markerStrokeWidth": 1}}'::jsonb)
2141 """).update();
2142     } catch (Exception any) {
2143       logger.error("Error inserting test drawing in data schema, some tests may fail", any);
2144     }
2145   }
2146 
2147   private void insertDrawingStyle() throws IOException {
2148     Upload upload = new Upload()
2149         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
2150         .setMimeType("image/svg+xml")
2151         .setFilename("ISO_7001_PI_PF_007.svg")
2152         .setContent(new ClassPathResource("test/ISO_7001_PI_PF_007.svg").getContentAsByteArray())
2153         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2154     upload = uploadRepository.save(upload);
2155     UUID drinkwaterImageId = upload.getId();
2156 
2157     upload = new Upload()
2158         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
2159         .setMimeType("image/svg+xml")
2160         .setFilename("lichtpunt.svg")
2161         .setContent(new ClassPathResource("test/lichtpunt.svg").getContentAsByteArray())
2162         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2163     upload = uploadRepository.save(upload);
2164     UUID lichtpuntImageId = upload.getId();
2165 
2166     upload = new Upload()
2167         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
2168         .setMimeType("image/svg+xml")
2169         .setFilename("ISO_7010_E003_-_First_aid_sign.svg")
2170         .setContent(new ClassPathResource("test/ISO_7010_E003_-_First_aid_sign.svg").getContentAsByteArray())
2171         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2172     upload = uploadRepository.save(upload);
2173     UUID firstAidImageId = upload.getId();
2174 
2175     Properties props = new Properties();
2176     props.putAll(Map.of(
2177         "water-uuid", drinkwaterImageId.toString(),
2178         "lichtpunt-uuid", lichtpuntImageId.toString(),
2179         "first-aid-uuid", firstAidImageId.toString()));
2180 
2181     String drawingStyles =
2182         new ClassPathResource("test/object-drawing-style.json").getContentAsString(StandardCharsets.UTF_8);
2183     drawingStyles = new PropertyPlaceholderHelper("${", "}").replacePlaceholders(drawingStyles, props);
2184 
2185     // save the updated drawing style
2186     upload = new Upload()
2187         .setCategory(Upload.CATEGORY_DRAWING_STYLE)
2188         .setMimeType("application/json")
2189         .setFilename("object-drawing-style.json")
2190         .setContent(drawingStyles.getBytes(StandardCharsets.UTF_8))
2191         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2192     uploadRepository.save(upload);
2193   }
2194 }