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 java.io.IOException;
21  import java.lang.invoke.MethodHandles;
22  import java.net.URI;
23  import java.nio.charset.StandardCharsets;
24  import java.sql.SQLException;
25  import java.time.OffsetDateTime;
26  import java.time.ZoneId;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.NoSuchElementException;
32  import java.util.Optional;
33  import java.util.Properties;
34  import java.util.Set;
35  import java.util.UUID;
36  import org.apache.solr.client.solrj.SolrServerException;
37  import org.quartz.SchedulerException;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  import org.springframework.beans.factory.annotation.Value;
41  import org.springframework.boot.SpringApplication;
42  import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
43  import org.springframework.boot.context.event.ApplicationReadyEvent;
44  import org.springframework.context.ApplicationContext;
45  import org.springframework.context.event.EventListener;
46  import org.springframework.core.io.ClassPathResource;
47  import org.springframework.jdbc.core.simple.JdbcClient;
48  import org.springframework.transaction.annotation.Transactional;
49  import org.springframework.util.PropertyPlaceholderHelper;
50  import org.tailormap.api.admin.model.TaskSchedule;
51  import org.tailormap.api.geotools.featuresources.AttachmentsHelper;
52  import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
53  import org.tailormap.api.geotools.featuresources.JDBCFeatureSourceHelper;
54  import org.tailormap.api.geotools.featuresources.WFSFeatureSourceHelper;
55  import org.tailormap.api.persistence.Application;
56  import org.tailormap.api.persistence.Catalog;
57  import org.tailormap.api.persistence.Configuration;
58  import org.tailormap.api.persistence.GeoService;
59  import org.tailormap.api.persistence.Group;
60  import org.tailormap.api.persistence.Page;
61  import org.tailormap.api.persistence.SearchIndex;
62  import org.tailormap.api.persistence.TMFeatureSource;
63  import org.tailormap.api.persistence.TMFeatureType;
64  import org.tailormap.api.persistence.Upload;
65  import org.tailormap.api.persistence.User;
66  import org.tailormap.api.persistence.helper.GeoServiceHelper;
67  import org.tailormap.api.persistence.json.AppContent;
68  import org.tailormap.api.persistence.json.AppLayerSettings;
69  import org.tailormap.api.persistence.json.AppSettings;
70  import org.tailormap.api.persistence.json.AppTreeLayerNode;
71  import org.tailormap.api.persistence.json.AppTreeLevelNode;
72  import org.tailormap.api.persistence.json.AppTreeNode;
73  import org.tailormap.api.persistence.json.AppUiSettings;
74  import org.tailormap.api.persistence.json.AttachmentAttributeType;
75  import org.tailormap.api.persistence.json.AttributeSettings;
76  import org.tailormap.api.persistence.json.AttributeValueSettings;
77  import org.tailormap.api.persistence.json.AuthorizationRule;
78  import org.tailormap.api.persistence.json.AuthorizationRuleDecision;
79  import org.tailormap.api.persistence.json.Bounds;
80  import org.tailormap.api.persistence.json.CatalogNode;
81  import org.tailormap.api.persistence.json.FeatureTypeRef;
82  import org.tailormap.api.persistence.json.FeatureTypeTemplate;
83  import org.tailormap.api.persistence.json.Filter;
84  import org.tailormap.api.persistence.json.FilterEditConfiguration;
85  import org.tailormap.api.persistence.json.FilterGroup;
86  import org.tailormap.api.persistence.json.GeoServiceDefaultLayerSettings;
87  import org.tailormap.api.persistence.json.GeoServiceLayerSettings;
88  import org.tailormap.api.persistence.json.GeoServiceSettings;
89  import org.tailormap.api.persistence.json.JDBCConnectionProperties;
90  import org.tailormap.api.persistence.json.MenuItem;
91  import org.tailormap.api.persistence.json.PageTile;
92  import org.tailormap.api.persistence.json.ServiceAuthentication;
93  import org.tailormap.api.persistence.json.TailormapObjectRef;
94  import org.tailormap.api.persistence.json.TileLayerHiDpiMode;
95  import org.tailormap.api.persistence.json.WMSStyle;
96  import org.tailormap.api.repository.ApplicationRepository;
97  import org.tailormap.api.repository.CatalogRepository;
98  import org.tailormap.api.repository.ConfigurationRepository;
99  import org.tailormap.api.repository.FeatureSourceRepository;
100 import org.tailormap.api.repository.GeoServiceRepository;
101 import org.tailormap.api.repository.GroupRepository;
102 import org.tailormap.api.repository.PageRepository;
103 import org.tailormap.api.repository.SearchIndexRepository;
104 import org.tailormap.api.repository.UploadRepository;
105 import org.tailormap.api.repository.UserRepository;
106 import org.tailormap.api.scheduling.IndexTask;
107 import org.tailormap.api.scheduling.TMJobDataMap;
108 import org.tailormap.api.scheduling.Task;
109 import org.tailormap.api.scheduling.TaskManagerService;
110 import org.tailormap.api.scheduling.TaskType;
111 import org.tailormap.api.security.InternalAdminAuthentication;
112 import org.tailormap.api.solr.SolrHelper;
113 import org.tailormap.api.solr.SolrService;
114 import org.tailormap.api.viewer.model.AppStyling;
115 import org.tailormap.api.viewer.model.Component;
116 import org.tailormap.api.viewer.model.ComponentConfig;
117 import tools.jackson.core.JacksonException;
118 import tools.jackson.databind.json.JsonMapper;
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                                 .name("begroeidterreindeel")
1305                                 .title("Visualisatie van de begroeide terreindelen")
1306                                 .abstractText(
1307                                     "Deze stylesheet bevat de regels voor de visualisatie van het objecttype Begroeid Terreindeel")
1308                                 .legendUrl(
1309                                     URI.create(
1310                                         "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")),
1311                             new WMSStyle()
1312                                 .name("purple_polygon")
1313                                 .title("purple_polygon")
1314                                 .abstractText(null)
1315                                 .legendUrl(
1316                                     URI.create(
1317                                         "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")))))
1318             .putLayerSettingsItem(
1319                 "lyr:snapshot-geoserver:postgis:kadastraal_perceel",
1320                 new AppLayerSettings().editable(true).addReadOnlyAttributesItem("aanduiding"))
1321             .putLayerSettingsItem(
1322                 "lyr:snapshot-geoserver:sqlserver:wegdeel", new AppLayerSettings().editable(true))
1323             .putLayerSettingsItem(
1324                 "lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel",
1325                 new AppLayerSettings().editable(false))
1326             .putLayerSettingsItem(
1327                 "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1328                 new AppLayerSettings()
1329                     .hiddenFunctionality(Set.of(FEATURE_INFO, ATTRIBUTE_LIST, EXPORT)))
1330             .addFilterGroupsItem(new FilterGroup()
1331                 .id("filtergroup1")
1332                 .source("PRESET")
1333                 .type(FilterGroup.TypeEnum.ATTRIBUTE)
1334                 .layerIds(List.of("lyr:snapshot-geoserver:postgis:begroeidterreindeel"))
1335                 .operator(FilterGroup.OperatorEnum.AND)
1336                 .addFiltersItem(new Filter()
1337                     .id("filter1")
1338                     .type(Filter.TypeEnum.ATTRIBUTE)
1339                     .condition(Filter.ConditionEnum.BEFORE)
1340                     .addValueItem("2025-06-05")
1341                     .attribute("creationdate")
1342                     .attributeType(Filter.AttributeTypeEnum.DATE))
1343                 .addFiltersItem(new Filter()
1344                     .id("filter2")
1345                     .type(Filter.TypeEnum.ATTRIBUTE)
1346                     .condition(Filter.ConditionEnum.UNIQUE_VALUES)
1347                     .addValueItem("bodembedekkers")
1348                     .addValueItem("bosplantsoen")
1349                     .addValueItem("gras- en kruidachtigen")
1350                     .attribute("plus_fysiekvoorkomen")
1351                     .attributeType(Filter.AttributeTypeEnum.STRING)
1352                     .editConfiguration(new FilterEditConfiguration()
1353                         .filterTool(FilterEditConfiguration.FilterToolEnum.CHECKBOX)
1354                         .attributeValuesSettings(List.of(
1355                             new AttributeValueSettings()
1356                                 .value("bodembedekkers")
1357                                 .initiallySelected(true)
1358                                 .selectable(true)
1359                                 .alias("Bodembedekkers"),
1360                             new AttributeValueSettings()
1361                                 .value("bosplantsoen")
1362                                 .initiallySelected(true)
1363                                 .selectable(true)
1364                                 .alias("Bosplantsoen"),
1365                             new AttributeValueSettings()
1366                                 .value("gras- en kruidachtigen")
1367                                 .initiallySelected(true)
1368                                 .selectable(true)
1369                                 .alias("Gras- en kruidachtigen"),
1370                             new AttributeValueSettings()
1371                                 .value("griend en hakhout")
1372                                 .initiallySelected(false)
1373                                 .selectable(true),
1374                             new AttributeValueSettings()
1375                                 .value("heesters")
1376                                 .initiallySelected(false)
1377                                 .selectable(true),
1378                             new AttributeValueSettings()
1379                                 .value("planten")
1380                                 .initiallySelected(false)
1381                                 .selectable(true),
1382                             new AttributeValueSettings()
1383                                 .value("struikrozen")
1384                                 .initiallySelected(false)
1385                                 .selectable(true),
1386                             new AttributeValueSettings()
1387                                 .value("waardeOnbekend")
1388                                 .initiallySelected(false)
1389                                 .selectable(true))))))
1390             .addFilterGroupsItem(new FilterGroup()
1391                 .id("filtergroup2")
1392                 .source("PRESET")
1393                 .type(FilterGroup.TypeEnum.ATTRIBUTE)
1394                 .layerIds(List.of("lyr:snapshot-geoserver:postgis:kadastraal_perceel"))
1395                 .operator(FilterGroup.OperatorEnum.AND)
1396                 .addFiltersItem(new Filter()
1397                     .id("filter3")
1398                     .type(Filter.TypeEnum.ATTRIBUTE)
1399                     .condition(Filter.ConditionEnum.u)
1400                     .addValueItem("1")
1401                     .addValueItem("12419")
1402                     .attribute("perceelnummer")
1403                     .attributeType(Filter.AttributeTypeEnum.DOUBLE)
1404                     .editConfiguration(new FilterEditConfiguration()
1405                         .filterTool(FilterEditConfiguration.FilterToolEnum.SLIDER)
1406                         .initialLowerValue(1d)
1407                         .initialUpperValue(12419d)
1408                         .minimumValue(1d)
1409                         .maximumValue(12419d)))));
1410 
1411     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1412     app.setInitialExtent(
1413         new Bounds().minx(130011d).miny(458031d).maxx(132703d).maxy(459995d));
1414     app.setMaxExtent(new Bounds().minx(-285401d).miny(22598d).maxx(595401d).maxy(903401d));
1415 
1416     if (map5url != null) {
1417       AppTreeLevelNode root =
1418           (AppTreeLevelNode) app.getContentRoot().getBaseLayerNodes().getFirst();
1419       List<String> childrenIds = new ArrayList<>(root.getChildrenIds());
1420       childrenIds.add("lyr:map5:map5topo");
1421       childrenIds.add("lyr:map5:map5topo_simple");
1422       childrenIds.add("lvl:luchtfoto-labels");
1423       root.setChildrenIds(childrenIds);
1424       app.getSettings()
1425           .putLayerSettingsItem("lyr:map5:map5topo", new AppLayerSettings().title("Map5"))
1426           .putLayerSettingsItem("lyr:map5:map5topo_simple", new AppLayerSettings().title("Map5 simple"));
1427       app.getContentRoot()
1428           .addBaseLayerNodesItem(new AppTreeLayerNode()
1429               .objectType("AppTreeLayerNode")
1430               .id("lyr:map5:map5topo")
1431               .serviceId("map5")
1432               .layerName("map5topo")
1433               .visible(false))
1434           .addBaseLayerNodesItem(new AppTreeLayerNode()
1435               .objectType("AppTreeLayerNode")
1436               .id("lyr:map5:map5topo_simple")
1437               .serviceId("map5")
1438               .layerName("map5topo_simple")
1439               .visible(false))
1440           .addBaseLayerNodesItem(new AppTreeLevelNode()
1441               .objectType("AppTreeLevelNode")
1442               .id("lvl:luchtfoto-labels")
1443               .title("Luchtfoto met labels")
1444               .addChildrenIdsItem("lyr:map5:luforoadslabels")
1445               .addChildrenIdsItem("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR2"))
1446           .addBaseLayerNodesItem(new AppTreeLayerNode()
1447               .objectType("AppTreeLayerNode")
1448               .id("lyr:map5:luforoadslabels")
1449               .serviceId("map5")
1450               .layerName("luforoadslabels")
1451               .visible(false))
1452           .addBaseLayerNodesItem(new AppTreeLayerNode()
1453               .objectType("AppTreeLayerNode")
1454               .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR2")
1455               .serviceId("pdok-hwh-luchtfotorgb")
1456               .layerName("Actueel_orthoHR")
1457               .visible(false));
1458     }
1459 
1460     applicationRepository.save(app);
1461 
1462     app = new Application()
1463         .setName("base")
1464         .setTitle("Service base app")
1465         .setCrs("EPSG:28992")
1466         .setAuthorizationRules(ruleAnonymousRead)
1467         .setContentRoot(new AppContent()
1468             .addBaseLayerNodesItem(new AppTreeLevelNode()
1469                 .objectType("AppTreeLevelNode")
1470                 .id("root-base-layers")
1471                 .root(true)
1472                 .title("Base layers")
1473                 .childrenIds(List.of(
1474                     "lyr:openbasiskaart:osm", "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR"))));
1475     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1476     applicationRepository.save(app);
1477 
1478     app = new Application()
1479         .setName("secured")
1480         .setTitle("secured app")
1481         .setCrs("EPSG:28992")
1482         .setAuthorizationRules(ruleLoggedIn)
1483         .setContentRoot(new AppContent()
1484             .addBaseLayerNodesItem(new AppTreeLevelNode()
1485                 .objectType("AppTreeLevelNode")
1486                 .id("root-base-layers")
1487                 .root(true)
1488                 .title("Base layers")
1489                 .childrenIds(List.of(
1490                     "lyr:openbasiskaart:osm",
1491                     "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR",
1492                     "lyr:openbasiskaart-proxied:osm")))
1493             .addBaseLayerNodesItem(new AppTreeLayerNode()
1494                 .objectType("AppTreeLayerNode")
1495                 .id("lyr:openbasiskaart-proxied:osm")
1496                 .serviceId("openbasiskaart-proxied")
1497                 .layerName("osm")
1498                 .visible(false))
1499             .addLayerNodesItem(new AppTreeLevelNode()
1500                 .objectType("AppTreeLevelNode")
1501                 .id("root")
1502                 .root(true)
1503                 .title("Layers")
1504                 .childrenIds(List.of(
1505                     "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1506                     "lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied",
1507                     "lvl:proxied")))
1508             .addLayerNodesItem(new AppTreeLayerNode()
1509                 .objectType("AppTreeLayerNode")
1510                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied")
1511                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1512                 .layerName("Gemeentegebied")
1513                 .visible(true))
1514             .addLayerNodesItem(new AppTreeLayerNode()
1515                 .objectType("AppTreeLayerNode")
1516                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied")
1517                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1518                 .layerName("Provinciegebied")
1519                 .visible(false))
1520             .addLayerNodesItem(new AppTreeLevelNode()
1521                 .objectType("AppTreeLevelNode")
1522                 .id("lvl:proxied")
1523                 .title("Proxied")
1524                 .childrenIds(List.of("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")))
1525             .addLayerNodesItem(new AppTreeLayerNode()
1526                 .objectType("AppTreeLayerNode")
1527                 .id("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")
1528                 .serviceId("snapshot-geoserver-proxied")
1529                 .layerName("postgis:begroeidterreindeel")
1530                 .visible(false)))
1531         .setSettings(
1532             new AppSettings()
1533                 .putLayerSettingsItem(
1534                     "lyr:openbasiskaart-proxied:osm",
1535                     new AppLayerSettings().title("Openbasiskaart (proxied)"))
1536                 .putLayerSettingsItem(
1537                     "lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel",
1538                     new AppLayerSettings()
1539                         .description("This layer should render using purple polygons")
1540                         .selectedStyles(
1541                             List.of(
1542                                 new WMSStyle()
1543                                     .name("purple_polygon")
1544                                     .title("purple_polygon")
1545                                     .abstractText(null)
1546                                     .legendUrl(
1547                                         URI.create(
1548                                             "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"))))));
1549 
1550     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1551     applicationRepository.save(app);
1552 
1553     app = new Application()
1554         .setName("secured-auth")
1555         .setTitle("secured (with authorizations)")
1556         .setCrs("EPSG:28992")
1557         .setAuthorizationRules(List.of(
1558             new AuthorizationRule()
1559                 .groupName("test-foo")
1560                 .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW)),
1561             new AuthorizationRule()
1562                 .groupName("test-bar")
1563                 .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW))))
1564         .setContentRoot(new AppContent()
1565             .addLayerNodesItem(new AppTreeLevelNode()
1566                 .objectType("AppTreeLevelNode")
1567                 .id("root")
1568                 .root(true)
1569                 .title("Layers")
1570                 .childrenIds(List.of("lvl:needs-auth", "lvl:public")))
1571             .addLayerNodesItem(new AppTreeLevelNode()
1572                 .objectType("AppTreeLevelNode")
1573                 .id("lvl:public")
1574                 .title("Public")
1575                 .childrenIds(List.of("lyr:snapshot-geoserver:BGT")))
1576             .addLayerNodesItem(new AppTreeLevelNode()
1577                 .objectType("AppTreeLevelNode")
1578                 .id("lvl:needs-auth")
1579                 .title("Needs auth")
1580                 .childrenIds(List.of(
1581                     "lyr:filtered-snapshot-geoserver:BGT",
1582                     "lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")))
1583             .addLayerNodesItem(new AppTreeLayerNode()
1584                 .objectType("AppTreeLayerNode")
1585                 .id("lyr:filtered-snapshot-geoserver:BGT")
1586                 .serviceId("filtered-snapshot-geoserver")
1587                 .layerName("BGT")
1588                 .visible(true))
1589             .addLayerNodesItem(new AppTreeLayerNode()
1590                 .objectType("AppTreeLayerNode")
1591                 .id("lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")
1592                 .serviceId("filtered-snapshot-geoserver")
1593                 .layerName("postgis:begroeidterreindeel")
1594                 .visible(true))
1595             .addLayerNodesItem(new AppTreeLayerNode()
1596                 .objectType("AppTreeLayerNode")
1597                 .id("lyr:snapshot-geoserver:BGT")
1598                 .serviceId("snapshot-geoserver")
1599                 .layerName("BGT")
1600                 .visible(true)));
1601 
1602     applicationRepository.save(app);
1603 
1604     app = new Application()
1605         .setName("austria")
1606         .setCrs("EPSG:3857")
1607         .setAuthorizationRules(ruleAnonymousRead)
1608         .setTitle("Austria")
1609         .setInitialExtent(
1610             new Bounds().minx(987982d).miny(5799551d).maxx(1963423d).maxy(6320708d))
1611         .setMaxExtent(
1612             new Bounds().minx(206516d).miny(5095461d).maxx(3146930d).maxy(7096232d))
1613         .setContentRoot(new AppContent()
1614             .addBaseLayerNodesItem(new AppTreeLevelNode()
1615                 .objectType("AppTreeLevelNode")
1616                 .id("root-base-layers")
1617                 .root(true)
1618                 .title("Base layers")
1619                 .childrenIds(List.of(
1620                     "lyr:at-basemap:geolandbasemap",
1621                     "lyr:at-basemap:orthofoto",
1622                     "lvl:orthofoto-labels",
1623                     "lyr:osm:xyz")))
1624             .addBaseLayerNodesItem(new AppTreeLayerNode()
1625                 .objectType("AppTreeLayerNode")
1626                 .id("lyr:at-basemap:geolandbasemap")
1627                 .serviceId("at-basemap")
1628                 .layerName("geolandbasemap")
1629                 .visible(true))
1630             .addBaseLayerNodesItem(new AppTreeLayerNode()
1631                 .objectType("AppTreeLayerNode")
1632                 .id("lyr:at-basemap:orthofoto")
1633                 .serviceId("at-basemap")
1634                 .layerName("bmaporthofoto30cm")
1635                 .visible(false))
1636             .addBaseLayerNodesItem(new AppTreeLevelNode()
1637                 .objectType("AppTreeLevelNode")
1638                 .id("lvl:orthofoto-labels")
1639                 .title("Orthophoto with labels")
1640                 .childrenIds(List.of("lyr:at-basemap:bmapoverlay", "lyr:at-basemap:orthofoto_2")))
1641             .addBaseLayerNodesItem(new AppTreeLayerNode()
1642                 .objectType("AppTreeLayerNode")
1643                 .id("lyr:at-basemap:bmapoverlay")
1644                 .serviceId("at-basemap")
1645                 .layerName("bmapoverlay")
1646                 .visible(false))
1647             .addBaseLayerNodesItem(new AppTreeLayerNode()
1648                 .objectType("AppTreeLayerNode")
1649                 .id("lyr:at-basemap:orthofoto_2")
1650                 .serviceId("at-basemap")
1651                 .layerName("bmaporthofoto30cm")
1652                 .visible(false))
1653             .addBaseLayerNodesItem(new AppTreeLayerNode()
1654                 .objectType("AppTreeLayerNode")
1655                 .id("lyr:osm:xyz")
1656                 .serviceId("osm")
1657                 .layerName("xyz")
1658                 .visible(false)));
1659 
1660     applicationRepository.save(app);
1661 
1662     app = new Application()
1663         .setName("3d_utrecht")
1664         .setCrs("EPSG:3857")
1665         .setAuthorizationRules(ruleLoggedIn)
1666         .setTitle("3D Utrecht")
1667         .setInitialExtent(
1668             new Bounds().minx(558390d).miny(6818485d).maxx(566751d).maxy(6824036d))
1669         .setMaxExtent(
1670             new Bounds().minx(91467d).miny(6496479d).maxx(1037043d).maxy(7147453d))
1671         .setSettings(new AppSettings().uiSettings(new AppUiSettings().enable3D(true)))
1672         .setContentRoot(new AppContent()
1673             .addBaseLayerNodesItem(new AppTreeLevelNode()
1674                 .objectType("AppTreeLevelNode")
1675                 .id("root-base-layers")
1676                 .root(true)
1677                 .title("Base layers")
1678                 .childrenIds(List.of("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR", "lyr:osm:xyz")))
1679             .addBaseLayerNodesItem(new AppTreeLayerNode()
1680                 .objectType("AppTreeLayerNode")
1681                 .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR")
1682                 .serviceId("pdok-hwh-luchtfotorgb")
1683                 .layerName("Actueel_orthoHR")
1684                 .visible(true))
1685             .addBaseLayerNodesItem(new AppTreeLayerNode()
1686                 .objectType("AppTreeLayerNode")
1687                 .id("lyr:osm:xyz")
1688                 .serviceId("osm")
1689                 .layerName("xyz")
1690                 .visible(false))
1691             .addLayerNodesItem(new AppTreeLevelNode()
1692                 .objectType("AppTreeLevelNode")
1693                 .id("root")
1694                 .root(true)
1695                 .title("Layers")
1696                 .childrenIds(List.of(
1697                     "lyr:3dbag_utrecht:tiles3d",
1698                     "lyr:snapshot-geoserver:postgis:begroeidterreindeel",
1699                     "lyr:3d_basisvoorziening_gebouwen_proxy:tiles3d",
1700                     "lyr:3d_utrecht_proxied_auth:tiles3d")))
1701             .addLayerNodesItem(new AppTreeLayerNode()
1702                 .objectType("AppTreeLayerNode")
1703                 .id("lyr:3dbag_utrecht:tiles3d")
1704                 .serviceId("3dbag_utrecht")
1705                 .layerName("tiles3d")
1706                 .visible(true))
1707             .addLayerNodesItem(new AppTreeLayerNode()
1708                 .objectType("AppTreeLayerNode")
1709                 .id("lyr:3d_basisvoorziening_gebouwen_proxy:tiles3d")
1710                 .serviceId("3d_basisvoorziening_gebouwen_proxy")
1711                 .layerName("tiles3d")
1712                 .visible(false))
1713             .addLayerNodesItem(new AppTreeLayerNode()
1714                 .objectType("AppTreeLayerNode")
1715                 .id("lyr:3d_utrecht_proxied_auth:tiles3d")
1716                 .serviceId("3d_utrecht_proxied_auth")
1717                 .layerName("tiles3d")
1718                 .visible(false))
1719             .addLayerNodesItem(new AppTreeLayerNode()
1720                 .objectType("AppTreeLayerNode")
1721                 .id("lyr:snapshot-geoserver:postgis:begroeidterreindeel")
1722                 .serviceId("snapshot-geoserver")
1723                 .layerName("postgis:begroeidterreindeel")
1724                 .visible(true))
1725             .addTerrainLayerNodesItem(new AppTreeLevelNode()
1726                 .objectType("AppTreeLevelNode")
1727                 .id("root-terrain-layers")
1728                 .root(true)
1729                 .title("Terrain Layers")
1730                 .childrenIds(List.of("lyr:ahn_terrain_model:quantizedmesh")))
1731             .addTerrainLayerNodesItem(new AppTreeLayerNode()
1732                 .objectType("AppTreeLayerNode")
1733                 .id("lyr:ahn_terrain_model:quantizedmesh")
1734                 .serviceId("ahn_terrain_model")
1735                 .layerName("quantizedmesh")
1736                 .visible(false)));
1737 
1738     applicationRepository.save(app);
1739 
1740     app = new Application()
1741         .setName("public-with-auth")
1742         .setTitle("Public app with one restricted layer in a group")
1743         .setCrs("EPSG:28992")
1744         .setAuthorizationRules(ruleAnonymousRead)
1745         .setInitialExtent(
1746             new Bounds().minx(130011d).miny(458031d).maxx(132703d).maxy(459995d))
1747         .setMaxExtent(
1748             new Bounds().minx(-285401d).miny(22598d).maxx(595401d).maxy(903401d))
1749         .setStyling(new AppStyling().logo(logo.getId().toString()))
1750         .setContentRoot(new AppContent()
1751             .addBaseLayerNodesItem(new AppTreeLevelNode()
1752                 .objectType("AppTreeLevelNode")
1753                 .id("root")
1754                 .root(true)
1755                 .title("Basemaps")
1756                 .childrenIds(List.of("lyr:openbasiskaart:osm")))
1757             .addBaseLayerNodesItem(new AppTreeLayerNode()
1758                 .objectType("AppTreeLayerNode")
1759                 .id("lyr:openbasiskaart:osm")
1760                 .serviceId("openbasiskaart")
1761                 .layerName("osm")
1762                 .visible(true))
1763             .addLayerNodesItem(new AppTreeLevelNode()
1764                 .objectType("AppTreeLevelNode")
1765                 .id("root")
1766                 .root(true)
1767                 .title("Application layers")
1768                 .childrenIds(List.of(
1769                     "lyr:snapshot-geoserver:postgis:kadastraal_perceel", "xpfhl34VmghkU12nP9Jer")))
1770             .addLayerNodesItem(new AppTreeLayerNode()
1771                 .objectType("AppTreeLayerNode")
1772                 .id("lyr:snapshot-geoserver:postgis:kadastraal_perceel")
1773                 .serviceId("snapshot-geoserver")
1774                 .layerName("postgis:kadastraal_perceel")
1775                 .visible(true))
1776             .addLayerNodesItem(new AppTreeLevelNode()
1777                 .id("xpfhl34VmghkU12nP9Jer")
1778                 .root(false)
1779                 .title("restricted")
1780                 .objectType("AppTreeLevelNode")
1781                 .childrenIds(List.of("lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")))
1782             .addLayerNodesItem(new AppTreeLayerNode()
1783                 .objectType("AppTreeLayerNode")
1784                 .id("lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")
1785                 .visible(true)
1786                 .serviceId("filtered-snapshot-geoserver")
1787                 .layerName("postgis:begroeidterreindeel")))
1788         .setSettings(new AppSettings());
1789 
1790     applicationRepository.save(app);
1791 
1792     Configuration config = new Configuration();
1793     config.setKey(Configuration.DEFAULT_APP);
1794     config.setValue("default");
1795     configurationRepository.save(config);
1796     config = new Configuration();
1797     config.setKey(Configuration.DEFAULT_BASE_APP);
1798     config.setValue("base");
1799     configurationRepository.save(config);
1800   }
1801 
1802   private void createConfigurationTestData() throws JacksonException {
1803     Configuration config = new Configuration();
1804     config.setKey("test");
1805     config.setAvailableForViewer(true);
1806     config.setValue("test value");
1807     config.setJsonValue(new JsonMapper().readTree("{ \"someProperty\": 1, \"nestedObject\": { \"num\": 42 } }"));
1808     configurationRepository.save(config);
1809   }
1810 
1811   @Transactional
1812   public void createSolrIndex() throws Exception {
1813     if (connectToSpatialDbs) {
1814       // flush() the repo because we need to make sure feature type testdata is fully stored
1815       // before creating the Solr index (which requires access to the feature type settings)
1816       featureSourceRepository.flush();
1817 
1818       logger.info("Creating Solr index");
1819       @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
1820       final String solrUrl = "http://" + (connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "solr") + ":8983/solr/";
1821       this.solrService.setSolrUrl(solrUrl);
1822       SolrHelper solrHelper = new SolrHelper(this.solrService.getSolrClientForIndexing())
1823           .withBatchSize(solrBatchSize)
1824           .withGeometryValidationRule(solrGeometryValidationRule);
1825       GeoService geoService =
1826           geoServiceRepository.findById("snapshot-geoserver").orElseThrow();
1827       Application defaultApp = applicationRepository.findByName("default");
1828 
1829       TMFeatureType begroeidterreindeelFT = geoService.findFeatureTypeForLayer(
1830           geoService.findLayer("postgis:begroeidterreindeel"), featureSourceRepository);
1831 
1832       TMFeatureType wegdeelFT = geoService.findFeatureTypeForLayer(
1833           geoService.findLayer("sqlserver:wegdeel"), featureSourceRepository);
1834 
1835       TMFeatureType kadastraalPerceelFT = geoService.findFeatureTypeForLayer(
1836           geoService.findLayer("postgis:kadastraal_perceel"), featureSourceRepository);
1837 
1838       try (solrHelper) {
1839         SearchIndex begroeidterreindeelIndex = null;
1840         if (begroeidterreindeelFT != null) {
1841           begroeidterreindeelIndex = new SearchIndex()
1842               .setName("Begroeidterreindeel")
1843               .setFeatureTypeId(begroeidterreindeelFT.getId())
1844               .setSearchFieldsUsed(List.of("class", "plus_fysiekvoorkomen", "bronhouder"))
1845               .setSearchDisplayFieldsUsed(List.of("class", "plus_fysiekvoorkomen"));
1846           begroeidterreindeelIndex = searchIndexRepository.save(begroeidterreindeelIndex);
1847           begroeidterreindeelIndex = solrHelper.addFeatureTypeIndex(
1848               begroeidterreindeelIndex,
1849               begroeidterreindeelFT,
1850               featureSourceFactoryHelper,
1851               searchIndexRepository);
1852           begroeidterreindeelIndex = searchIndexRepository.save(begroeidterreindeelIndex);
1853         }
1854 
1855         SearchIndex kadastraalPerceelIndex = null;
1856         if (kadastraalPerceelFT != null) {
1857           kadastraalPerceelIndex = new SearchIndex()
1858               .setName("kadastraal_perceel")
1859               .setFeatureTypeId(kadastraalPerceelFT.getId())
1860               .setSearchFieldsUsed(List.of("aanduiding"))
1861               .setSearchDisplayFieldsUsed(List.of("aanduiding"));
1862           kadastraalPerceelIndex = searchIndexRepository.save(kadastraalPerceelIndex);
1863           kadastraalPerceelIndex = solrHelper.addFeatureTypeIndex(
1864               kadastraalPerceelIndex,
1865               kadastraalPerceelFT,
1866               featureSourceFactoryHelper,
1867               searchIndexRepository);
1868           kadastraalPerceelIndex = searchIndexRepository.save(kadastraalPerceelIndex);
1869         }
1870 
1871         SearchIndex wegdeelIndex = null;
1872         if (wegdeelFT != null) {
1873           wegdeelIndex = new SearchIndex()
1874               .setName("Wegdeel")
1875               .setFeatureTypeId(wegdeelFT.getId())
1876               .setSearchFieldsUsed(List.of(
1877                   "function_", "plus_fysiekvoorkomenwegdeel", "surfacematerial", "bronhouder"))
1878               .setSearchDisplayFieldsUsed(List.of("function_", "plus_fysiekvoorkomenwegdeel"));
1879           wegdeelIndex = searchIndexRepository.save(wegdeelIndex);
1880           wegdeelIndex = solrHelper.addFeatureTypeIndex(
1881               wegdeelIndex, wegdeelFT, featureSourceFactoryHelper, searchIndexRepository);
1882           wegdeelIndex = searchIndexRepository.save(wegdeelIndex);
1883         }
1884 
1885         featureSourceRepository
1886             .getByTitle("PostGIS")
1887             .flatMap(fs -> fs.getFeatureTypes().stream()
1888                 .filter(ft -> ft.getName().equals("bak"))
1889                 .findFirst())
1890             .ifPresent(ft -> {
1891               SearchIndex bak = new SearchIndex()
1892                   .setName("bak")
1893                   .setFeatureTypeId(ft.getId())
1894                   .setSearchFieldsUsed(List.of("gmlid", "identificatie", "plus_type"))
1895                   .setSearchDisplayFieldsUsed(List.of("gmlid", "plus_type", "bronhouder"));
1896               searchIndexRepository.save(bak);
1897               try {
1898                 bak = solrHelper.addFeatureTypeIndex(
1899                     bak, ft, featureSourceFactoryHelper, searchIndexRepository);
1900                 searchIndexRepository.save(bak);
1901               } catch (IOException | SolrServerException e) {
1902                 throw new RuntimeException(e);
1903               }
1904             });
1905 
1906         // creating a solr index of config this will/should fail because there is no primary key in
1907         // the FT
1908         featureSourceRepository
1909             .getByTitle("PostGIS OSM")
1910             .flatMap(fs -> fs.getFeatureTypes().stream()
1911                 .filter(ft -> ft.getName().equals("osm_roads"))
1912                 .findFirst())
1913             .ifPresent(ft -> {
1914               SearchIndex osm_no_pk = new SearchIndex()
1915                   .setName("osm_no_pk")
1916                   .setFeatureTypeId(ft.getId())
1917                   .setSearchFieldsUsed(List.of("landuse", "osm_id", "natural", "boundary"))
1918                   .setSearchDisplayFieldsUsed(
1919                       List.of("landuse", "osm_id", "natural", "amenity", "boundary"));
1920               searchIndexRepository.save(osm_no_pk);
1921             });
1922 
1923         AppTreeLayerNode begroeidTerreindeelLayerNode = defaultApp
1924             .getAllAppTreeLayerNode()
1925             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:postgis:begroeidterreindeel"))
1926             .findFirst()
1927             .orElse(null);
1928 
1929         if (begroeidTerreindeelLayerNode != null && begroeidterreindeelIndex != null) {
1930           defaultApp
1931               .getAppLayerSettings(begroeidTerreindeelLayerNode)
1932               .setSearchIndexId(begroeidterreindeelIndex.getId());
1933         }
1934 
1935         AppTreeLayerNode kadastraalPerceelLayerNode = defaultApp
1936             .getAllAppTreeLayerNode()
1937             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:postgis:kadastraal_perceel"))
1938             .findFirst()
1939             .orElse(null);
1940 
1941         if (kadastraalPerceelLayerNode != null && kadastraalPerceelIndex != null) {
1942           defaultApp
1943               .getAppLayerSettings(kadastraalPerceelLayerNode)
1944               .setSearchIndexId(kadastraalPerceelIndex.getId());
1945         }
1946 
1947         AppTreeLayerNode wegdeel = defaultApp
1948             .getAllAppTreeLayerNode()
1949             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:sqlserver:wegdeel"))
1950             .findFirst()
1951             .orElse(null);
1952 
1953         if (wegdeel != null && wegdeelIndex != null) {
1954           defaultApp.getAppLayerSettings(wegdeel).setSearchIndexId(wegdeelIndex.getId());
1955         }
1956 
1957         applicationRepository.save(defaultApp);
1958       }
1959     }
1960   }
1961 
1962   private void createSearchIndexTasks() {
1963     logger.info("Creating search index tasks");
1964     List.of("Begroeidterreindeel", "kadastraal_perceel")
1965         .forEach(name -> searchIndexRepository.findByName(name).ifPresent(index -> {
1966           index.setSchedule(new TaskSchedule()
1967               /* hour */
1968               .cronExpression("0 0 0/1 1/1 * ? *")
1969               // /* 15 min */
1970               // .cronExpression("0 0/15 * 1/1 * ? *")
1971               .description("Update Solr index \"" + name + "\" every hour"));
1972           try {
1973             final UUID uuid = taskManagerService.createTask(
1974                 IndexTask.class,
1975                 new TMJobDataMap(Map.of(
1976                     Task.TYPE_KEY,
1977                     TaskType.INDEX,
1978                     Task.DESCRIPTION_KEY,
1979                     index.getSchedule().getDescription(),
1980                     IndexTask.INDEX_KEY,
1981                     index.getId().toString(),
1982                     Task.PRIORITY_KEY,
1983                     10)),
1984                 index.getSchedule().getCronExpression());
1985 
1986             index.getSchedule().setUuid(uuid);
1987             searchIndexRepository.save(index);
1988 
1989             logger.info("Created task to update Solr index with key: {}", uuid);
1990           } catch (SchedulerException e) {
1991             logger.error("Error creating scheduled solr index task", e);
1992           }
1993         }));
1994   }
1995 
1996   private void createPages() throws IOException {
1997     Upload logo = new Upload()
1998         .setCategory(Upload.CATEGORY_PORTAL_IMAGE)
1999         .setFilename("gradient.svg")
2000         .setMimeType("image/svg+xml")
2001         .setContent(new ClassPathResource("test/gradient-logo.svg").getContentAsByteArray())
2002         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2003     uploadRepository.save(logo);
2004 
2005     Page loggedIn = new Page();
2006     loggedIn.setAuthorizationRules(ruleLoggedIn);
2007     loggedIn.setName("loggedIn");
2008     loggedIn.setType("page");
2009     loggedIn.setContent("About Tailormap");
2010     loggedIn.setContent("""
2011 # About Tailormap
2012 This is a page for logged in users.
2013 """);
2014     pageRepository.save(loggedIn);
2015 
2016     Page about = new Page();
2017     about.setAuthorizationRules(ruleAnonymousRead);
2018     about.setName("about");
2019     about.setType("page");
2020     about.setContent("About Tailormap");
2021     about.setContent("""
2022 # About Tailormap
2023 
2024 This is a page about *Tailormap*. It doesn't say much yet.
2025 """);
2026     pageRepository.save(about);
2027 
2028     Page page = new Page();
2029     page.setAuthorizationRules(ruleAnonymousRead);
2030     page.setName("home");
2031     page.setType("page");
2032     page.setTitle("Tailormap - Home");
2033     page.setContent("""
2034 # Welcome to Tailormap!
2035 
2036 This page is only visible when you implement a frontend to display pages, or get it (including a simple CMS)
2037 from [B3Partners](https://www.b3partners.nl)!
2038 """);
2039     page.setClassName(null);
2040     page.setTiles(List.of(
2041         new PageTile()
2042             .authorizationRules(ruleAnonymousRead)
2043             .id(UUID.randomUUID().toString())
2044             .title("Default app")
2045             .tileType(PageTile.TileTypeEnum.APPLICATION)
2046             .applicationId(Optional.ofNullable(applicationRepository.findByName("default"))
2047                 .map(Application::getId)
2048                 .orElse(null))
2049             .image(logo.getId().toString())
2050             .content("*Default app* tile content")
2051             .filterRequireAuthorization(false)
2052             .openInNewWindow(false),
2053         new PageTile()
2054             .authorizationRules(ruleAnonymousRead)
2055             .id(UUID.randomUUID().toString())
2056             .title("Secured app")
2057             .tileType(PageTile.TileTypeEnum.APPLICATION)
2058             .applicationId(Optional.ofNullable(applicationRepository.findByName("secured"))
2059                 .map(Application::getId)
2060                 .orElse(null))
2061             .filterRequireAuthorization(true)
2062             .content("Secure app, only shown if user has authorization")
2063             .openInNewWindow(false),
2064         new PageTile()
2065             .authorizationRules(ruleAnonymousRead)
2066             .id(UUID.randomUUID().toString())
2067             .title("Secured app (unfiltered)")
2068             .tileType(PageTile.TileTypeEnum.APPLICATION)
2069             .applicationId(Optional.ofNullable(applicationRepository.findByName("secured"))
2070                 .map(Application::getId)
2071                 .orElse(null))
2072             .filterRequireAuthorization(false)
2073             .content("Secure app, tile shown to everyone")
2074             .openInNewWindow(false),
2075         new PageTile()
2076             .authorizationRules(ruleAnonymousRead)
2077             .id(UUID.randomUUID().toString())
2078             .title("About")
2079             .tileType(PageTile.TileTypeEnum.PAGE)
2080             .pageId(about.getId())
2081             .openInNewWindow(false),
2082         new PageTile()
2083             .authorizationRules(ruleAnonymousRead)
2084             .id(UUID.randomUUID().toString())
2085             .title("B3Partners")
2086             .tileType(PageTile.TileTypeEnum.URL)
2087             .url("https://www.b3partners.nl/")
2088             .openInNewWindow(true),
2089         new PageTile()
2090             .authorizationRules(ruleLoggedIn)
2091             .id(UUID.randomUUID().toString())
2092             .title("Github repository")
2093             .tileType(PageTile.TileTypeEnum.URL)
2094             .url("https://github.com/Tailormap/tailormap-viewer")
2095             .openInNewWindow(true),
2096         new PageTile()
2097             .authorizationRules(ruleAnonymousRead)
2098             .id(UUID.randomUUID().toString())
2099             .tileType(PageTile.TileTypeEnum.PAGE)
2100             .title("Secured page")
2101             .pageId(loggedIn.getId())
2102             .filterRequireAuthorization(true)
2103             .openInNewWindow(true),
2104         new PageTile()
2105             .authorizationRules(ruleAnonymousRead)
2106             .id(UUID.randomUUID().toString())
2107             .tileType(PageTile.TileTypeEnum.PAGE)
2108             .title("Secured page (unfiltered)")
2109             .pageId(loggedIn.getId())
2110             .filterRequireAuthorization(false)
2111             .openInNewWindow(true)));
2112     pageRepository.save(page);
2113 
2114     Configuration c = new Configuration();
2115     c.setKey(HOME_PAGE);
2116     c.setValue(page.getId().toString());
2117     configurationRepository.save(c);
2118 
2119     List<MenuItem> globalMenuItems = List.of(
2120         new MenuItem().pageId(about.getId()).label("About").openInNewWindow(false),
2121         new MenuItem()
2122             .label("B3Partners website")
2123             .url("https://www.b3partners.nl/")
2124             .openInNewWindow(true)
2125             .exclusiveOnPageId(about.getId()));
2126     c = new Configuration();
2127     c.setKey(PORTAL_MENU);
2128     c.setJsonValue(new JsonMapper().valueToTree(globalMenuItems));
2129     configurationRepository.save(c);
2130   }
2131 
2132   private void insertTestDrawing() {
2133     // note that the drawing uuid is hardcoded and used in the DrawingControllerIntegrationTest
2134     try {
2135       this.jdbcClient.sql("""
2136 INSERT INTO data.drawing (id,name,description,domain_data,"access",created_by,created_at,updated_by,updated_at,srid,"version") VALUES
2137 ('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);
2138 """).update();
2139 
2140       this.jdbcClient.sql("""
2141 INSERT INTO data.drawing_feature (drawing_id,id,geometry,properties) VALUES
2142 ('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),
2143 ('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),
2144 ('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)
2145 """).update();
2146     } catch (Exception any) {
2147       logger.error("Error inserting test drawing in data schema, some tests may fail", any);
2148     }
2149   }
2150 
2151   private void insertDrawingStyle() throws IOException {
2152     Upload upload = new Upload()
2153         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
2154         .setMimeType("image/svg+xml")
2155         .setFilename("ISO_7001_PI_PF_007.svg")
2156         .setContent(new ClassPathResource("test/ISO_7001_PI_PF_007.svg").getContentAsByteArray())
2157         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2158     upload = uploadRepository.save(upload);
2159     UUID drinkwaterImageId = upload.getId();
2160 
2161     upload = new Upload()
2162         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
2163         .setMimeType("image/svg+xml")
2164         .setFilename("lichtpunt.svg")
2165         .setContent(new ClassPathResource("test/lichtpunt.svg").getContentAsByteArray())
2166         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2167     upload = uploadRepository.save(upload);
2168     UUID lichtpuntImageId = upload.getId();
2169 
2170     upload = new Upload()
2171         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
2172         .setMimeType("image/svg+xml")
2173         .setFilename("ISO_7010_E003_-_First_aid_sign.svg")
2174         .setContent(new ClassPathResource("test/ISO_7010_E003_-_First_aid_sign.svg").getContentAsByteArray())
2175         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2176     upload = uploadRepository.save(upload);
2177     UUID firstAidImageId = upload.getId();
2178 
2179     Properties props = new Properties();
2180     props.putAll(Map.of(
2181         "water-uuid", drinkwaterImageId.toString(),
2182         "lichtpunt-uuid", lichtpuntImageId.toString(),
2183         "first-aid-uuid", firstAidImageId.toString()));
2184 
2185     String drawingStyles =
2186         new ClassPathResource("test/object-drawing-style.json").getContentAsString(StandardCharsets.UTF_8);
2187     drawingStyles = new PropertyPlaceholderHelper("${", "}").replacePlaceholders(drawingStyles, props);
2188 
2189     // save the updated drawing style
2190     upload = new Upload()
2191         .setCategory(Upload.CATEGORY_DRAWING_STYLE)
2192         .setMimeType("application/json")
2193         .setFilename("object-drawing-style.json")
2194         .setContent(drawingStyles.getBytes(StandardCharsets.UTF_8))
2195         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2196     uploadRepository.save(upload);
2197   }
2198 }