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