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 -> {
1036           ft.getSettings()
1037               .addAttachmentAttributesItem(new AttachmentAttributeType()
1038                   .attributeName("bijlage")
1039                   .maxAttachmentSize(4_000_000L)
1040                   .mimeType("image/jpeg, image/svg+xml, .png, image/*"));
1041           try {
1042             AttachmentsHelper.createAttachmentTableForFeatureType(ft);
1043           } catch (IOException | SQLException e) {
1044             throw new RuntimeException("Failed to create attachments table for kadastraal_perceel", e);
1045           }
1046           // hide primary key
1047           ft.getSettings().addHideAttributesItem("gml_id");
1048           // make some attributes editable
1049           ft.getSettings().addEditableAttributesItem("begin_geldigheid");
1050           ft.getSettings().addEditableAttributesItem("tijdstip_registratie");
1051         });
1052 
1053     featureSources.get("postgis_osm").getFeatureTypes().stream()
1054         .filter(ft -> ft.getName().equals("osm_polygon"))
1055         .findFirst()
1056         .ifPresent(ft -> {
1057           ft.getSettings().addEditableAttributesItem("osm_id");
1058           ft.getSettings().addEditableAttributesItem("building");
1059           ft.getSettings().addEditableAttributesItem("z_order");
1060         });
1061   }
1062 
1063   public void createAppTestData() throws Exception {
1064     Upload logo = new Upload()
1065         .setCategory(Upload.CATEGORY_APP_LOGO)
1066         .setFilename("gradient.svg")
1067         .setMimeType("image/svg+xml")
1068         .setContent(new ClassPathResource("test/gradient-logo.svg").getContentAsByteArray())
1069         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
1070     uploadRepository.save(logo);
1071 
1072     List<AppTreeNode> baseNodes = List.of(
1073         new AppTreeLayerNode()
1074             .objectType("AppTreeLayerNode")
1075             .id("lyr:openbasiskaart:osm")
1076             .serviceId("openbasiskaart")
1077             .layerName("osm")
1078             .visible(true),
1079         new AppTreeLayerNode()
1080             .objectType("AppTreeLayerNode")
1081             .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR")
1082             .serviceId("pdok-hwh-luchtfotorgb")
1083             .layerName("Actueel_orthoHR")
1084             .visible(false));
1085 
1086     Application app = new Application()
1087         .setName("default")
1088         .setTitle("Tailormap demo")
1089         .setCrs("EPSG:28992")
1090         .setAuthorizationRules(ruleAnonymousRead)
1091         .setComponents(List.of(
1092             new Component()
1093                 .type("SIMPLE_SEARCH")
1094                 .config(new ComponentConfig()
1095                     .enabled(true)
1096                     .putAdditionalProperty("municipalities", List.of("0344"))),
1097             new Component().type("EDIT").config(new ComponentConfig().enabled(true)),
1098             new Component()
1099                 .type("TOC")
1100                 .config(new ComponentConfig()
1101                     .enabled(true)
1102                     .putAdditionalProperty("showEditLayerIcon", true)),
1103             new Component()
1104                 .type("COORDINATE_LINK_WINDOW")
1105                 .config(new ComponentConfig()
1106                     .enabled(true)
1107                     .putAdditionalProperty(
1108                         "urls",
1109                         List.of(
1110                             Map.of(
1111                                 "id",
1112                                 "google-maps",
1113                                 "url",
1114                                 "https://www.google.com/maps/@[lat],[lon],18z",
1115                                 "alias",
1116                                 "Google Maps",
1117                                 "projection",
1118                                 "EPSG:4326"),
1119                             Map.of(
1120                                 "id",
1121                                 "tm-demo",
1122                                 "url",
1123                                 "https://demo.tailormap.com/#@[X],[Y],18",
1124                                 "alias",
1125                                 "Tailormap demo",
1126                                 "projection",
1127                                 "EPSG:28992"))))))
1128         .setContentRoot(new AppContent()
1129             .addBaseLayerNodesItem(new AppTreeLevelNode()
1130                 .objectType("AppTreeLevelNode")
1131                 .id("root-base-layers")
1132                 .root(true)
1133                 .title("Base layers")
1134                 .childrenIds(List.of(
1135                     "lyr:openbasiskaart:osm",
1136                     "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR",
1137                     "lyr:openbasiskaart-proxied:osm",
1138                     "lyr:openbasiskaart-tms:xyz",
1139                     "lyr:b3p-mapproxy-luchtfoto:xyz")))
1140             .addBaseLayerNodesItem(
1141                 // This layer from a secured proxied service should not be proxyable in a
1142                 // public app, see test_wms_secured_proxy_not_in_public_app() testcase
1143                 new AppTreeLayerNode()
1144                     .objectType("AppTreeLayerNode")
1145                     .id("lyr:openbasiskaart-proxied:osm")
1146                     .serviceId("openbasiskaart-proxied")
1147                     .layerName("osm")
1148                     .visible(false))
1149             .addBaseLayerNodesItem(new AppTreeLayerNode()
1150                 .objectType("AppTreeLayerNode")
1151                 .id("lyr:openbasiskaart-tms:xyz")
1152                 .serviceId("openbasiskaart-tms")
1153                 .layerName("xyz")
1154                 .visible(false))
1155             .addBaseLayerNodesItem(new AppTreeLayerNode()
1156                 .objectType("AppTreeLayerNode")
1157                 .id("lyr:b3p-mapproxy-luchtfoto:xyz")
1158                 .serviceId("b3p-mapproxy-luchtfoto")
1159                 .layerName("xyz")
1160                 .visible(false))
1161             .addLayerNodesItem(new AppTreeLevelNode()
1162                 .objectType("AppTreeLevelNode")
1163                 .id("root")
1164                 .root(true)
1165                 .title("Layers")
1166                 .childrenIds(List.of(
1167                     "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1168                     "lyr:bestuurlijkegebieden-proxied:Provinciegebied",
1169                     "lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied",
1170                     "lyr:snapshot-geoserver:postgis:begroeidterreindeel",
1171                     "lyr:snapshot-geoserver:postgis:bak",
1172                     "lyr:snapshot-geoserver:postgis:kadastraal_perceel",
1173                     "lyr:snapshot-geoserver:sqlserver:wegdeel",
1174                     "lyr:snapshot-geoserver:oracle:WATERDEEL",
1175                     "lyr:snapshot-geoserver:BGT",
1176                     "lvl:proxied",
1177                     "lvl:osm",
1178                     "lvl:archeo")))
1179             .addLayerNodesItem(new AppTreeLayerNode()
1180                 .objectType("AppTreeLayerNode")
1181                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied")
1182                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1183                 .layerName("Provinciegebied")
1184                 .visible(true))
1185             // This is a layer from proxied service with auth that should also not be
1186             // visible, but it has a feature source attached, should also be denied for
1187             // features access and not be included in TOC
1188             .addLayerNodesItem(new AppTreeLayerNode()
1189                 .objectType("AppTreeLayerNode")
1190                 .id("lyr:bestuurlijkegebieden-proxied:Provinciegebied")
1191                 .serviceId("bestuurlijkegebieden-proxied")
1192                 .layerName("Provinciegebied")
1193                 .visible(false))
1194             .addLayerNodesItem(new AppTreeLayerNode()
1195                 .objectType("AppTreeLayerNode")
1196                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied")
1197                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1198                 .layerName("Gemeentegebied")
1199                 .visible(true))
1200             .addLayerNodesItem(new AppTreeLayerNode()
1201                 .objectType("AppTreeLayerNode")
1202                 .id("lyr:snapshot-geoserver:postgis:begroeidterreindeel")
1203                 .serviceId("snapshot-geoserver")
1204                 .layerName("postgis:begroeidterreindeel")
1205                 .visible(true))
1206             .addLayerNodesItem(new AppTreeLayerNode()
1207                 .objectType("AppTreeLayerNode")
1208                 .id("lyr:snapshot-geoserver:postgis:bak")
1209                 .serviceId("snapshot-geoserver")
1210                 .layerName("postgis:bak")
1211                 .visible(false))
1212             .addLayerNodesItem(new AppTreeLayerNode()
1213                 .objectType("AppTreeLayerNode")
1214                 .id("lyr:snapshot-geoserver:postgis:kadastraal_perceel")
1215                 .serviceId("snapshot-geoserver")
1216                 .layerName("postgis:kadastraal_perceel")
1217                 .visible(false))
1218             .addLayerNodesItem(new AppTreeLayerNode()
1219                 .objectType("AppTreeLayerNode")
1220                 .id("lyr:snapshot-geoserver:sqlserver:wegdeel")
1221                 .serviceId("snapshot-geoserver")
1222                 .layerName("sqlserver:wegdeel")
1223                 .visible(true))
1224             .addLayerNodesItem(new AppTreeLayerNode()
1225                 .objectType("AppTreeLayerNode")
1226                 .id("lyr:snapshot-geoserver:oracle:WATERDEEL")
1227                 .serviceId("snapshot-geoserver")
1228                 .layerName("oracle:WATERDEEL")
1229                 .visible(true))
1230             .addLayerNodesItem(new AppTreeLayerNode()
1231                 .objectType("AppTreeLayerNode")
1232                 .id("lyr:snapshot-geoserver:BGT")
1233                 .serviceId("snapshot-geoserver")
1234                 .layerName("BGT")
1235                 .visible(false))
1236             .addLayerNodesItem(new AppTreeLevelNode()
1237                 .objectType("AppTreeLevelNode")
1238                 .id("lvl:proxied")
1239                 .title("Proxied")
1240                 .childrenIds(List.of("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")))
1241             .addLayerNodesItem(new AppTreeLayerNode()
1242                 .objectType("AppTreeLayerNode")
1243                 .id("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")
1244                 .serviceId("snapshot-geoserver-proxied")
1245                 .layerName("postgis:begroeidterreindeel")
1246                 .visible(false))
1247             .addLayerNodesItem(new AppTreeLevelNode()
1248                 .objectType("AppTreeLevelNode")
1249                 .id("lvl:osm")
1250                 .title("OSM")
1251                 .childrenIds(List.of("lyr:snapshot-geoserver:postgis:osm_polygon")))
1252             .addLayerNodesItem(new AppTreeLayerNode()
1253                 .objectType("AppTreeLayerNode")
1254                 .id("lyr:snapshot-geoserver:postgis:osm_polygon")
1255                 .serviceId("snapshot-geoserver")
1256                 .layerName("postgis:osm_polygon")
1257                 .visible(false))
1258             .addLayerNodesItem(new AppTreeLevelNode()
1259                 .objectType("AppTreeLevelNode")
1260                 .id("lvl:archeo")
1261                 .title("Archeology")
1262                 .childrenIds(List.of("lyr:demo:geomorfologie")))
1263             .addLayerNodesItem(new AppTreeLayerNode()
1264                 .objectType("AppTreeLayerNode")
1265                 .id("lyr:demo:geomorfologie")
1266                 .serviceId("demo")
1267                 .layerName("geomorfologie")
1268                 .visible(true)))
1269         .setStyling(new AppStyling().logo(logo.getId().toString()))
1270         .setSettings(new AppSettings()
1271             .putLayerSettingsItem("lyr:openbasiskaart:osm", new AppLayerSettings().title("Openbasiskaart"))
1272             .putLayerSettingsItem(
1273                 "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR", new AppLayerSettings().title("Luchtfoto"))
1274             .putLayerSettingsItem(
1275                 "lyr:openbasiskaart-proxied:osm",
1276                 new AppLayerSettings().title("Openbasiskaart (proxied)"))
1277             .putLayerSettingsItem(
1278                 "lyr:snapshot-geoserver:oracle:WATERDEEL",
1279                 new AppLayerSettings()
1280                     .opacity(50)
1281                     .title("Waterdeel overridden title")
1282                     .editable(true)
1283                     .description("This is the layer description from the app layer setting.")
1284                     .attribution(
1285                         "CC BY 4.0 [BGT/Kadaster](https://www.nationaalgeoregister.nl/geonetwork/srv/api/records/2cb4769c-b56e-48fa-8685-c48f61b9a319)"))
1286             .putLayerSettingsItem(
1287                 "lyr:snapshot-geoserver:postgis:osm_polygon",
1288                 new AppLayerSettings()
1289                     .description("OpenStreetMap polygon data in EPSG:3857")
1290                     .opacity(60)
1291                     .editable(true)
1292                     .title("OSM Polygon (EPSG:3857)")
1293                     .attribution(
1294                         "© [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors"))
1295             .putLayerSettingsItem(
1296                 "lyr:snapshot-geoserver:postgis:begroeidterreindeel",
1297                 new AppLayerSettings()
1298                     .editable(true)
1299                     .addHideAttributesItem("begroeidterreindeeloptalud")
1300                     .addReadOnlyAttributesItem("eindregistratie"))
1301             .putLayerSettingsItem(
1302                 "lyr:snapshot-geoserver:postgis:kadastraal_perceel",
1303                 new AppLayerSettings().editable(true).addReadOnlyAttributesItem("aanduiding"))
1304             .putLayerSettingsItem(
1305                 "lyr:snapshot-geoserver:sqlserver:wegdeel", new AppLayerSettings().editable(true))
1306             .putLayerSettingsItem(
1307                 "lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel",
1308                 new AppLayerSettings().editable(false))
1309             .putLayerSettingsItem(
1310                 "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1311                 new AppLayerSettings()
1312                     .hiddenFunctionality(Set.of(FEATURE_INFO, ATTRIBUTE_LIST, EXPORT)))
1313             .addFilterGroupsItem(new FilterGroup()
1314                 .id("filtergroup1")
1315                 .source("PRESET")
1316                 .type(FilterGroup.TypeEnum.ATTRIBUTE)
1317                 .layerIds(List.of("lyr:snapshot-geoserver:postgis:begroeidterreindeel"))
1318                 .operator(FilterGroup.OperatorEnum.AND)
1319                 .addFiltersItem(new Filter()
1320                     .id("filter1")
1321                     .type(Filter.TypeEnum.ATTRIBUTE)
1322                     .condition(Filter.ConditionEnum.BEFORE)
1323                     .addValueItem("2025-06-05")
1324                     .attribute("creationdate")
1325                     .attributeType(Filter.AttributeTypeEnum.DATE))
1326                 .addFiltersItem(new Filter()
1327                     .id("filter2")
1328                     .type(Filter.TypeEnum.ATTRIBUTE)
1329                     .condition(Filter.ConditionEnum.UNIQUE_VALUES)
1330                     .addValueItem("bodembedekkers")
1331                     .addValueItem("bosplantsoen")
1332                     .addValueItem("gras- en kruidachtigen")
1333                     .attribute("plus_fysiekvoorkomen")
1334                     .attributeType(Filter.AttributeTypeEnum.STRING)
1335                     .editConfiguration(new FilterEditConfiguration()
1336                         .filterTool(FilterEditConfiguration.FilterToolEnum.CHECKBOX)
1337                         .attributeValuesSettings(List.of(
1338                             new AttributeValueSettings()
1339                                 .value("bodembedekkers")
1340                                 .initiallySelected(true)
1341                                 .selectable(true)
1342                                 .alias("Bodembedekkers"),
1343                             new AttributeValueSettings()
1344                                 .value("bosplantsoen")
1345                                 .initiallySelected(true)
1346                                 .selectable(true)
1347                                 .alias("Bosplantsoen"),
1348                             new AttributeValueSettings()
1349                                 .value("gras- en kruidachtigen")
1350                                 .initiallySelected(true)
1351                                 .selectable(true)
1352                                 .alias("Gras- en kruidachtigen"),
1353                             new AttributeValueSettings()
1354                                 .value("griend en hakhout")
1355                                 .initiallySelected(false)
1356                                 .selectable(true),
1357                             new AttributeValueSettings()
1358                                 .value("heesters")
1359                                 .initiallySelected(false)
1360                                 .selectable(true),
1361                             new AttributeValueSettings()
1362                                 .value("planten")
1363                                 .initiallySelected(false)
1364                                 .selectable(true),
1365                             new AttributeValueSettings()
1366                                 .value("struikrozen")
1367                                 .initiallySelected(false)
1368                                 .selectable(true),
1369                             new AttributeValueSettings()
1370                                 .value("waardeOnbekend")
1371                                 .initiallySelected(false)
1372                                 .selectable(true))))))
1373             .addFilterGroupsItem(new FilterGroup()
1374                 .id("filtergroup2")
1375                 .source("PRESET")
1376                 .type(FilterGroup.TypeEnum.ATTRIBUTE)
1377                 .layerIds(List.of("lyr:snapshot-geoserver:postgis:kadastraal_perceel"))
1378                 .operator(FilterGroup.OperatorEnum.AND)
1379                 .addFiltersItem(new Filter()
1380                     .id("filter3")
1381                     .type(Filter.TypeEnum.ATTRIBUTE)
1382                     .condition(Filter.ConditionEnum.u)
1383                     .addValueItem("1")
1384                     .addValueItem("12419")
1385                     .attribute("perceelnummer")
1386                     .attributeType(Filter.AttributeTypeEnum.DOUBLE)
1387                     .editConfiguration(new FilterEditConfiguration()
1388                         .filterTool(FilterEditConfiguration.FilterToolEnum.SLIDER)
1389                         .initialLowerValue(1d)
1390                         .initialUpperValue(12419d)
1391                         .minimumValue(1d)
1392                         .maximumValue(12419d)))));
1393 
1394     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1395     app.setInitialExtent(
1396         new Bounds().minx(130011d).miny(458031d).maxx(132703d).maxy(459995d));
1397     app.setMaxExtent(new Bounds().minx(-285401d).miny(22598d).maxx(595401d).maxy(903401d));
1398 
1399     if (map5url != null) {
1400       AppTreeLevelNode root =
1401           (AppTreeLevelNode) app.getContentRoot().getBaseLayerNodes().getFirst();
1402       List<String> childrenIds = new ArrayList<>(root.getChildrenIds());
1403       childrenIds.add("lyr:map5:map5topo");
1404       childrenIds.add("lyr:map5:map5topo_simple");
1405       childrenIds.add("lvl:luchtfoto-labels");
1406       root.setChildrenIds(childrenIds);
1407       app.getSettings()
1408           .putLayerSettingsItem("lyr:map5:map5topo", new AppLayerSettings().title("Map5"))
1409           .putLayerSettingsItem("lyr:map5:map5topo_simple", new AppLayerSettings().title("Map5 simple"));
1410       app.getContentRoot()
1411           .addBaseLayerNodesItem(new AppTreeLayerNode()
1412               .objectType("AppTreeLayerNode")
1413               .id("lyr:map5:map5topo")
1414               .serviceId("map5")
1415               .layerName("map5topo")
1416               .visible(false))
1417           .addBaseLayerNodesItem(new AppTreeLayerNode()
1418               .objectType("AppTreeLayerNode")
1419               .id("lyr:map5:map5topo_simple")
1420               .serviceId("map5")
1421               .layerName("map5topo_simple")
1422               .visible(false))
1423           .addBaseLayerNodesItem(new AppTreeLevelNode()
1424               .objectType("AppTreeLevelNode")
1425               .id("lvl:luchtfoto-labels")
1426               .title("Luchtfoto met labels")
1427               .addChildrenIdsItem("lyr:map5:luforoadslabels")
1428               .addChildrenIdsItem("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR2"))
1429           .addBaseLayerNodesItem(new AppTreeLayerNode()
1430               .objectType("AppTreeLayerNode")
1431               .id("lyr:map5:luforoadslabels")
1432               .serviceId("map5")
1433               .layerName("luforoadslabels")
1434               .visible(false))
1435           .addBaseLayerNodesItem(new AppTreeLayerNode()
1436               .objectType("AppTreeLayerNode")
1437               .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR2")
1438               .serviceId("pdok-hwh-luchtfotorgb")
1439               .layerName("Actueel_orthoHR")
1440               .visible(false));
1441     }
1442 
1443     applicationRepository.save(app);
1444 
1445     app = new Application()
1446         .setName("base")
1447         .setTitle("Service base app")
1448         .setCrs("EPSG:28992")
1449         .setAuthorizationRules(ruleAnonymousRead)
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", "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR"))));
1458     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1459     applicationRepository.save(app);
1460 
1461     app = new Application()
1462         .setName("secured")
1463         .setTitle("secured app")
1464         .setCrs("EPSG:28992")
1465         .setAuthorizationRules(ruleLoggedIn)
1466         .setContentRoot(new AppContent()
1467             .addBaseLayerNodesItem(new AppTreeLevelNode()
1468                 .objectType("AppTreeLevelNode")
1469                 .id("root-base-layers")
1470                 .root(true)
1471                 .title("Base layers")
1472                 .childrenIds(List.of(
1473                     "lyr:openbasiskaart:osm",
1474                     "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR",
1475                     "lyr:openbasiskaart-proxied:osm")))
1476             .addBaseLayerNodesItem(new AppTreeLayerNode()
1477                 .objectType("AppTreeLayerNode")
1478                 .id("lyr:openbasiskaart-proxied:osm")
1479                 .serviceId("openbasiskaart-proxied")
1480                 .layerName("osm")
1481                 .visible(false))
1482             .addLayerNodesItem(new AppTreeLevelNode()
1483                 .objectType("AppTreeLevelNode")
1484                 .id("root")
1485                 .root(true)
1486                 .title("Layers")
1487                 .childrenIds(List.of(
1488                     "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1489                     "lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied",
1490                     "lvl:proxied")))
1491             .addLayerNodesItem(new AppTreeLayerNode()
1492                 .objectType("AppTreeLayerNode")
1493                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied")
1494                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1495                 .layerName("Gemeentegebied")
1496                 .visible(true))
1497             .addLayerNodesItem(new AppTreeLayerNode()
1498                 .objectType("AppTreeLayerNode")
1499                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied")
1500                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1501                 .layerName("Provinciegebied")
1502                 .visible(false))
1503             .addLayerNodesItem(new AppTreeLevelNode()
1504                 .objectType("AppTreeLevelNode")
1505                 .id("lvl:proxied")
1506                 .title("Proxied")
1507                 .childrenIds(List.of("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")))
1508             .addLayerNodesItem(new AppTreeLayerNode()
1509                 .objectType("AppTreeLayerNode")
1510                 .id("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")
1511                 .serviceId("snapshot-geoserver-proxied")
1512                 .layerName("postgis:begroeidterreindeel")
1513                 .visible(false)))
1514         .setSettings(new AppSettings()
1515             .putLayerSettingsItem(
1516                 "lyr:openbasiskaart-proxied:osm",
1517                 new AppLayerSettings().title("Openbasiskaart (proxied)")));
1518 
1519     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1520     applicationRepository.save(app);
1521 
1522     app = new Application()
1523         .setName("secured-auth")
1524         .setTitle("secured (with authorizations)")
1525         .setCrs("EPSG:28992")
1526         .setAuthorizationRules(List.of(
1527             new AuthorizationRule()
1528                 .groupName("test-foo")
1529                 .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW)),
1530             new AuthorizationRule()
1531                 .groupName("test-bar")
1532                 .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW))))
1533         .setContentRoot(new AppContent()
1534             .addLayerNodesItem(new AppTreeLevelNode()
1535                 .objectType("AppTreeLevelNode")
1536                 .id("root")
1537                 .root(true)
1538                 .title("Layers")
1539                 .childrenIds(List.of("lvl:needs-auth", "lvl:public")))
1540             .addLayerNodesItem(new AppTreeLevelNode()
1541                 .objectType("AppTreeLevelNode")
1542                 .id("lvl:public")
1543                 .title("Public")
1544                 .childrenIds(List.of("lyr:snapshot-geoserver:BGT")))
1545             .addLayerNodesItem(new AppTreeLevelNode()
1546                 .objectType("AppTreeLevelNode")
1547                 .id("lvl:needs-auth")
1548                 .title("Needs auth")
1549                 .childrenIds(List.of(
1550                     "lyr:filtered-snapshot-geoserver:BGT",
1551                     "lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")))
1552             .addLayerNodesItem(new AppTreeLayerNode()
1553                 .objectType("AppTreeLayerNode")
1554                 .id("lyr:filtered-snapshot-geoserver:BGT")
1555                 .serviceId("filtered-snapshot-geoserver")
1556                 .layerName("BGT")
1557                 .visible(true))
1558             .addLayerNodesItem(new AppTreeLayerNode()
1559                 .objectType("AppTreeLayerNode")
1560                 .id("lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")
1561                 .serviceId("filtered-snapshot-geoserver")
1562                 .layerName("postgis:begroeidterreindeel")
1563                 .visible(true))
1564             .addLayerNodesItem(new AppTreeLayerNode()
1565                 .objectType("AppTreeLayerNode")
1566                 .id("lyr:snapshot-geoserver:BGT")
1567                 .serviceId("snapshot-geoserver")
1568                 .layerName("BGT")
1569                 .visible(true)));
1570 
1571     applicationRepository.save(app);
1572 
1573     app = new Application()
1574         .setName("austria")
1575         .setCrs("EPSG:3857")
1576         .setAuthorizationRules(ruleAnonymousRead)
1577         .setTitle("Austria")
1578         .setInitialExtent(
1579             new Bounds().minx(987982d).miny(5799551d).maxx(1963423d).maxy(6320708d))
1580         .setMaxExtent(
1581             new Bounds().minx(206516d).miny(5095461d).maxx(3146930d).maxy(7096232d))
1582         .setContentRoot(new AppContent()
1583             .addBaseLayerNodesItem(new AppTreeLevelNode()
1584                 .objectType("AppTreeLevelNode")
1585                 .id("root-base-layers")
1586                 .root(true)
1587                 .title("Base layers")
1588                 .childrenIds(List.of(
1589                     "lyr:at-basemap:geolandbasemap",
1590                     "lyr:at-basemap:orthofoto",
1591                     "lvl:orthofoto-labels",
1592                     "lyr:osm:xyz")))
1593             .addBaseLayerNodesItem(new AppTreeLayerNode()
1594                 .objectType("AppTreeLayerNode")
1595                 .id("lyr:at-basemap:geolandbasemap")
1596                 .serviceId("at-basemap")
1597                 .layerName("geolandbasemap")
1598                 .visible(true))
1599             .addBaseLayerNodesItem(new AppTreeLayerNode()
1600                 .objectType("AppTreeLayerNode")
1601                 .id("lyr:at-basemap:orthofoto")
1602                 .serviceId("at-basemap")
1603                 .layerName("bmaporthofoto30cm")
1604                 .visible(false))
1605             .addBaseLayerNodesItem(new AppTreeLevelNode()
1606                 .objectType("AppTreeLevelNode")
1607                 .id("lvl:orthofoto-labels")
1608                 .title("Orthophoto with labels")
1609                 .childrenIds(List.of("lyr:at-basemap:bmapoverlay", "lyr:at-basemap:orthofoto_2")))
1610             .addBaseLayerNodesItem(new AppTreeLayerNode()
1611                 .objectType("AppTreeLayerNode")
1612                 .id("lyr:at-basemap:bmapoverlay")
1613                 .serviceId("at-basemap")
1614                 .layerName("bmapoverlay")
1615                 .visible(false))
1616             .addBaseLayerNodesItem(new AppTreeLayerNode()
1617                 .objectType("AppTreeLayerNode")
1618                 .id("lyr:at-basemap:orthofoto_2")
1619                 .serviceId("at-basemap")
1620                 .layerName("bmaporthofoto30cm")
1621                 .visible(false))
1622             .addBaseLayerNodesItem(new AppTreeLayerNode()
1623                 .objectType("AppTreeLayerNode")
1624                 .id("lyr:osm:xyz")
1625                 .serviceId("osm")
1626                 .layerName("xyz")
1627                 .visible(false)));
1628 
1629     applicationRepository.save(app);
1630 
1631     app = new Application()
1632         .setName("3d_utrecht")
1633         .setCrs("EPSG:3857")
1634         .setAuthorizationRules(ruleLoggedIn)
1635         .setTitle("3D Utrecht")
1636         .setInitialExtent(
1637             new Bounds().minx(558390d).miny(6818485d).maxx(566751d).maxy(6824036d))
1638         .setMaxExtent(
1639             new Bounds().minx(91467d).miny(6496479d).maxx(1037043d).maxy(7147453d))
1640         .setSettings(new AppSettings().uiSettings(new AppUiSettings().enable3D(true)))
1641         .setContentRoot(new AppContent()
1642             .addBaseLayerNodesItem(new AppTreeLevelNode()
1643                 .objectType("AppTreeLevelNode")
1644                 .id("root-base-layers")
1645                 .root(true)
1646                 .title("Base layers")
1647                 .childrenIds(List.of("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR", "lyr:osm:xyz")))
1648             .addBaseLayerNodesItem(new AppTreeLayerNode()
1649                 .objectType("AppTreeLayerNode")
1650                 .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR")
1651                 .serviceId("pdok-hwh-luchtfotorgb")
1652                 .layerName("Actueel_orthoHR")
1653                 .visible(true))
1654             .addBaseLayerNodesItem(new AppTreeLayerNode()
1655                 .objectType("AppTreeLayerNode")
1656                 .id("lyr:osm:xyz")
1657                 .serviceId("osm")
1658                 .layerName("xyz")
1659                 .visible(false))
1660             .addLayerNodesItem(new AppTreeLevelNode()
1661                 .objectType("AppTreeLevelNode")
1662                 .id("root")
1663                 .root(true)
1664                 .title("Layers")
1665                 .childrenIds(List.of(
1666                     "lyr:3dbag_utrecht:tiles3d",
1667                     "lyr:snapshot-geoserver:postgis:begroeidterreindeel",
1668                     "lyr:3d_basisvoorziening_gebouwen_proxy:tiles3d",
1669                     "lyr:3d_utrecht_proxied_auth:tiles3d")))
1670             .addLayerNodesItem(new AppTreeLayerNode()
1671                 .objectType("AppTreeLayerNode")
1672                 .id("lyr:3dbag_utrecht:tiles3d")
1673                 .serviceId("3dbag_utrecht")
1674                 .layerName("tiles3d")
1675                 .visible(true))
1676             .addLayerNodesItem(new AppTreeLayerNode()
1677                 .objectType("AppTreeLayerNode")
1678                 .id("lyr:3d_basisvoorziening_gebouwen_proxy:tiles3d")
1679                 .serviceId("3d_basisvoorziening_gebouwen_proxy")
1680                 .layerName("tiles3d")
1681                 .visible(false))
1682             .addLayerNodesItem(new AppTreeLayerNode()
1683                 .objectType("AppTreeLayerNode")
1684                 .id("lyr:3d_utrecht_proxied_auth:tiles3d")
1685                 .serviceId("3d_utrecht_proxied_auth")
1686                 .layerName("tiles3d")
1687                 .visible(false))
1688             .addLayerNodesItem(new AppTreeLayerNode()
1689                 .objectType("AppTreeLayerNode")
1690                 .id("lyr:snapshot-geoserver:postgis:begroeidterreindeel")
1691                 .serviceId("snapshot-geoserver")
1692                 .layerName("postgis:begroeidterreindeel")
1693                 .visible(true))
1694             .addTerrainLayerNodesItem(new AppTreeLevelNode()
1695                 .objectType("AppTreeLevelNode")
1696                 .id("root-terrain-layers")
1697                 .root(true)
1698                 .title("Terrain Layers")
1699                 .childrenIds(List.of("lyr:ahn_terrain_model:quantizedmesh")))
1700             .addTerrainLayerNodesItem(new AppTreeLayerNode()
1701                 .objectType("AppTreeLayerNode")
1702                 .id("lyr:ahn_terrain_model:quantizedmesh")
1703                 .serviceId("ahn_terrain_model")
1704                 .layerName("quantizedmesh")
1705                 .visible(false)));
1706 
1707     applicationRepository.save(app);
1708 
1709     app = new Application()
1710         .setName("public-with-auth")
1711         .setTitle("Public app with one restricted layer in a group")
1712         .setCrs("EPSG:28992")
1713         .setAuthorizationRules(ruleAnonymousRead)
1714         .setInitialExtent(
1715             new Bounds().minx(130011d).miny(458031d).maxx(132703d).maxy(459995d))
1716         .setMaxExtent(
1717             new Bounds().minx(-285401d).miny(22598d).maxx(595401d).maxy(903401d))
1718         .setStyling(new AppStyling().logo(logo.getId().toString()))
1719         .setContentRoot(new AppContent()
1720             .addBaseLayerNodesItem(new AppTreeLevelNode()
1721                 .objectType("AppTreeLevelNode")
1722                 .id("root")
1723                 .root(true)
1724                 .title("Basemaps")
1725                 .childrenIds(List.of("lyr:openbasiskaart:osm")))
1726             .addBaseLayerNodesItem(new AppTreeLayerNode()
1727                 .objectType("AppTreeLayerNode")
1728                 .id("lyr:openbasiskaart:osm")
1729                 .serviceId("openbasiskaart")
1730                 .layerName("osm")
1731                 .visible(true))
1732             .addLayerNodesItem(new AppTreeLevelNode()
1733                 .objectType("AppTreeLevelNode")
1734                 .id("root")
1735                 .root(true)
1736                 .title("Application layers")
1737                 .childrenIds(List.of(
1738                     "lyr:snapshot-geoserver:postgis:kadastraal_perceel", "xpfhl34VmghkU12nP9Jer")))
1739             .addLayerNodesItem(new AppTreeLayerNode()
1740                 .objectType("AppTreeLayerNode")
1741                 .id("lyr:snapshot-geoserver:postgis:kadastraal_perceel")
1742                 .serviceId("snapshot-geoserver")
1743                 .layerName("postgis:kadastraal_perceel")
1744                 .visible(true))
1745             .addLayerNodesItem(new AppTreeLevelNode()
1746                 .id("xpfhl34VmghkU12nP9Jer")
1747                 .root(false)
1748                 .title("restricted")
1749                 .objectType("AppTreeLevelNode")
1750                 .childrenIds(List.of("lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")))
1751             .addLayerNodesItem(new AppTreeLayerNode()
1752                 .objectType("AppTreeLayerNode")
1753                 .id("lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")
1754                 .visible(true)
1755                 .serviceId("filtered-snapshot-geoserver")
1756                 .layerName("postgis:begroeidterreindeel")))
1757         .setSettings(new AppSettings());
1758 
1759     applicationRepository.save(app);
1760 
1761     Configuration config = new Configuration();
1762     config.setKey(Configuration.DEFAULT_APP);
1763     config.setValue("default");
1764     configurationRepository.save(config);
1765     config = new Configuration();
1766     config.setKey(Configuration.DEFAULT_BASE_APP);
1767     config.setValue("base");
1768     configurationRepository.save(config);
1769   }
1770 
1771   private void createConfigurationTestData() throws JsonProcessingException {
1772     Configuration config = new Configuration();
1773     config.setKey("test");
1774     config.setAvailableForViewer(true);
1775     config.setValue("test value");
1776     config.setJsonValue(new ObjectMapper().readTree("{ \"someProperty\": 1, \"nestedObject\": { \"num\": 42 } }"));
1777     configurationRepository.save(config);
1778   }
1779 
1780   @Transactional
1781   public void createSolrIndex() throws Exception {
1782     if (connectToSpatialDbs) {
1783       // flush() the repo because we need to make sure feature type testdata is fully stored
1784       // before creating the Solr index (which requires access to the feature type settings)
1785       featureSourceRepository.flush();
1786 
1787       logger.info("Creating Solr index");
1788       @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
1789       final String solrUrl = "http://" + (connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "solr") + ":8983/solr/";
1790       this.solrService.setSolrUrl(solrUrl);
1791       SolrHelper solrHelper = new SolrHelper(this.solrService.getSolrClientForIndexing())
1792           .withBatchSize(solrBatchSize)
1793           .withGeometryValidationRule(solrGeometryValidationRule);
1794       GeoService geoService =
1795           geoServiceRepository.findById("snapshot-geoserver").orElseThrow();
1796       Application defaultApp = applicationRepository.findByName("default");
1797 
1798       TMFeatureType begroeidterreindeelFT = geoService.findFeatureTypeForLayer(
1799           geoService.findLayer("postgis:begroeidterreindeel"), featureSourceRepository);
1800 
1801       TMFeatureType wegdeelFT = geoService.findFeatureTypeForLayer(
1802           geoService.findLayer("sqlserver:wegdeel"), featureSourceRepository);
1803 
1804       TMFeatureType kadastraalPerceelFT = geoService.findFeatureTypeForLayer(
1805           geoService.findLayer("postgis:kadastraal_perceel"), featureSourceRepository);
1806 
1807       try (solrHelper) {
1808         SearchIndex begroeidterreindeelIndex = null;
1809         if (begroeidterreindeelFT != null) {
1810           begroeidterreindeelIndex = new SearchIndex()
1811               .setName("Begroeidterreindeel")
1812               .setFeatureTypeId(begroeidterreindeelFT.getId())
1813               .setSearchFieldsUsed(List.of("class", "plus_fysiekvoorkomen", "bronhouder"))
1814               .setSearchDisplayFieldsUsed(List.of("class", "plus_fysiekvoorkomen"));
1815           begroeidterreindeelIndex = searchIndexRepository.save(begroeidterreindeelIndex);
1816           begroeidterreindeelIndex = solrHelper.addFeatureTypeIndex(
1817               begroeidterreindeelIndex,
1818               begroeidterreindeelFT,
1819               featureSourceFactoryHelper,
1820               searchIndexRepository);
1821           begroeidterreindeelIndex = searchIndexRepository.save(begroeidterreindeelIndex);
1822         }
1823 
1824         SearchIndex kadastraalPerceelIndex = null;
1825         if (kadastraalPerceelFT != null) {
1826           kadastraalPerceelIndex = new SearchIndex()
1827               .setName("kadastraal_perceel")
1828               .setFeatureTypeId(kadastraalPerceelFT.getId())
1829               .setSearchFieldsUsed(List.of("aanduiding"))
1830               .setSearchDisplayFieldsUsed(List.of("aanduiding"));
1831           kadastraalPerceelIndex = searchIndexRepository.save(kadastraalPerceelIndex);
1832           kadastraalPerceelIndex = solrHelper.addFeatureTypeIndex(
1833               kadastraalPerceelIndex,
1834               kadastraalPerceelFT,
1835               featureSourceFactoryHelper,
1836               searchIndexRepository);
1837           kadastraalPerceelIndex = searchIndexRepository.save(kadastraalPerceelIndex);
1838         }
1839 
1840         SearchIndex wegdeelIndex = null;
1841         if (wegdeelFT != null) {
1842           wegdeelIndex = new SearchIndex()
1843               .setName("Wegdeel")
1844               .setFeatureTypeId(wegdeelFT.getId())
1845               .setSearchFieldsUsed(List.of(
1846                   "function_", "plus_fysiekvoorkomenwegdeel", "surfacematerial", "bronhouder"))
1847               .setSearchDisplayFieldsUsed(List.of("function_", "plus_fysiekvoorkomenwegdeel"));
1848           wegdeelIndex = searchIndexRepository.save(wegdeelIndex);
1849           wegdeelIndex = solrHelper.addFeatureTypeIndex(
1850               wegdeelIndex, wegdeelFT, featureSourceFactoryHelper, searchIndexRepository);
1851           wegdeelIndex = searchIndexRepository.save(wegdeelIndex);
1852         }
1853 
1854         featureSourceRepository
1855             .getByTitle("PostGIS")
1856             .flatMap(fs -> fs.getFeatureTypes().stream()
1857                 .filter(ft -> ft.getName().equals("bak"))
1858                 .findFirst())
1859             .ifPresent(ft -> {
1860               SearchIndex bak = new SearchIndex()
1861                   .setName("bak")
1862                   .setFeatureTypeId(ft.getId())
1863                   .setSearchFieldsUsed(List.of("gmlid", "identificatie", "plus_type"))
1864                   .setSearchDisplayFieldsUsed(List.of("gmlid", "plus_type", "bronhouder"));
1865               searchIndexRepository.save(bak);
1866               try {
1867                 bak = solrHelper.addFeatureTypeIndex(
1868                     bak, ft, featureSourceFactoryHelper, searchIndexRepository);
1869                 searchIndexRepository.save(bak);
1870               } catch (IOException | SolrServerException e) {
1871                 throw new RuntimeException(e);
1872               }
1873             });
1874 
1875         // creating a solr index of config this will/should fail because there is no primary key in
1876         // the FT
1877         featureSourceRepository
1878             .getByTitle("PostGIS OSM")
1879             .flatMap(fs -> fs.getFeatureTypes().stream()
1880                 .filter(ft -> ft.getName().equals("osm_roads"))
1881                 .findFirst())
1882             .ifPresent(ft -> {
1883               SearchIndex osm_no_pk = new SearchIndex()
1884                   .setName("osm_no_pk")
1885                   .setFeatureTypeId(ft.getId())
1886                   .setSearchFieldsUsed(List.of("landuse", "osm_id", "natural", "boundary"))
1887                   .setSearchDisplayFieldsUsed(
1888                       List.of("landuse", "osm_id", "natural", "amenity", "boundary"));
1889               searchIndexRepository.save(osm_no_pk);
1890             });
1891 
1892         AppTreeLayerNode begroeidTerreindeelLayerNode = defaultApp
1893             .getAllAppTreeLayerNode()
1894             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:postgis:begroeidterreindeel"))
1895             .findFirst()
1896             .orElse(null);
1897 
1898         if (begroeidTerreindeelLayerNode != null && begroeidterreindeelIndex != null) {
1899           defaultApp
1900               .getAppLayerSettings(begroeidTerreindeelLayerNode)
1901               .setSearchIndexId(begroeidterreindeelIndex.getId());
1902         }
1903 
1904         AppTreeLayerNode kadastraalPerceelLayerNode = defaultApp
1905             .getAllAppTreeLayerNode()
1906             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:postgis:kadastraal_perceel"))
1907             .findFirst()
1908             .orElse(null);
1909 
1910         if (kadastraalPerceelLayerNode != null && kadastraalPerceelIndex != null) {
1911           defaultApp
1912               .getAppLayerSettings(kadastraalPerceelLayerNode)
1913               .setSearchIndexId(kadastraalPerceelIndex.getId());
1914         }
1915 
1916         AppTreeLayerNode wegdeel = defaultApp
1917             .getAllAppTreeLayerNode()
1918             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:sqlserver:wegdeel"))
1919             .findFirst()
1920             .orElse(null);
1921 
1922         if (wegdeel != null && wegdeelIndex != null) {
1923           defaultApp.getAppLayerSettings(wegdeel).setSearchIndexId(wegdeelIndex.getId());
1924         }
1925 
1926         applicationRepository.save(defaultApp);
1927       }
1928     }
1929   }
1930 
1931   private void createSearchIndexTasks() {
1932     logger.info("Creating search index tasks");
1933     List.of("Begroeidterreindeel", "kadastraal_perceel")
1934         .forEach(name -> searchIndexRepository.findByName(name).ifPresent(index -> {
1935           index.setSchedule(new TaskSchedule()
1936               /* hour */
1937               .cronExpression("0 0 0/1 1/1 * ? *")
1938               // /* 15 min */
1939               // .cronExpression("0 0/15 * 1/1 * ? *")
1940               .description("Update Solr index \"" + name + "\" every hour"));
1941           try {
1942             final UUID uuid = taskManagerService.createTask(
1943                 IndexTask.class,
1944                 new TMJobDataMap(Map.of(
1945                     Task.TYPE_KEY,
1946                     TaskType.INDEX,
1947                     Task.DESCRIPTION_KEY,
1948                     index.getSchedule().getDescription(),
1949                     IndexTask.INDEX_KEY,
1950                     index.getId().toString(),
1951                     Task.PRIORITY_KEY,
1952                     10)),
1953                 index.getSchedule().getCronExpression());
1954 
1955             index.getSchedule().setUuid(uuid);
1956             searchIndexRepository.save(index);
1957 
1958             logger.info("Created task to update Solr index with key: {}", uuid);
1959           } catch (SchedulerException e) {
1960             logger.error("Error creating scheduled solr index task", e);
1961           }
1962         }));
1963   }
1964 
1965   private void createPages() throws IOException {
1966     Upload logo = new Upload()
1967         .setCategory(Upload.CATEGORY_PORTAL_IMAGE)
1968         .setFilename("gradient.svg")
1969         .setMimeType("image/svg+xml")
1970         .setContent(new ClassPathResource("test/gradient-logo.svg").getContentAsByteArray())
1971         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
1972     uploadRepository.save(logo);
1973 
1974     Page loggedIn = new Page();
1975     loggedIn.setAuthorizationRules(ruleLoggedIn);
1976     loggedIn.setName("loggedIn");
1977     loggedIn.setType("page");
1978     loggedIn.setContent("About Tailormap");
1979     loggedIn.setContent("""
1980 # About Tailormap
1981 This is a page for logged in users.
1982 """);
1983     pageRepository.save(loggedIn);
1984 
1985     Page about = new Page();
1986     about.setAuthorizationRules(ruleAnonymousRead);
1987     about.setName("about");
1988     about.setType("page");
1989     about.setContent("About Tailormap");
1990     about.setContent("""
1991 # About Tailormap
1992 
1993 This is a page about *Tailormap*. It doesn't say much yet.
1994 """);
1995     pageRepository.save(about);
1996 
1997     Page page = new Page();
1998     page.setAuthorizationRules(ruleAnonymousRead);
1999     page.setName("home");
2000     page.setType("page");
2001     page.setTitle("Tailormap - Home");
2002     page.setContent(
2003         """
2004 # Welcome to Tailormap!
2005 
2006 This page is only visible when you implement a frontend to display pages, or get it (including a simple CMS)
2007 from [B3Partners](https://www.b3partners.nl)!
2008 """);
2009     page.setClassName(null);
2010     page.setTiles(List.of(
2011         new PageTile()
2012             .authorizationRules(ruleAnonymousRead)
2013             .id(UUID.randomUUID().toString())
2014             .title("Default app")
2015             .tileType(PageTile.TileTypeEnum.APPLICATION)
2016             .applicationId(Optional.ofNullable(applicationRepository.findByName("default"))
2017                 .map(Application::getId)
2018                 .orElse(null))
2019             .image(logo.getId().toString())
2020             .content("*Default app* tile content")
2021             .filterRequireAuthorization(false)
2022             .openInNewWindow(false),
2023         new PageTile()
2024             .authorizationRules(ruleAnonymousRead)
2025             .id(UUID.randomUUID().toString())
2026             .title("Secured app")
2027             .tileType(PageTile.TileTypeEnum.APPLICATION)
2028             .applicationId(Optional.ofNullable(applicationRepository.findByName("secured"))
2029                 .map(Application::getId)
2030                 .orElse(null))
2031             .filterRequireAuthorization(true)
2032             .content("Secure app, only shown if user has authorization")
2033             .openInNewWindow(false),
2034         new PageTile()
2035             .authorizationRules(ruleAnonymousRead)
2036             .id(UUID.randomUUID().toString())
2037             .title("Secured app (unfiltered)")
2038             .tileType(PageTile.TileTypeEnum.APPLICATION)
2039             .applicationId(Optional.ofNullable(applicationRepository.findByName("secured"))
2040                 .map(Application::getId)
2041                 .orElse(null))
2042             .filterRequireAuthorization(false)
2043             .content("Secure app, tile shown to everyone")
2044             .openInNewWindow(false),
2045         new PageTile()
2046             .authorizationRules(ruleAnonymousRead)
2047             .id(UUID.randomUUID().toString())
2048             .title("About")
2049             .tileType(PageTile.TileTypeEnum.PAGE)
2050             .pageId(about.getId())
2051             .openInNewWindow(false),
2052         new PageTile()
2053             .authorizationRules(ruleAnonymousRead)
2054             .id(UUID.randomUUID().toString())
2055             .title("B3Partners")
2056             .tileType(PageTile.TileTypeEnum.URL)
2057             .url("https://www.b3partners.nl/")
2058             .openInNewWindow(true),
2059         new PageTile()
2060             .authorizationRules(ruleLoggedIn)
2061             .id(UUID.randomUUID().toString())
2062             .title("Github repository")
2063             .tileType(PageTile.TileTypeEnum.URL)
2064             .url("https://github.com/Tailormap/tailormap-viewer")
2065             .openInNewWindow(true),
2066         new PageTile()
2067             .authorizationRules(ruleAnonymousRead)
2068             .id(UUID.randomUUID().toString())
2069             .tileType(PageTile.TileTypeEnum.PAGE)
2070             .title("Secured page")
2071             .pageId(loggedIn.getId())
2072             .filterRequireAuthorization(true)
2073             .openInNewWindow(true),
2074         new PageTile()
2075             .authorizationRules(ruleAnonymousRead)
2076             .id(UUID.randomUUID().toString())
2077             .tileType(PageTile.TileTypeEnum.PAGE)
2078             .title("Secured page (unfiltered)")
2079             .pageId(loggedIn.getId())
2080             .filterRequireAuthorization(false)
2081             .openInNewWindow(true)));
2082     pageRepository.save(page);
2083 
2084     Configuration c = new Configuration();
2085     c.setKey(HOME_PAGE);
2086     c.setValue(page.getId().toString());
2087     configurationRepository.save(c);
2088 
2089     List<MenuItem> globalMenuItems = List.of(
2090         new MenuItem().pageId(about.getId()).label("About").openInNewWindow(false),
2091         new MenuItem()
2092             .label("B3Partners website")
2093             .url("https://www.b3partners.nl/")
2094             .openInNewWindow(true)
2095             .exclusiveOnPageId(about.getId()));
2096     c = new Configuration();
2097     c.setKey(PORTAL_MENU);
2098     c.setJsonValue(new ObjectMapper().valueToTree(globalMenuItems));
2099     configurationRepository.save(c);
2100   }
2101 
2102   private void insertTestDrawing() {
2103     // note that the drawing uuid is hardcoded and used in the DrawingControllerIntegrationTest
2104     try {
2105       this.jdbcClient
2106           .sql(
2107               """
2108 INSERT INTO data.drawing (id,name,description,domain_data,"access",created_by,created_at,updated_by,updated_at,srid,"version") VALUES
2109 ('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);
2110 """)
2111           .update();
2112 
2113       this.jdbcClient
2114           .sql(
2115               """
2116 INSERT INTO data.drawing_feature (drawing_id,id,geometry,properties) VALUES
2117 ('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),
2118 ('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),
2119 ('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)
2120 """)
2121           .update();
2122     } catch (Exception any) {
2123       logger.error("Error inserting test drawing in data schema, some tests may fail", any);
2124     }
2125   }
2126 
2127   private void insertDrawingStyle() throws IOException {
2128     Upload upload = new Upload()
2129         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
2130         .setMimeType("image/svg+xml")
2131         .setFilename("ISO_7001_PI_PF_007.svg")
2132         .setContent(new ClassPathResource("test/ISO_7001_PI_PF_007.svg").getContentAsByteArray())
2133         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2134     upload = uploadRepository.save(upload);
2135     UUID drinkwaterImageId = upload.getId();
2136 
2137     upload = new Upload()
2138         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
2139         .setMimeType("image/svg+xml")
2140         .setFilename("lichtpunt.svg")
2141         .setContent(new ClassPathResource("test/lichtpunt.svg").getContentAsByteArray())
2142         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2143     upload = uploadRepository.save(upload);
2144     UUID lichtpuntImageId = upload.getId();
2145 
2146     upload = new Upload()
2147         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
2148         .setMimeType("image/svg+xml")
2149         .setFilename("ISO_7010_E003_-_First_aid_sign.svg")
2150         .setContent(new ClassPathResource("test/ISO_7010_E003_-_First_aid_sign.svg").getContentAsByteArray())
2151         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2152     upload = uploadRepository.save(upload);
2153     UUID firstAidImageId = upload.getId();
2154 
2155     Properties props = new Properties();
2156     props.putAll(Map.of(
2157         "water-uuid", drinkwaterImageId.toString(),
2158         "lichtpunt-uuid", lichtpuntImageId.toString(),
2159         "first-aid-uuid", firstAidImageId.toString()));
2160 
2161     String drawingStyles =
2162         new ClassPathResource("test/object-drawing-style.json").getContentAsString(StandardCharsets.UTF_8);
2163     drawingStyles = new PropertyPlaceholderHelper("${", "}").replacePlaceholders(drawingStyles, props);
2164 
2165     // save the updated drawing style
2166     upload = new Upload()
2167         .setCategory(Upload.CATEGORY_DRAWING_STYLE)
2168         .setMimeType("application/json")
2169         .setFilename("object-drawing-style.json")
2170         .setContent(drawingStyles.getBytes(StandardCharsets.UTF_8))
2171         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2172     uploadRepository.save(upload);
2173   }
2174 }