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.time.OffsetDateTime;
26  import java.time.ZoneId;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.NoSuchElementException;
32  import java.util.Optional;
33  import java.util.Properties;
34  import java.util.Set;
35  import java.util.UUID;
36  import org.apache.solr.client.solrj.SolrServerException;
37  import org.quartz.SchedulerException;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  import org.springframework.beans.factory.annotation.Value;
41  import org.springframework.boot.SpringApplication;
42  import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
43  import org.springframework.boot.context.event.ApplicationReadyEvent;
44  import org.springframework.context.ApplicationContext;
45  import org.springframework.context.event.EventListener;
46  import org.springframework.core.io.ClassPathResource;
47  import org.springframework.jdbc.core.simple.JdbcClient;
48  import org.springframework.transaction.annotation.Transactional;
49  import org.springframework.util.PropertyPlaceholderHelper;
50  import org.tailormap.api.admin.model.TaskSchedule;
51  import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
52  import org.tailormap.api.geotools.featuresources.JDBCFeatureSourceHelper;
53  import org.tailormap.api.geotools.featuresources.WFSFeatureSourceHelper;
54  import org.tailormap.api.persistence.Application;
55  import org.tailormap.api.persistence.Catalog;
56  import org.tailormap.api.persistence.Configuration;
57  import org.tailormap.api.persistence.GeoService;
58  import org.tailormap.api.persistence.Group;
59  import org.tailormap.api.persistence.Page;
60  import org.tailormap.api.persistence.SearchIndex;
61  import org.tailormap.api.persistence.TMFeatureSource;
62  import org.tailormap.api.persistence.TMFeatureType;
63  import org.tailormap.api.persistence.Upload;
64  import org.tailormap.api.persistence.User;
65  import org.tailormap.api.persistence.helper.GeoServiceHelper;
66  import org.tailormap.api.persistence.json.AppContent;
67  import org.tailormap.api.persistence.json.AppLayerSettings;
68  import org.tailormap.api.persistence.json.AppSettings;
69  import org.tailormap.api.persistence.json.AppTreeLayerNode;
70  import org.tailormap.api.persistence.json.AppTreeLevelNode;
71  import org.tailormap.api.persistence.json.AppTreeNode;
72  import org.tailormap.api.persistence.json.AppUiSettings;
73  import org.tailormap.api.persistence.json.AttributeSettings;
74  import org.tailormap.api.persistence.json.AttributeValueSettings;
75  import org.tailormap.api.persistence.json.AuthorizationRule;
76  import org.tailormap.api.persistence.json.AuthorizationRuleDecision;
77  import org.tailormap.api.persistence.json.Bounds;
78  import org.tailormap.api.persistence.json.CatalogNode;
79  import org.tailormap.api.persistence.json.FeatureTypeRef;
80  import org.tailormap.api.persistence.json.FeatureTypeTemplate;
81  import org.tailormap.api.persistence.json.Filter;
82  import org.tailormap.api.persistence.json.FilterEditConfiguration;
83  import org.tailormap.api.persistence.json.FilterGroup;
84  import org.tailormap.api.persistence.json.GeoServiceDefaultLayerSettings;
85  import org.tailormap.api.persistence.json.GeoServiceLayerSettings;
86  import org.tailormap.api.persistence.json.GeoServiceSettings;
87  import org.tailormap.api.persistence.json.JDBCConnectionProperties;
88  import org.tailormap.api.persistence.json.MenuItem;
89  import org.tailormap.api.persistence.json.PageTile;
90  import org.tailormap.api.persistence.json.ServiceAuthentication;
91  import org.tailormap.api.persistence.json.TailormapObjectRef;
92  import org.tailormap.api.persistence.json.TileLayerHiDpiMode;
93  import org.tailormap.api.repository.ApplicationRepository;
94  import org.tailormap.api.repository.CatalogRepository;
95  import org.tailormap.api.repository.ConfigurationRepository;
96  import org.tailormap.api.repository.FeatureSourceRepository;
97  import org.tailormap.api.repository.GeoServiceRepository;
98  import org.tailormap.api.repository.GroupRepository;
99  import org.tailormap.api.repository.PageRepository;
100 import org.tailormap.api.repository.SearchIndexRepository;
101 import org.tailormap.api.repository.UploadRepository;
102 import org.tailormap.api.repository.UserRepository;
103 import org.tailormap.api.scheduling.FailingPocTask;
104 import org.tailormap.api.scheduling.IndexTask;
105 import org.tailormap.api.scheduling.InterruptablePocTask;
106 import org.tailormap.api.scheduling.PocTask;
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("tasks")) {
232         createScheduledTasks();
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     // Superuser with all access
295     u = new User().setUsername("tm-admin").setPassword(adminHashedPassword);
296     u.addOrUpdateAdminProperty("some-property", "some-value", true);
297     u.addOrUpdateAdminProperty("admin-property", "private-value", false);
298     u.getGroups().add(groupRepository.findById(Group.ADMIN).orElseThrow());
299     u.getGroups().add(groupBar);
300     userRepository.save(u);
301   }
302 
303   private final List<AuthorizationRule> ruleAnonymousRead = List.of(new AuthorizationRule()
304       .groupName(Group.ANONYMOUS)
305       .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW)));
306 
307   private final List<AuthorizationRule> ruleLoggedIn = List.of(new AuthorizationRule()
308       .groupName(Group.AUTHENTICATED)
309       .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW)));
310 
311   @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
312   private void createCatalogTestData() throws Exception {
313     Catalog catalog = catalogRepository.findById(Catalog.MAIN).orElseThrow();
314     CatalogNode rootCatalogNode = catalog.getNodes().get(0);
315     CatalogNode catalogNode = new CatalogNode().id("test").title("Test services");
316     rootCatalogNode.addChildrenItem(catalogNode.getId());
317     catalog.getNodes().add(catalogNode);
318 
319     String osmAttribution = "© [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors";
320 
321     Bounds rdTileGridExtent =
322         new Bounds().minx(-285401.92).maxx(595401.92).miny(22598.08).maxy(903401.92);
323 
324     Upload legend = new Upload()
325         .setCategory(Upload.CATEGORY_LEGEND)
326         .setFilename("gemeentegebied-legend.png")
327         .setMimeType("image/png")
328         .setContent(new ClassPathResource("test/gemeentegebied-legend.png").getContentAsByteArray())
329         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
330     uploadRepository.save(legend);
331 
332     Collection<GeoService> services = List.of(
333         new GeoService()
334             .setId("demo")
335             .setProtocol(WMS)
336             .setTitle("Demo")
337             .setPublished(true)
338             .setAuthorizationRules(ruleAnonymousRead)
339             .setUrl("https://demo.tailormap.com/geoserver/geodata/ows?SERVICE=WMS"),
340         new GeoService()
341             .setId("osm")
342             .setProtocol(XYZ)
343             .setTitle("OSM")
344             .setUrl("https://tile.openstreetmap.org/{z}/{x}/{y}.png")
345             .setAuthorizationRules(ruleAnonymousRead)
346             .setSettings(new GeoServiceSettings()
347                 .xyzCrs("EPSG:3857")
348                 .layerSettings(Map.of(
349                     "xyz",
350                     new GeoServiceLayerSettings()
351                         .attribution(osmAttribution)
352                         .maxZoom(19)))),
353         // Layer settings configured later, using the same settings for this one and proxied one
354         new GeoService()
355             .setId("snapshot-geoserver")
356             .setProtocol(WMS)
357             .setTitle("Test GeoServer")
358             .setUrl("https://snapshot.tailormap.nl/geoserver/wms")
359             .setAuthorizationRules(ruleAnonymousRead)
360             .setPublished(true),
361         new GeoService()
362             .setId("filtered-snapshot-geoserver")
363             .setProtocol(WMS)
364             .setTitle("Test GeoServer (with authorization rules)")
365             .setUrl("https://snapshot.tailormap.nl/geoserver/wms")
366             .setAuthorizationRules(List.of(
367                 new AuthorizationRule()
368                     .groupName("test-foo")
369                     .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW)),
370                 new AuthorizationRule()
371                     .groupName("test-baz")
372                     .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW))))
373             .setSettings(new GeoServiceSettings()
374                 .layerSettings(Map.of(
375                     "BGT",
376                     new GeoServiceLayerSettings()
377                         .addAuthorizationRulesItem(new AuthorizationRule()
378                             .groupName("test-foo")
379                             .decisions(Map.of(
380                                 ACCESS_TYPE_VIEW, AuthorizationRuleDecision.DENY)))
381                         .addAuthorizationRulesItem(new AuthorizationRule()
382                             .groupName("test-baz")
383                             .decisions(Map.of(
384                                 ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW))))))
385             .setPublished(true),
386         new GeoService()
387             .setId("snapshot-geoserver-proxied")
388             .setProtocol(WMS)
389             .setTitle("Test GeoServer (proxied)")
390             .setUrl("https://snapshot.tailormap.nl/geoserver/wms")
391             .setAuthorizationRules(ruleAnonymousRead)
392             .setSettings(new GeoServiceSettings().useProxy(true)),
393         new GeoService()
394             .setId("openbasiskaart")
395             .setProtocol(WMTS)
396             .setTitle("Openbasiskaart")
397             .setUrl("https://www.openbasiskaart.nl/mapcache/wmts")
398             .setAuthorizationRules(ruleAnonymousRead)
399             .setSettings(new GeoServiceSettings()
400                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings().attribution(osmAttribution))
401                 .layerSettings(Map.of(
402                     "osm",
403                     new GeoServiceLayerSettings()
404                         .title("Openbasiskaart")
405                         .hiDpiDisabled(false)
406                         .hiDpiMode(TileLayerHiDpiMode.SUBSTITUTELAYERSHOWNEXTZOOMLEVEL)
407                         .hiDpiSubstituteLayer("osm-hq")))),
408         new GeoService()
409             .setId("openbasiskaart-proxied")
410             .setProtocol(WMTS)
411             .setTitle("Openbasiskaart (proxied)")
412             .setUrl("https://www.openbasiskaart.nl/mapcache/wmts")
413             .setAuthorizationRules(ruleAnonymousRead)
414             // The service actually doesn't require authentication, but also doesn't mind it
415             // Just for testing
416             .setAuthentication(new ServiceAuthentication()
417                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
418                 .username("test")
419                 .password("test"))
420             .setSettings(new GeoServiceSettings()
421                 .useProxy(true)
422                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings().attribution(osmAttribution))
423                 .layerSettings(Map.of(
424                     "osm",
425                     new GeoServiceLayerSettings()
426                         .hiDpiDisabled(false)
427                         .hiDpiMode(TileLayerHiDpiMode.SUBSTITUTELAYERSHOWNEXTZOOMLEVEL)
428                         .hiDpiSubstituteLayer("osm-hq")))),
429         new GeoService()
430             .setId("openbasiskaart-tms")
431             .setProtocol(XYZ)
432             .setTitle("Openbasiskaart (TMS)")
433             .setUrl("https://openbasiskaart.nl/mapcache/tms/1.0.0/osm@rd/{z}/{x}/{-y}.png")
434             .setAuthorizationRules(ruleAnonymousRead)
435             .setSettings(
436                 new GeoServiceSettings()
437                     .xyzCrs("EPSG:28992")
438                     .defaultLayerSettings(
439                         new GeoServiceDefaultLayerSettings().attribution(osmAttribution))
440                     .layerSettings(
441                         Map.of(
442                             "xyz",
443                             new GeoServiceLayerSettings()
444                                 .maxZoom(15)
445                                 .tileGridExtent(rdTileGridExtent)
446                                 .hiDpiDisabled(false)
447                                 .hiDpiMode(
448                                     TileLayerHiDpiMode
449                                         .SUBSTITUTELAYERTILEPIXELRATIOONLY)
450                                 .hiDpiSubstituteLayer(
451                                     "https://openbasiskaart.nl/mapcache/tms/1.0.0/osm-hq@rd-hq/{z}/{x}/{-y}.png")))),
452         new GeoService()
453             .setId("pdok-hwh-luchtfotorgb")
454             .setProtocol(WMTS)
455             .setTitle("PDOK HWH luchtfoto")
456             .setUrl("https://service.pdok.nl/hwh/luchtfotorgb/wmts/v1_0")
457             .setAuthorizationRules(ruleAnonymousRead)
458             .setPublished(true)
459             .setSettings(new GeoServiceSettings()
460                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings()
461                     .attribution("© [Beeldmateriaal.nl](https://beeldmateriaal.nl)")
462                     .hiDpiDisabled(false))
463                 .putLayerSettingsItem(
464                     "Actueel_orthoHR", new GeoServiceLayerSettings().title("Luchtfoto"))),
465         new GeoService()
466             .setId("b3p-mapproxy-luchtfoto")
467             .setProtocol(XYZ)
468             .setTitle("Luchtfoto (TMS)")
469             .setUrl("https://mapproxy.b3p.nl/tms/1.0.0/luchtfoto/EPSG28992/{z}/{x}/{-y}.jpeg")
470             .setAuthorizationRules(ruleAnonymousRead)
471             .setPublished(true)
472             .setSettings(new GeoServiceSettings()
473                 .xyzCrs("EPSG:28992")
474                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings()
475                     .attribution("© [Beeldmateriaal.nl](https://beeldmateriaal.nl)")
476                     .hiDpiDisabled(false))
477                 .layerSettings(Map.of(
478                     "xyz",
479                     new GeoServiceLayerSettings()
480                         .maxZoom(14)
481                         .tileGridExtent(rdTileGridExtent)
482                         .hiDpiMode(TileLayerHiDpiMode.SHOWNEXTZOOMLEVEL)))),
483         new GeoService()
484             .setId("at-basemap")
485             .setProtocol(WMTS)
486             .setTitle("basemap.at")
487             .setUrl("https://mapsneu.wien.gv.at/basemapneu/1.0.0/WMTSCapabilities.xml")
488             .setAuthorizationRules(ruleAnonymousRead)
489             .setPublished(true)
490             .setSettings(new GeoServiceSettings()
491                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings()
492                     .attribution("© [basemap.at](https://basemap.at)")
493                     .hiDpiDisabled(true))
494                 .layerSettings(Map.of(
495                     "geolandbasemap",
496                     new GeoServiceLayerSettings()
497                         .title("Basemap")
498                         .hiDpiDisabled(false)
499                         .hiDpiMode(TileLayerHiDpiMode.SUBSTITUTELAYERTILEPIXELRATIOONLY)
500                         .hiDpiSubstituteLayer("bmaphidpi"),
501                     "bmaporthofoto30cm",
502                     new GeoServiceLayerSettings()
503                         .title("Orthophoto")
504                         .hiDpiDisabled(false)))),
505         new GeoService()
506             .setId("pdok-kadaster-bestuurlijkegebieden")
507             .setProtocol(WMS)
508             .setUrl("https://service.pdok.nl/kadaster/bestuurlijkegebieden/wms/v1_0?service=WMS")
509             .setAuthorizationRules(ruleAnonymousRead)
510             .setSettings(new GeoServiceSettings()
511                 .defaultLayerSettings(new GeoServiceDefaultLayerSettings()
512                     .description("This layer shows an administrative boundary."))
513                 // No attribution required: service is CC0
514                 .serverType(GeoServiceSettings.ServerTypeEnum.MAPSERVER)
515                 .useProxy(true)
516                 .putLayerSettingsItem(
517                     "Gemeentegebied",
518                     new GeoServiceLayerSettings()
519                         .legendImageId(legend.getId().toString())))
520             .setPublished(true)
521             .setTitle("PDOK Kadaster bestuurlijke gebieden"),
522         new GeoService()
523             .setId("bestuurlijkegebieden-proxied")
524             .setProtocol(WMS)
525             .setUrl("https://service.pdok.nl/kadaster/bestuurlijkegebieden/wms/v1_0?service=WMS")
526             .setAuthorizationRules(ruleAnonymousRead)
527             // The service actually doesn't require authentication, but also doesn't mind it
528             // Just for testing that proxied services with auth are not available in public
529             // apps (even when logged in), in any controllers (map, proxy, features)
530             .setAuthentication(new ServiceAuthentication()
531                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
532                 .username("test")
533                 .password("test"))
534             .setSettings(new GeoServiceSettings()
535                 // No attribution required: service is CC0
536                 .serverType(GeoServiceSettings.ServerTypeEnum.MAPSERVER)
537                 .useProxy(true))
538             .setPublished(true)
539             .setTitle("Bestuurlijke gebieden (proxied met auth)"),
540         new GeoService()
541             .setId("3dbag_utrecht")
542             .setProtocol(TILES3D)
543             .setUrl("https://3dtilesnederland.nl/tiles/1.0/implicit/nederland/344.json")
544             .setTitle("3D BAG Utrecht")
545             .setPublished(true)
546             .setAuthorizationRules(ruleAnonymousRead),
547         new GeoService()
548             .setId("ahn_terrain_model")
549             .setProtocol(QUANTIZEDMESH)
550             .setUrl(
551                 "https://api.pdok.nl/kadaster/3d-basisvoorziening/ogc/v1/collections/digitaalterreinmodel")
552             .setTitle("AHN Terrain Model")
553             .setPublished(true)
554             .setAuthorizationRules(ruleAnonymousRead),
555         new GeoService()
556             .setId("3d_basisvoorziening_gebouwen_proxy")
557             .setProtocol(TILES3D)
558             .setUrl(
559                 "https://api.pdok.nl/kadaster/3d-basisvoorziening/ogc/v1_0/collections/gebouwen/3dtiles")
560             .setTitle("3D Basisvoorziening Gebouwen Proxy")
561             .setPublished(true)
562             .setAuthorizationRules(ruleAnonymousRead)
563             .setSettings(new GeoServiceSettings().useProxy(true)),
564         new GeoService()
565             .setId("3d_utrecht_proxied_auth")
566             .setProtocol(TILES3D)
567             .setUrl("https://snapshot.tailormap.nl/tiles3d-proxy/3dtiles")
568             .setTitle("3D Utrecht Proxied with Authorization")
569             .setPublished(true)
570             .setAuthorizationRules(ruleAnonymousRead)
571             .setAuthentication(new ServiceAuthentication()
572                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
573                 .username(tiles3dProxyUsername)
574                 .password(tiles3dProxyPassword))
575             .setSettings(new GeoServiceSettings().useProxy(true))
576         // TODO MapServer WMS "https://wms.geonorge.no/skwms1/wms.adm_enheter_historisk"
577         );
578 
579     if (map5url != null) {
580       GeoServiceLayerSettings osmAttr = new GeoServiceLayerSettings().attribution(osmAttribution);
581       GeoServiceLayerSettings map5Attr = new GeoServiceLayerSettings()
582           .attribution("Kaarten: [Map5.nl](https://map5.nl), data: " + osmAttribution);
583       services = new ArrayList<>(services);
584       services.add(new GeoService()
585           .setId("map5")
586           .setProtocol(WMTS)
587           .setTitle("Map5")
588           .setUrl(map5url)
589           .setAuthorizationRules(ruleAnonymousRead)
590           .setSettings(new GeoServiceSettings()
591               .defaultLayerSettings(new GeoServiceDefaultLayerSettings().hiDpiDisabled(true))
592               .layerSettings(Map.of(
593                   "openlufo",
594                   new GeoServiceLayerSettings()
595                       .attribution("© [Beeldmateriaal.nl](https://beeldmateriaal.nl), "
596                           + osmAttribution),
597                   "luforoadslabels",
598                   osmAttr,
599                   "map5topo",
600                   new GeoServiceLayerSettings()
601                       .attribution(map5Attr.getAttribution())
602                       .hiDpiDisabled(false)
603                       .hiDpiMode(TileLayerHiDpiMode.SUBSTITUTELAYERSHOWNEXTZOOMLEVEL)
604                       .hiDpiSubstituteLayer("map5topo_hq"),
605                   "map5topo_gray",
606                   map5Attr,
607                   "map5topo_simple",
608                   map5Attr,
609                   "map5topo_simple_gray",
610                   map5Attr,
611                   "opensimpletopo",
612                   osmAttr,
613                   "opensimpletopo_gray",
614                   osmAttr,
615                   "opentopo",
616                   osmAttr,
617                   "opentopo_gray",
618                   osmAttr))));
619     }
620 
621     for (GeoService geoService : services) {
622       try {
623         geoServiceHelper.loadServiceCapabilities(geoService);
624       } catch (Exception e) {
625         logger.error(
626             "Error loading capabilities for {} service URL {}: {}, {}",
627             geoService.getProtocol().getValue(),
628             geoService.getUrl(),
629             e.getClass(),
630             e.getMessage());
631       }
632 
633       geoServiceRepository.save(geoService);
634       catalogNode.addItemsItem(new TailormapObjectRef()
635           .kind(TailormapObjectRef.KindEnum.GEO_SERVICE)
636           .id(geoService.getId()));
637     }
638 
639     CatalogNode wfsFeatureSourceCatalogNode =
640         new CatalogNode().id("wfs_feature_sources").title("WFS feature sources");
641     rootCatalogNode.addChildrenItem(wfsFeatureSourceCatalogNode.getId());
642     catalog.getNodes().add(wfsFeatureSourceCatalogNode);
643 
644     services.stream().filter(s -> s.getProtocol() == WMS).forEach(s -> {
645       geoServiceHelper.findAndSaveRelatedWFS(s);
646       List<TMFeatureSource> linkedSources = featureSourceRepository.findByLinkedServiceId(s.getId());
647       for (TMFeatureSource linkedSource : linkedSources) {
648         wfsFeatureSourceCatalogNode.addItemsItem(new TailormapObjectRef()
649             .kind(TailormapObjectRef.KindEnum.FEATURE_SOURCE)
650             .id(linkedSource.getId().toString()));
651       }
652     });
653 
654     String geodataPassword = "980f1c8A-25933b2";
655 
656     Map<String, TMFeatureSource> featureSources = Map.of(
657         "postgis",
658         new TMFeatureSource()
659             .setProtocol(TMFeatureSource.Protocol.JDBC)
660             .setTitle("PostGIS")
661             .setJdbcConnection(new JDBCConnectionProperties()
662                 .dbtype(JDBCConnectionProperties.DbtypeEnum.POSTGIS)
663                 .host(connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "postgis")
664                 .port(connectToSpatialDbsAtLocalhost ? 54322 : 5432)
665                 .database("geodata")
666                 .schema("public")
667                 .additionalProperties(Map.of("connectionOptions", "?ApplicationName=tailormap-api")))
668             .setAuthentication(new ServiceAuthentication()
669                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
670                 .username("geodata")
671                 .password(geodataPassword)),
672         "postgis_osm",
673         new TMFeatureSource()
674             .setProtocol(TMFeatureSource.Protocol.JDBC)
675             .setTitle("PostGIS OSM")
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("osm")
682                 .additionalProperties(Map.of("connectionOptions", "?ApplicationName=tailormap-api")))
683             .setAuthentication(new ServiceAuthentication()
684                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
685                 .username("geodata")
686                 .password(geodataPassword)),
687         "oracle",
688         new TMFeatureSource()
689             .setProtocol(TMFeatureSource.Protocol.JDBC)
690             .setTitle("Oracle")
691             .setJdbcConnection(new JDBCConnectionProperties()
692                 .dbtype(JDBCConnectionProperties.DbtypeEnum.ORACLE)
693                 .host(connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "oracle")
694                 .database("/FREEPDB1")
695                 .schema("GEODATA")
696                 .additionalProperties(Map.of("connectionOptions", "?oracle.jdbc.J2EE13Compliant=true")))
697             .setAuthentication(new ServiceAuthentication()
698                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
699                 .username("geodata")
700                 .password(geodataPassword)),
701         "sqlserver",
702         new TMFeatureSource()
703             .setProtocol(TMFeatureSource.Protocol.JDBC)
704             .setTitle("MS SQL Server")
705             .setJdbcConnection(new JDBCConnectionProperties()
706                 .dbtype(JDBCConnectionProperties.DbtypeEnum.SQLSERVER)
707                 .host(connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "sqlserver")
708                 .database("geodata")
709                 .schema("dbo")
710                 .additionalProperties(Map.of("connectionOptions", ";encrypt=false")))
711             .setAuthentication(new ServiceAuthentication()
712                 .method(ServiceAuthentication.MethodEnum.PASSWORD)
713                 .username("geodata")
714                 .password(geodataPassword)),
715         "pdok-kadaster-bestuurlijkegebieden",
716         new TMFeatureSource()
717             .setProtocol(TMFeatureSource.Protocol.WFS)
718             .setUrl(
719                 "https://service.pdok.nl/kadaster/bestuurlijkegebieden/wfs/v1_0?service=WFS&VERSION=2.0.0")
720             .setTitle("Bestuurlijke gebieden")
721             .setNotes(
722                 "Overzicht van de bestuurlijke indeling van Nederland in gemeenten en provincies alsmede de rijksgrens. Gegevens zijn afgeleid uit de Basisregistratie Kadaster (BRK)."));
723     featureSourceRepository.saveAll(featureSources.values());
724 
725     new WFSFeatureSourceHelper().loadCapabilities(featureSources.get("pdok-kadaster-bestuurlijkegebieden"));
726     geoServiceRepository.findById("pdok-kadaster-bestuurlijkegebieden").ifPresent(geoService -> {
727       geoService
728           .getSettings()
729           .getLayerSettings()
730           .put(
731               "Provinciegebied",
732               new GeoServiceLayerSettings()
733                   .description("The administrative boundary of Dutch Provinces, connected to a WFS.")
734                   .featureType(new FeatureTypeRef()
735                       .featureSourceId(featureSources
736                           .get("pdok-kadaster-bestuurlijkegebieden")
737                           .getId())
738                       .featureTypeName("bestuurlijkegebieden:Provinciegebied"))
739                   .title("Provinciegebied (WFS)"));
740       geoServiceRepository.save(geoService);
741     });
742 
743     geoServiceRepository.findById("bestuurlijkegebieden-proxied").ifPresent(geoService -> {
744       geoService
745           .getSettings()
746           .getLayerSettings()
747           .put(
748               "Provinciegebied",
749               new GeoServiceLayerSettings()
750                   .featureType(new FeatureTypeRef()
751                       .featureSourceId(featureSources
752                           .get("pdok-kadaster-bestuurlijkegebieden")
753                           .getId())
754                       .featureTypeName("bestuurlijkegebieden:Provinciegebied"))
755                   .title("Provinciegebied (WFS, proxied met auth)"));
756       geoServiceRepository.save(geoService);
757     });
758 
759     CatalogNode featureSourceCatalogNode =
760         new CatalogNode().id("feature_sources").title("Test feature sources");
761     rootCatalogNode.addChildrenItem(featureSourceCatalogNode.getId());
762     catalog.getNodes().add(featureSourceCatalogNode);
763 
764     for (TMFeatureSource featureSource : featureSources.values()) {
765       featureSourceCatalogNode.addItemsItem(new TailormapObjectRef()
766           .kind(TailormapObjectRef.KindEnum.FEATURE_SOURCE)
767           .id(featureSource.getId().toString()));
768     }
769     catalogRepository.save(catalog);
770 
771     if (connectToSpatialDbs) {
772       featureSources.values().forEach(fs -> {
773         try {
774           if (fs.getProtocol() == TMFeatureSource.Protocol.JDBC) {
775             new JDBCFeatureSourceHelper().loadCapabilities(fs);
776           } else if (fs.getProtocol() == TMFeatureSource.Protocol.WFS) {
777             new WFSFeatureSourceHelper().loadCapabilities(fs);
778           }
779         } catch (Exception e) {
780           logger.error("Error loading capabilities for feature source {}", fs.getTitle(), e);
781         }
782       });
783 
784       services.stream()
785           // Set layer settings for both the proxied and non-proxied one, but don't overwrite the
786           // authorization rules for the "filtered-snapshot-geoserver" service
787           .filter(s -> s.getId().startsWith("snapshot-geoserver"))
788           .forEach(s -> s.getSettings()
789               .layerSettings(Map.of(
790                   "postgis:begroeidterreindeel",
791                   new GeoServiceLayerSettings()
792                       .description(
793                           """
794 This layer shows data from https://www.postgis.net/
795 
796 https://postgis.net/brand.svg""")
797                       .featureType(new FeatureTypeRef()
798                           .featureSourceId(featureSources
799                               .get("postgis")
800                               .getId())
801                           .featureTypeName("begroeidterreindeel")),
802                   "postgis:bak",
803                   new GeoServiceLayerSettings()
804                       .featureType(new FeatureTypeRef()
805                           .featureSourceId(featureSources
806                               .get("postgis")
807                               .getId())
808                           .featureTypeName("bak")),
809                   "postgis:kadastraal_perceel",
810                   new GeoServiceLayerSettings()
811                       .description("cadastral parcel label points")
812                       .featureType(new FeatureTypeRef()
813                           .featureSourceId(featureSources
814                               .get("postgis")
815                               .getId())
816                           .featureTypeName("kadastraal_perceel")),
817                   "sqlserver:wegdeel",
818                   new GeoServiceLayerSettings()
819                       .attribution(
820                           "CC BY 4.0 [BGT/Kadaster](https://www.nationaalgeoregister.nl/geonetwork/srv/api/records/2cb4769c-b56e-48fa-8685-c48f61b9a319)")
821                       .description(
822                           """
823 This layer shows data from [MS SQL Server](https://learn.microsoft.com/en-us/sql/relational-databases/spatial/spatial-data-sql-server).
824 
825 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""")
826                       .featureType(new FeatureTypeRef()
827                           .featureSourceId(featureSources
828                               .get("sqlserver")
829                               .getId())
830                           .featureTypeName("wegdeel")),
831                   "oracle:WATERDEEL",
832                   new GeoServiceLayerSettings()
833                       .description("This layer shows data from Oracle Spatial.")
834                       .featureType(new FeatureTypeRef()
835                           .featureSourceId(featureSources
836                               .get("oracle")
837                               .getId())
838                           .featureTypeName("WATERDEEL")),
839                   "postgis:osm_polygon",
840                   new GeoServiceLayerSettings()
841                       .description("This layer shows OSM data from postgis.")
842                       .featureType(new FeatureTypeRef()
843                           .featureSourceId(featureSources
844                               .get("postgis_osm")
845                               .getId())
846                           .featureTypeName("osm_polygon")))));
847     }
848 
849     featureSources.get("pdok-kadaster-bestuurlijkegebieden").getFeatureTypes().stream()
850         .filter(ft -> ft.getName().equals("bestuurlijkegebieden:Provinciegebied"))
851         .findFirst()
852         .ifPresent(ft -> {
853           ft.getSettings().addHideAttributesItem("identificatie");
854           ft.getSettings().addHideAttributesItem("ligtInLandCode");
855           ft.getSettings().addHideAttributesItem("fuuid");
856           ft.getSettings().putAttributeSettingsItem("naam", new AttributeSettings().title("Naam"));
857           ft.getSettings()
858               .setTemplate(
859                   new FeatureTypeTemplate()
860                       .templateLanguage("simple")
861                       .markupLanguage("markdown")
862                       .template(
863                           """
864 ### Provincie
865 Deze provincie heet **{{naam}}** en ligt in _{{ligtInLandNaam}}_.
866 
867 | Attribuut | Waarde             |
868 | --------- | ------------------ |
869 | `code`    | {{code}}           |
870 | `naam`    | {{naam}}           |
871 | `ligt in` | {{ligtInLandNaam}} |"""));
872         });
873 
874     featureSources.get("postgis").getFeatureTypes().stream()
875         .filter(ft -> ft.getName().equals("begroeidterreindeel"))
876         .findFirst()
877         .ifPresent(ft -> {
878           ft.getSettings().addHideAttributesItem("terminationdate");
879           ft.getSettings().addHideAttributesItem("geom_kruinlijn");
880           ft.getSettings().putAttributeSettingsItem("gmlid", new AttributeSettings().title("GML ID"));
881           ft.getSettings()
882               .putAttributeSettingsItem("identificatie", new AttributeSettings().title("Identificatie"));
883           ft.getSettings()
884               .putAttributeSettingsItem(
885                   "tijdstipregistratie", new AttributeSettings().title("Registratie"));
886           ft.getSettings()
887               .putAttributeSettingsItem(
888                   "eindregistratie", new AttributeSettings().title("Eind registratie"));
889           ft.getSettings().putAttributeSettingsItem("class", new AttributeSettings().title("Klasse"));
890           ft.getSettings()
891               .putAttributeSettingsItem("bronhouder", new AttributeSettings().title("Bronhouder"));
892           ft.getSettings()
893               .putAttributeSettingsItem("inonderzoek", new AttributeSettings().title("In onderzoek"));
894           ft.getSettings()
895               .putAttributeSettingsItem(
896                   "relatievehoogteligging", new AttributeSettings().title("Relatieve hoogteligging"));
897           ft.getSettings()
898               .putAttributeSettingsItem("bgt_status", new AttributeSettings().title("BGT status"));
899           ft.getSettings()
900               .putAttributeSettingsItem("plus_status", new AttributeSettings().title("Plus-status"));
901           ft.getSettings()
902               .putAttributeSettingsItem(
903                   "plus_fysiekvoorkomen", new AttributeSettings().title("Plus-fysiek voorkomen"));
904           ft.getSettings()
905               .putAttributeSettingsItem(
906                   "begroeidterreindeeloptalud", new AttributeSettings().title("Op talud"));
907           ft.getSettings().addAttributeOrderItem("identificatie");
908           ft.getSettings().addAttributeOrderItem("bronhouder");
909           ft.getSettings().addAttributeOrderItem("class");
910         });
911 
912     featureSources.get("postgis").getFeatureTypes().stream()
913         .filter(ft -> ft.getName().equals("bak"))
914         .findFirst()
915         .ifPresent(ft -> {
916           ft.getSettings().addHideAttributesItem("gmlid");
917           ft.getSettings().addHideAttributesItem("lv_publicatiedatum");
918           ft.getSettings().addHideAttributesItem("creationdate");
919           ft.getSettings().addHideAttributesItem("tijdstipregistratie");
920           ft.getSettings().addHideAttributesItem("eindregistratie");
921           ft.getSettings().addHideAttributesItem("terminationdate");
922           ft.getSettings().addHideAttributesItem("inonderzoek");
923           ft.getSettings().addHideAttributesItem("relatievehoogteligging");
924           ft.getSettings().addHideAttributesItem("bgt_status");
925           ft.getSettings().addHideAttributesItem("plus_status");
926           ft.getSettings().addHideAttributesItem("function_");
927           ft.getSettings().addHideAttributesItem("plus_type");
928         });
929 
930     featureSources.get("postgis").getFeatureTypes().stream()
931         .filter(ft -> ft.getName().equals("kadastraal_perceel"))
932         .findFirst()
933         .ifPresent(ft -> ft.getSettings().addHideAttributesItem("gml_id"));
934   }
935 
936   public void createAppTestData() throws Exception {
937     Upload logo = new Upload()
938         .setCategory(Upload.CATEGORY_APP_LOGO)
939         .setFilename("gradient.svg")
940         .setMimeType("image/svg+xml")
941         .setContent(new ClassPathResource("test/gradient-logo.svg").getContentAsByteArray())
942         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
943     uploadRepository.save(logo);
944 
945     List<AppTreeNode> baseNodes = List.of(
946         new AppTreeLayerNode()
947             .objectType("AppTreeLayerNode")
948             .id("lyr:openbasiskaart:osm")
949             .serviceId("openbasiskaart")
950             .layerName("osm")
951             .visible(true),
952         new AppTreeLayerNode()
953             .objectType("AppTreeLayerNode")
954             .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR")
955             .serviceId("pdok-hwh-luchtfotorgb")
956             .layerName("Actueel_orthoHR")
957             .visible(false));
958 
959     Application app = new Application()
960         .setName("default")
961         .setTitle("Tailormap demo")
962         .setCrs("EPSG:28992")
963         .setAuthorizationRules(ruleAnonymousRead)
964         .setComponents(List.of(
965             new Component()
966                 .type("SIMPLE_SEARCH")
967                 .config(new ComponentConfig()
968                     .enabled(true)
969                     .putAdditionalProperty("municipalities", List.of("0344"))),
970             new Component().type("EDIT").config(new ComponentConfig().enabled(true)),
971             new Component()
972                 .type("COORDINATE_LINK_WINDOW")
973                 .config(new ComponentConfig()
974                     .enabled(true)
975                     .putAdditionalProperty(
976                         "urls",
977                         List.of(
978                             Map.of(
979                                 "id",
980                                 "google-maps",
981                                 "url",
982                                 "https://www.google.com/maps/@[lat],[lon],18z",
983                                 "alias",
984                                 "Google Maps",
985                                 "projection",
986                                 "EPSG:4326"),
987                             Map.of(
988                                 "id",
989                                 "tm-demo",
990                                 "url",
991                                 "https://demo.tailormap.com/#@[X],[Y],18",
992                                 "alias",
993                                 "Tailormap demo",
994                                 "projection",
995                                 "EPSG:28992"))))))
996         .setContentRoot(new AppContent()
997             .addBaseLayerNodesItem(new AppTreeLevelNode()
998                 .objectType("AppTreeLevelNode")
999                 .id("root-base-layers")
1000                 .root(true)
1001                 .title("Base layers")
1002                 .childrenIds(List.of(
1003                     "lyr:openbasiskaart:osm",
1004                     "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR",
1005                     "lyr:openbasiskaart-proxied:osm",
1006                     "lyr:openbasiskaart-tms:xyz",
1007                     "lyr:b3p-mapproxy-luchtfoto:xyz")))
1008             .addBaseLayerNodesItem(
1009                 // This layer from a secured proxied service should not be proxyable in a
1010                 // public app, see test_wms_secured_proxy_not_in_public_app() testcase
1011                 new AppTreeLayerNode()
1012                     .objectType("AppTreeLayerNode")
1013                     .id("lyr:openbasiskaart-proxied:osm")
1014                     .serviceId("openbasiskaart-proxied")
1015                     .layerName("osm")
1016                     .visible(false))
1017             .addBaseLayerNodesItem(new AppTreeLayerNode()
1018                 .objectType("AppTreeLayerNode")
1019                 .id("lyr:openbasiskaart-tms:xyz")
1020                 .serviceId("openbasiskaart-tms")
1021                 .layerName("xyz")
1022                 .visible(false))
1023             .addBaseLayerNodesItem(new AppTreeLayerNode()
1024                 .objectType("AppTreeLayerNode")
1025                 .id("lyr:b3p-mapproxy-luchtfoto:xyz")
1026                 .serviceId("b3p-mapproxy-luchtfoto")
1027                 .layerName("xyz")
1028                 .visible(false))
1029             .addLayerNodesItem(new AppTreeLevelNode()
1030                 .objectType("AppTreeLevelNode")
1031                 .id("root")
1032                 .root(true)
1033                 .title("Layers")
1034                 .childrenIds(List.of(
1035                     "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1036                     "lyr:bestuurlijkegebieden-proxied:Provinciegebied",
1037                     "lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied",
1038                     "lyr:snapshot-geoserver:postgis:begroeidterreindeel",
1039                     "lyr:snapshot-geoserver:postgis:bak",
1040                     "lyr:snapshot-geoserver:postgis:kadastraal_perceel",
1041                     "lyr:snapshot-geoserver:sqlserver:wegdeel",
1042                     "lyr:snapshot-geoserver:oracle:WATERDEEL",
1043                     "lyr:snapshot-geoserver:BGT",
1044                     "lvl:proxied",
1045                     "lvl:osm",
1046                     "lvl:archeo")))
1047             .addLayerNodesItem(new AppTreeLayerNode()
1048                 .objectType("AppTreeLayerNode")
1049                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied")
1050                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1051                 .layerName("Provinciegebied")
1052                 .visible(true))
1053             // This is a layer from proxied service with auth that should also not be
1054             // visible, but it has a feature source attached, should also be denied for
1055             // features access and not be included in TOC
1056             .addLayerNodesItem(new AppTreeLayerNode()
1057                 .objectType("AppTreeLayerNode")
1058                 .id("lyr:bestuurlijkegebieden-proxied:Provinciegebied")
1059                 .serviceId("bestuurlijkegebieden-proxied")
1060                 .layerName("Provinciegebied")
1061                 .visible(false))
1062             .addLayerNodesItem(new AppTreeLayerNode()
1063                 .objectType("AppTreeLayerNode")
1064                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied")
1065                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1066                 .layerName("Gemeentegebied")
1067                 .visible(true))
1068             .addLayerNodesItem(new AppTreeLayerNode()
1069                 .objectType("AppTreeLayerNode")
1070                 .id("lyr:snapshot-geoserver:postgis:begroeidterreindeel")
1071                 .serviceId("snapshot-geoserver")
1072                 .layerName("postgis:begroeidterreindeel")
1073                 .visible(true))
1074             .addLayerNodesItem(new AppTreeLayerNode()
1075                 .objectType("AppTreeLayerNode")
1076                 .id("lyr:snapshot-geoserver:postgis:bak")
1077                 .serviceId("snapshot-geoserver")
1078                 .layerName("postgis:bak")
1079                 .visible(false))
1080             .addLayerNodesItem(new AppTreeLayerNode()
1081                 .objectType("AppTreeLayerNode")
1082                 .id("lyr:snapshot-geoserver:postgis:kadastraal_perceel")
1083                 .serviceId("snapshot-geoserver")
1084                 .layerName("postgis:kadastraal_perceel")
1085                 .visible(false))
1086             .addLayerNodesItem(new AppTreeLayerNode()
1087                 .objectType("AppTreeLayerNode")
1088                 .id("lyr:snapshot-geoserver:sqlserver:wegdeel")
1089                 .serviceId("snapshot-geoserver")
1090                 .layerName("sqlserver:wegdeel")
1091                 .visible(true))
1092             .addLayerNodesItem(new AppTreeLayerNode()
1093                 .objectType("AppTreeLayerNode")
1094                 .id("lyr:snapshot-geoserver:oracle:WATERDEEL")
1095                 .serviceId("snapshot-geoserver")
1096                 .layerName("oracle:WATERDEEL")
1097                 .visible(true))
1098             .addLayerNodesItem(new AppTreeLayerNode()
1099                 .objectType("AppTreeLayerNode")
1100                 .id("lyr:snapshot-geoserver:BGT")
1101                 .serviceId("snapshot-geoserver")
1102                 .layerName("BGT")
1103                 .visible(false))
1104             .addLayerNodesItem(new AppTreeLevelNode()
1105                 .objectType("AppTreeLevelNode")
1106                 .id("lvl:proxied")
1107                 .title("Proxied")
1108                 .childrenIds(List.of("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")))
1109             .addLayerNodesItem(new AppTreeLayerNode()
1110                 .objectType("AppTreeLayerNode")
1111                 .id("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")
1112                 .serviceId("snapshot-geoserver-proxied")
1113                 .layerName("postgis:begroeidterreindeel")
1114                 .visible(false))
1115             .addLayerNodesItem(new AppTreeLevelNode()
1116                 .objectType("AppTreeLevelNode")
1117                 .id("lvl:osm")
1118                 .title("OSM")
1119                 .childrenIds(List.of("lyr:snapshot-geoserver:postgis:osm_polygon")))
1120             .addLayerNodesItem(new AppTreeLayerNode()
1121                 .objectType("AppTreeLayerNode")
1122                 .id("lyr:snapshot-geoserver:postgis:osm_polygon")
1123                 .serviceId("snapshot-geoserver")
1124                 .layerName("postgis:osm_polygon")
1125                 .visible(false))
1126             .addLayerNodesItem(new AppTreeLevelNode()
1127                 .objectType("AppTreeLevelNode")
1128                 .id("lvl:archeo")
1129                 .title("Archeology")
1130                 .childrenIds(List.of("lyr:demo:geomorfologie")))
1131             .addLayerNodesItem(new AppTreeLayerNode()
1132                 .objectType("AppTreeLayerNode")
1133                 .id("lyr:demo:geomorfologie")
1134                 .serviceId("demo")
1135                 .layerName("geomorfologie")
1136                 .visible(true)))
1137         .setStyling(new AppStyling().logo(logo.getId().toString()))
1138         .setSettings(new AppSettings()
1139             .putLayerSettingsItem("lyr:openbasiskaart:osm", new AppLayerSettings().title("Openbasiskaart"))
1140             .putLayerSettingsItem(
1141                 "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR", new AppLayerSettings().title("Luchtfoto"))
1142             .putLayerSettingsItem(
1143                 "lyr:openbasiskaart-proxied:osm",
1144                 new AppLayerSettings().title("Openbasiskaart (proxied)"))
1145             .putLayerSettingsItem(
1146                 "lyr:snapshot-geoserver:oracle:WATERDEEL",
1147                 new AppLayerSettings()
1148                     .opacity(50)
1149                     .title("Waterdeel overridden title")
1150                     .editable(true)
1151                     .description("This is the layer description from the app layer setting.")
1152                     .attribution(
1153                         "CC BY 4.0 [BGT/Kadaster](https://www.nationaalgeoregister.nl/geonetwork/srv/api/records/2cb4769c-b56e-48fa-8685-c48f61b9a319)"))
1154             .putLayerSettingsItem(
1155                 "lyr:snapshot-geoserver:postgis:osm_polygon",
1156                 new AppLayerSettings()
1157                     .description("OpenStreetMap polygon data in EPSG:3857")
1158                     .opacity(60)
1159                     .editable(true)
1160                     .title("OSM Polygon (EPSG:3857)")
1161                     .attribution(
1162                         "© [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors"))
1163             .putLayerSettingsItem(
1164                 "lyr:snapshot-geoserver:postgis:begroeidterreindeel",
1165                 new AppLayerSettings()
1166                     .editable(true)
1167                     .addHideAttributesItem("begroeidterreindeeloptalud")
1168                     .addReadOnlyAttributesItem("eindregistratie"))
1169             .putLayerSettingsItem(
1170                 "lyr:snapshot-geoserver:postgis:kadastraal_perceel",
1171                 new AppLayerSettings().editable(true).addReadOnlyAttributesItem("aanduiding"))
1172             .putLayerSettingsItem(
1173                 "lyr:snapshot-geoserver:sqlserver:wegdeel", new AppLayerSettings().editable(true))
1174             .putLayerSettingsItem(
1175                 "lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel",
1176                 new AppLayerSettings().editable(false))
1177             .putLayerSettingsItem(
1178                 "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1179                 new AppLayerSettings()
1180                     .hiddenFunctionality(Set.of(FEATURE_INFO, ATTRIBUTE_LIST, EXPORT)))
1181             .addFilterGroupsItem(new FilterGroup()
1182                 .id("filtergroup1")
1183                 .source("PRESET")
1184                 .type(FilterGroup.TypeEnum.ATTRIBUTE)
1185                 .layerIds(List.of("lyr:snapshot-geoserver:postgis:begroeidterreindeel"))
1186                 .operator(FilterGroup.OperatorEnum.AND)
1187                 .addFiltersItem(new Filter()
1188                     .id("filter1")
1189                     .type(Filter.TypeEnum.ATTRIBUTE)
1190                     .condition(Filter.ConditionEnum.BEFORE)
1191                     .addValueItem("2025-06-05")
1192                     .attribute("creationdate")
1193                     .attributeType(Filter.AttributeTypeEnum.DATE))
1194                 .addFiltersItem(new Filter()
1195                     .id("filter2")
1196                     .type(Filter.TypeEnum.ATTRIBUTE)
1197                     .condition(Filter.ConditionEnum.UNIQUE_VALUES)
1198                     .addValueItem("bodembedekkers")
1199                     .addValueItem("bosplantsoen")
1200                     .addValueItem("gras- en kruidachtigen")
1201                     .attribute("plus_fysiekvoorkomen")
1202                     .attributeType(Filter.AttributeTypeEnum.STRING)
1203                     .editConfiguration(new FilterEditConfiguration()
1204                         .filterTool(FilterEditConfiguration.FilterToolEnum.CHECKBOX)
1205                         .attributeValuesSettings(List.of(
1206                             new AttributeValueSettings()
1207                                 .value("bodembedekkers")
1208                                 .initiallySelected(true)
1209                                 .selectable(true)
1210                                 .alias("Bodembedekkers"),
1211                             new AttributeValueSettings()
1212                                 .value("bosplantsoen")
1213                                 .initiallySelected(true)
1214                                 .selectable(true)
1215                                 .alias("Bosplantsoen"),
1216                             new AttributeValueSettings()
1217                                 .value("gras- en kruidachtigen")
1218                                 .initiallySelected(true)
1219                                 .selectable(true)
1220                                 .alias("Gras- en kruidachtigen"),
1221                             new AttributeValueSettings()
1222                                 .value("griend en hakhout")
1223                                 .initiallySelected(false)
1224                                 .selectable(true),
1225                             new AttributeValueSettings()
1226                                 .value("heesters")
1227                                 .initiallySelected(false)
1228                                 .selectable(true),
1229                             new AttributeValueSettings()
1230                                 .value("planten")
1231                                 .initiallySelected(false)
1232                                 .selectable(true),
1233                             new AttributeValueSettings()
1234                                 .value("struikrozen")
1235                                 .initiallySelected(false)
1236                                 .selectable(true),
1237                             new AttributeValueSettings()
1238                                 .value("waardeOnbekend")
1239                                 .initiallySelected(false)
1240                                 .selectable(true))))))
1241             .addFilterGroupsItem(new FilterGroup()
1242                 .id("filtergroup2")
1243                 .source("PRESET")
1244                 .type(FilterGroup.TypeEnum.ATTRIBUTE)
1245                 .layerIds(List.of("lyr:snapshot-geoserver:postgis:kadastraal_perceel"))
1246                 .operator(FilterGroup.OperatorEnum.AND)
1247                 .addFiltersItem(new Filter()
1248                     .id("filter3")
1249                     .type(Filter.TypeEnum.ATTRIBUTE)
1250                     .condition(Filter.ConditionEnum.u)
1251                     .addValueItem("1")
1252                     .addValueItem("12419")
1253                     .attribute("perceelnummer")
1254                     .attributeType(Filter.AttributeTypeEnum.DOUBLE)
1255                     .editConfiguration(new FilterEditConfiguration()
1256                         .filterTool(FilterEditConfiguration.FilterToolEnum.SLIDER)
1257                         .initialLowerValue(1d)
1258                         .initialUpperValue(12419d)
1259                         .minimumValue(1d)
1260                         .maximumValue(12419d)))));
1261 
1262     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1263     app.setInitialExtent(
1264         new Bounds().minx(130011d).miny(458031d).maxx(132703d).maxy(459995d));
1265     app.setMaxExtent(new Bounds().minx(-285401d).miny(22598d).maxx(595401d).maxy(903401d));
1266 
1267     if (map5url != null) {
1268       AppTreeLevelNode root =
1269           (AppTreeLevelNode) app.getContentRoot().getBaseLayerNodes().get(0);
1270       List<String> childrenIds = new ArrayList<>(root.getChildrenIds());
1271       childrenIds.add("lyr:map5:map5topo");
1272       childrenIds.add("lyr:map5:map5topo_simple");
1273       childrenIds.add("lvl:luchtfoto-labels");
1274       root.setChildrenIds(childrenIds);
1275       app.getSettings()
1276           .putLayerSettingsItem("lyr:map5:map5topo", new AppLayerSettings().title("Map5"))
1277           .putLayerSettingsItem("lyr:map5:map5topo_simple", new AppLayerSettings().title("Map5 simple"));
1278       app.getContentRoot()
1279           .addBaseLayerNodesItem(new AppTreeLayerNode()
1280               .objectType("AppTreeLayerNode")
1281               .id("lyr:map5:map5topo")
1282               .serviceId("map5")
1283               .layerName("map5topo")
1284               .visible(false))
1285           .addBaseLayerNodesItem(new AppTreeLayerNode()
1286               .objectType("AppTreeLayerNode")
1287               .id("lyr:map5:map5topo_simple")
1288               .serviceId("map5")
1289               .layerName("map5topo_simple")
1290               .visible(false))
1291           .addBaseLayerNodesItem(new AppTreeLevelNode()
1292               .objectType("AppTreeLevelNode")
1293               .id("lvl:luchtfoto-labels")
1294               .title("Luchtfoto met labels")
1295               .addChildrenIdsItem("lyr:map5:luforoadslabels")
1296               .addChildrenIdsItem("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR2"))
1297           .addBaseLayerNodesItem(new AppTreeLayerNode()
1298               .objectType("AppTreeLayerNode")
1299               .id("lyr:map5:luforoadslabels")
1300               .serviceId("map5")
1301               .layerName("luforoadslabels")
1302               .visible(false))
1303           .addBaseLayerNodesItem(new AppTreeLayerNode()
1304               .objectType("AppTreeLayerNode")
1305               .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR2")
1306               .serviceId("pdok-hwh-luchtfotorgb")
1307               .layerName("Actueel_orthoHR")
1308               .visible(false));
1309     }
1310 
1311     applicationRepository.save(app);
1312 
1313     app = new Application()
1314         .setName("base")
1315         .setTitle("Service base app")
1316         .setCrs("EPSG:28992")
1317         .setAuthorizationRules(ruleAnonymousRead)
1318         .setContentRoot(new AppContent()
1319             .addBaseLayerNodesItem(new AppTreeLevelNode()
1320                 .objectType("AppTreeLevelNode")
1321                 .id("root-base-layers")
1322                 .root(true)
1323                 .title("Base layers")
1324                 .childrenIds(List.of(
1325                     "lyr:openbasiskaart:osm", "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR"))));
1326     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1327     applicationRepository.save(app);
1328 
1329     app = new Application()
1330         .setName("secured")
1331         .setTitle("secured app")
1332         .setCrs("EPSG:28992")
1333         .setAuthorizationRules(ruleLoggedIn)
1334         .setContentRoot(new AppContent()
1335             .addBaseLayerNodesItem(new AppTreeLevelNode()
1336                 .objectType("AppTreeLevelNode")
1337                 .id("root-base-layers")
1338                 .root(true)
1339                 .title("Base layers")
1340                 .childrenIds(List.of(
1341                     "lyr:openbasiskaart:osm",
1342                     "lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR",
1343                     "lyr:openbasiskaart-proxied:osm")))
1344             .addBaseLayerNodesItem(new AppTreeLayerNode()
1345                 .objectType("AppTreeLayerNode")
1346                 .id("lyr:openbasiskaart-proxied:osm")
1347                 .serviceId("openbasiskaart-proxied")
1348                 .layerName("osm")
1349                 .visible(false))
1350             .addLayerNodesItem(new AppTreeLevelNode()
1351                 .objectType("AppTreeLevelNode")
1352                 .id("root")
1353                 .root(true)
1354                 .title("Layers")
1355                 .childrenIds(List.of(
1356                     "lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied",
1357                     "lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied",
1358                     "lvl:proxied")))
1359             .addLayerNodesItem(new AppTreeLayerNode()
1360                 .objectType("AppTreeLayerNode")
1361                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Gemeentegebied")
1362                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1363                 .layerName("Gemeentegebied")
1364                 .visible(true))
1365             .addLayerNodesItem(new AppTreeLayerNode()
1366                 .objectType("AppTreeLayerNode")
1367                 .id("lyr:pdok-kadaster-bestuurlijkegebieden:Provinciegebied")
1368                 .serviceId("pdok-kadaster-bestuurlijkegebieden")
1369                 .layerName("Provinciegebied")
1370                 .visible(false))
1371             .addLayerNodesItem(new AppTreeLevelNode()
1372                 .objectType("AppTreeLevelNode")
1373                 .id("lvl:proxied")
1374                 .title("Proxied")
1375                 .childrenIds(List.of("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")))
1376             .addLayerNodesItem(new AppTreeLayerNode()
1377                 .objectType("AppTreeLayerNode")
1378                 .id("lyr:snapshot-geoserver-proxied:postgis:begroeidterreindeel")
1379                 .serviceId("snapshot-geoserver-proxied")
1380                 .layerName("postgis:begroeidterreindeel")
1381                 .visible(false)))
1382         .setSettings(new AppSettings()
1383             .putLayerSettingsItem(
1384                 "lyr:openbasiskaart-proxied:osm",
1385                 new AppLayerSettings().title("Openbasiskaart (proxied)")));
1386 
1387     app.getContentRoot().getBaseLayerNodes().addAll(baseNodes);
1388     applicationRepository.save(app);
1389 
1390     app = new Application()
1391         .setName("secured-auth")
1392         .setTitle("secured (with authorizations)")
1393         .setCrs("EPSG:28992")
1394         .setAuthorizationRules(List.of(
1395             new AuthorizationRule()
1396                 .groupName("test-foo")
1397                 .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW)),
1398             new AuthorizationRule()
1399                 .groupName("test-bar")
1400                 .decisions(Map.of(ACCESS_TYPE_VIEW, AuthorizationRuleDecision.ALLOW))))
1401         .setContentRoot(new AppContent()
1402             .addLayerNodesItem(new AppTreeLevelNode()
1403                 .objectType("AppTreeLevelNode")
1404                 .id("root")
1405                 .root(true)
1406                 .title("Layers")
1407                 .childrenIds(List.of("lvl:needs-auth", "lvl:public")))
1408             .addLayerNodesItem(new AppTreeLevelNode()
1409                 .objectType("AppTreeLevelNode")
1410                 .id("lvl:public")
1411                 .title("Public")
1412                 .childrenIds(List.of("lyr:snapshot-geoserver:BGT")))
1413             .addLayerNodesItem(new AppTreeLevelNode()
1414                 .objectType("AppTreeLevelNode")
1415                 .id("lvl:needs-auth")
1416                 .title("Needs auth")
1417                 .childrenIds(List.of(
1418                     "lyr:filtered-snapshot-geoserver:BGT",
1419                     "lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")))
1420             .addLayerNodesItem(new AppTreeLayerNode()
1421                 .objectType("AppTreeLayerNode")
1422                 .id("lyr:filtered-snapshot-geoserver:BGT")
1423                 .serviceId("filtered-snapshot-geoserver")
1424                 .layerName("BGT")
1425                 .visible(true))
1426             .addLayerNodesItem(new AppTreeLayerNode()
1427                 .objectType("AppTreeLayerNode")
1428                 .id("lyr:filtered-snapshot-geoserver:postgis:begroeidterreindeel")
1429                 .serviceId("filtered-snapshot-geoserver")
1430                 .layerName("postgis:begroeidterreindeel")
1431                 .visible(true))
1432             .addLayerNodesItem(new AppTreeLayerNode()
1433                 .objectType("AppTreeLayerNode")
1434                 .id("lyr:snapshot-geoserver:BGT")
1435                 .serviceId("snapshot-geoserver")
1436                 .layerName("BGT")
1437                 .visible(true)));
1438 
1439     applicationRepository.save(app);
1440 
1441     app = new Application()
1442         .setName("austria")
1443         .setCrs("EPSG:3857")
1444         .setAuthorizationRules(ruleAnonymousRead)
1445         .setTitle("Austria")
1446         .setInitialExtent(
1447             new Bounds().minx(987982d).miny(5799551d).maxx(1963423d).maxy(6320708d))
1448         .setMaxExtent(
1449             new Bounds().minx(206516d).miny(5095461d).maxx(3146930d).maxy(7096232d))
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:at-basemap:geolandbasemap",
1458                     "lyr:at-basemap:orthofoto",
1459                     "lvl:orthofoto-labels",
1460                     "lyr:osm:xyz")))
1461             .addBaseLayerNodesItem(new AppTreeLayerNode()
1462                 .objectType("AppTreeLayerNode")
1463                 .id("lyr:at-basemap:geolandbasemap")
1464                 .serviceId("at-basemap")
1465                 .layerName("geolandbasemap")
1466                 .visible(true))
1467             .addBaseLayerNodesItem(new AppTreeLayerNode()
1468                 .objectType("AppTreeLayerNode")
1469                 .id("lyr:at-basemap:orthofoto")
1470                 .serviceId("at-basemap")
1471                 .layerName("bmaporthofoto30cm")
1472                 .visible(false))
1473             .addBaseLayerNodesItem(new AppTreeLevelNode()
1474                 .objectType("AppTreeLevelNode")
1475                 .id("lvl:orthofoto-labels")
1476                 .title("Orthophoto with labels")
1477                 .childrenIds(List.of("lyr:at-basemap:bmapoverlay", "lyr:at-basemap:orthofoto_2")))
1478             .addBaseLayerNodesItem(new AppTreeLayerNode()
1479                 .objectType("AppTreeLayerNode")
1480                 .id("lyr:at-basemap:bmapoverlay")
1481                 .serviceId("at-basemap")
1482                 .layerName("bmapoverlay")
1483                 .visible(false))
1484             .addBaseLayerNodesItem(new AppTreeLayerNode()
1485                 .objectType("AppTreeLayerNode")
1486                 .id("lyr:at-basemap:orthofoto_2")
1487                 .serviceId("at-basemap")
1488                 .layerName("bmaporthofoto30cm")
1489                 .visible(false))
1490             .addBaseLayerNodesItem(new AppTreeLayerNode()
1491                 .objectType("AppTreeLayerNode")
1492                 .id("lyr:osm:xyz")
1493                 .serviceId("osm")
1494                 .layerName("xyz")
1495                 .visible(false)));
1496 
1497     applicationRepository.save(app);
1498 
1499     app = new Application()
1500         .setName("3d_utrecht")
1501         .setCrs("EPSG:3857")
1502         .setAuthorizationRules(ruleLoggedIn)
1503         .setTitle("3D Utrecht")
1504         .setInitialExtent(
1505             new Bounds().minx(558390d).miny(6818485d).maxx(566751d).maxy(6824036d))
1506         .setMaxExtent(
1507             new Bounds().minx(91467d).miny(6496479d).maxx(1037043d).maxy(7147453d))
1508         .setSettings(new AppSettings().uiSettings(new AppUiSettings().enable3D(true)))
1509         .setContentRoot(new AppContent()
1510             .addBaseLayerNodesItem(new AppTreeLevelNode()
1511                 .objectType("AppTreeLevelNode")
1512                 .id("root-base-layers")
1513                 .root(true)
1514                 .title("Base layers")
1515                 .childrenIds(List.of("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR", "lyr:osm:xyz")))
1516             .addBaseLayerNodesItem(new AppTreeLayerNode()
1517                 .objectType("AppTreeLayerNode")
1518                 .id("lyr:pdok-hwh-luchtfotorgb:Actueel_orthoHR")
1519                 .serviceId("pdok-hwh-luchtfotorgb")
1520                 .layerName("Actueel_orthoHR")
1521                 .visible(true))
1522             .addBaseLayerNodesItem(new AppTreeLayerNode()
1523                 .objectType("AppTreeLayerNode")
1524                 .id("lyr:osm:xyz")
1525                 .serviceId("osm")
1526                 .layerName("xyz")
1527                 .visible(false))
1528             .addLayerNodesItem(new AppTreeLevelNode()
1529                 .objectType("AppTreeLevelNode")
1530                 .id("root")
1531                 .root(true)
1532                 .title("Layers")
1533                 .childrenIds(List.of(
1534                     "lyr:3dbag_utrecht:tiles3d",
1535                     "lyr:snapshot-geoserver:postgis:begroeidterreindeel",
1536                     "lyr:3d_basisvoorziening_gebouwen_proxy:tiles3d",
1537                     "lyr:3d_utrecht_proxied_auth:tiles3d")))
1538             .addLayerNodesItem(new AppTreeLayerNode()
1539                 .objectType("AppTreeLayerNode")
1540                 .id("lyr:3dbag_utrecht:tiles3d")
1541                 .serviceId("3dbag_utrecht")
1542                 .layerName("tiles3d")
1543                 .visible(true))
1544             .addLayerNodesItem(new AppTreeLayerNode()
1545                 .objectType("AppTreeLayerNode")
1546                 .id("lyr:3d_basisvoorziening_gebouwen_proxy:tiles3d")
1547                 .serviceId("3d_basisvoorziening_gebouwen_proxy")
1548                 .layerName("tiles3d")
1549                 .visible(false))
1550             .addLayerNodesItem(new AppTreeLayerNode()
1551                 .objectType("AppTreeLayerNode")
1552                 .id("lyr:3d_utrecht_proxied_auth:tiles3d")
1553                 .serviceId("3d_utrecht_proxied_auth")
1554                 .layerName("tiles3d")
1555                 .visible(false))
1556             .addLayerNodesItem(new AppTreeLayerNode()
1557                 .objectType("AppTreeLayerNode")
1558                 .id("lyr:snapshot-geoserver:postgis:begroeidterreindeel")
1559                 .serviceId("snapshot-geoserver")
1560                 .layerName("postgis:begroeidterreindeel")
1561                 .visible(true))
1562             .addTerrainLayerNodesItem(new AppTreeLevelNode()
1563                 .objectType("AppTreeLevelNode")
1564                 .id("root-terrain-layers")
1565                 .root(true)
1566                 .title("Terrain Layers")
1567                 .childrenIds(List.of("lyr:ahn_terrain_model:quantizedmesh")))
1568             .addTerrainLayerNodesItem(new AppTreeLayerNode()
1569                 .objectType("AppTreeLayerNode")
1570                 .id("lyr:ahn_terrain_model:quantizedmesh")
1571                 .serviceId("ahn_terrain_model")
1572                 .layerName("quantizedmesh")
1573                 .visible(false)));
1574 
1575     applicationRepository.save(app);
1576 
1577     Configuration config = new Configuration();
1578     config.setKey(Configuration.DEFAULT_APP);
1579     config.setValue("default");
1580     configurationRepository.save(config);
1581     config = new Configuration();
1582     config.setKey(Configuration.DEFAULT_BASE_APP);
1583     config.setValue("base");
1584     configurationRepository.save(config);
1585   }
1586 
1587   private void createConfigurationTestData() throws JsonProcessingException {
1588     Configuration config = new Configuration();
1589     config.setKey("test");
1590     config.setAvailableForViewer(true);
1591     config.setValue("test value");
1592     config.setJsonValue(new ObjectMapper().readTree("{ \"someProperty\": 1, \"nestedObject\": { \"num\": 42 } }"));
1593     configurationRepository.save(config);
1594   }
1595 
1596   @Transactional
1597   public void createSolrIndex() throws Exception {
1598     if (connectToSpatialDbs) {
1599       // flush() the repo because we need to make sure feature type testdata is fully stored
1600       // before creating the Solr index (which requires access to the feature type settings)
1601       featureSourceRepository.flush();
1602 
1603       logger.info("Creating Solr index");
1604       @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
1605       final String solrUrl = "http://" + (connectToSpatialDbsAtLocalhost ? "127.0.0.1" : "solr") + ":8983/solr/";
1606       this.solrService.setSolrUrl(solrUrl);
1607       SolrHelper solrHelper = new SolrHelper(this.solrService.getSolrClientForIndexing())
1608           .withBatchSize(solrBatchSize)
1609           .withGeometryValidationRule(solrGeometryValidationRule);
1610       GeoService geoService =
1611           geoServiceRepository.findById("snapshot-geoserver").orElseThrow();
1612       Application defaultApp = applicationRepository.findByName("default");
1613 
1614       TMFeatureType begroeidterreindeelFT = geoService.findFeatureTypeForLayer(
1615           geoService.findLayer("postgis:begroeidterreindeel"), featureSourceRepository);
1616 
1617       TMFeatureType wegdeelFT = geoService.findFeatureTypeForLayer(
1618           geoService.findLayer("sqlserver:wegdeel"), featureSourceRepository);
1619 
1620       TMFeatureType kadastraalPerceelFT = geoService.findFeatureTypeForLayer(
1621           geoService.findLayer("postgis:kadastraal_perceel"), featureSourceRepository);
1622 
1623       try (solrHelper) {
1624         SearchIndex begroeidterreindeelIndex = null;
1625         if (begroeidterreindeelFT != null) {
1626           begroeidterreindeelIndex = new SearchIndex()
1627               .setName("Begroeidterreindeel")
1628               .setFeatureTypeId(begroeidterreindeelFT.getId())
1629               .setSearchFieldsUsed(List.of("class", "plus_fysiekvoorkomen", "bronhouder"))
1630               .setSearchDisplayFieldsUsed(List.of("class", "plus_fysiekvoorkomen"));
1631           begroeidterreindeelIndex = searchIndexRepository.save(begroeidterreindeelIndex);
1632           begroeidterreindeelIndex = solrHelper.addFeatureTypeIndex(
1633               begroeidterreindeelIndex,
1634               begroeidterreindeelFT,
1635               featureSourceFactoryHelper,
1636               searchIndexRepository);
1637           begroeidterreindeelIndex = searchIndexRepository.save(begroeidterreindeelIndex);
1638         }
1639 
1640         SearchIndex kadastraalPerceelIndex = null;
1641         if (kadastraalPerceelFT != null) {
1642           kadastraalPerceelIndex = new SearchIndex()
1643               .setName("kadastraal_perceel")
1644               .setFeatureTypeId(kadastraalPerceelFT.getId())
1645               .setSearchFieldsUsed(List.of("aanduiding"))
1646               .setSearchDisplayFieldsUsed(List.of("aanduiding"));
1647           kadastraalPerceelIndex = searchIndexRepository.save(kadastraalPerceelIndex);
1648           kadastraalPerceelIndex = solrHelper.addFeatureTypeIndex(
1649               kadastraalPerceelIndex,
1650               kadastraalPerceelFT,
1651               featureSourceFactoryHelper,
1652               searchIndexRepository);
1653           kadastraalPerceelIndex = searchIndexRepository.save(kadastraalPerceelIndex);
1654         }
1655 
1656         SearchIndex wegdeelIndex = null;
1657         if (wegdeelFT != null) {
1658           wegdeelIndex = new SearchIndex()
1659               .setName("Wegdeel")
1660               .setFeatureTypeId(wegdeelFT.getId())
1661               .setSearchFieldsUsed(List.of(
1662                   "function_", "plus_fysiekvoorkomenwegdeel", "surfacematerial", "bronhouder"))
1663               .setSearchDisplayFieldsUsed(List.of("function_", "plus_fysiekvoorkomenwegdeel"));
1664           wegdeelIndex = searchIndexRepository.save(wegdeelIndex);
1665           wegdeelIndex = solrHelper.addFeatureTypeIndex(
1666               wegdeelIndex, wegdeelFT, featureSourceFactoryHelper, searchIndexRepository);
1667           wegdeelIndex = searchIndexRepository.save(wegdeelIndex);
1668         }
1669 
1670         featureSourceRepository
1671             .getByTitle("PostGIS")
1672             .flatMap(fs -> fs.getFeatureTypes().stream()
1673                 .filter(ft -> ft.getName().equals("bak"))
1674                 .findFirst())
1675             .ifPresent(ft -> {
1676               SearchIndex bak = new SearchIndex()
1677                   .setName("bak")
1678                   .setFeatureTypeId(ft.getId())
1679                   .setSearchFieldsUsed(List.of("gmlid", "identificatie", "plus_type"))
1680                   .setSearchDisplayFieldsUsed(List.of("gmlid", "plus_type", "bronhouder"));
1681               searchIndexRepository.save(bak);
1682               try {
1683                 bak = solrHelper.addFeatureTypeIndex(
1684                     bak, ft, featureSourceFactoryHelper, searchIndexRepository);
1685                 searchIndexRepository.save(bak);
1686               } catch (IOException | SolrServerException e) {
1687                 throw new RuntimeException(e);
1688               }
1689             });
1690 
1691         // creating a solr index of config this will/should fail because there is no primary key in
1692         // the FT
1693         featureSourceRepository
1694             .getByTitle("PostGIS OSM")
1695             .flatMap(fs -> fs.getFeatureTypes().stream()
1696                 .filter(ft -> ft.getName().equals("osm_roads"))
1697                 .findFirst())
1698             .ifPresent(ft -> {
1699               SearchIndex osm_no_pk = new SearchIndex()
1700                   .setName("osm_no_pk")
1701                   .setFeatureTypeId(ft.getId())
1702                   .setSearchFieldsUsed(List.of("landuse", "osm_id", "natural", "boundary"))
1703                   .setSearchDisplayFieldsUsed(
1704                       List.of("landuse", "osm_id", "natural", "amenity", "boundary"));
1705               searchIndexRepository.save(osm_no_pk);
1706             });
1707 
1708         AppTreeLayerNode begroeidTerreindeelLayerNode = defaultApp
1709             .getAllAppTreeLayerNode()
1710             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:postgis:begroeidterreindeel"))
1711             .findFirst()
1712             .orElse(null);
1713 
1714         if (begroeidTerreindeelLayerNode != null && begroeidterreindeelIndex != null) {
1715           defaultApp
1716               .getAppLayerSettings(begroeidTerreindeelLayerNode)
1717               .setSearchIndexId(begroeidterreindeelIndex.getId());
1718         }
1719 
1720         AppTreeLayerNode kadastraalPerceelLayerNode = defaultApp
1721             .getAllAppTreeLayerNode()
1722             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:postgis:kadastraal_perceel"))
1723             .findFirst()
1724             .orElse(null);
1725 
1726         if (kadastraalPerceelLayerNode != null && kadastraalPerceelIndex != null) {
1727           defaultApp
1728               .getAppLayerSettings(kadastraalPerceelLayerNode)
1729               .setSearchIndexId(kadastraalPerceelIndex.getId());
1730         }
1731 
1732         AppTreeLayerNode wegdeel = defaultApp
1733             .getAllAppTreeLayerNode()
1734             .filter(node -> node.getId().equals("lyr:snapshot-geoserver:sqlserver:wegdeel"))
1735             .findFirst()
1736             .orElse(null);
1737 
1738         if (wegdeel != null && wegdeelIndex != null) {
1739           defaultApp.getAppLayerSettings(wegdeel).setSearchIndexId(wegdeelIndex.getId());
1740         }
1741 
1742         applicationRepository.save(defaultApp);
1743       }
1744     }
1745   }
1746 
1747   private void createScheduledTasks() {
1748     try {
1749       logger.info("Creating POC tasks");
1750       logger.info(
1751           "Created 15 minutely task with key: {}",
1752           taskManagerService.createTask(
1753               PocTask.class,
1754               new TMJobDataMap(Map.of(
1755                   Task.TYPE_KEY,
1756                   TaskType.POC.getValue(),
1757                   "foo",
1758                   "foobar",
1759                   Task.DESCRIPTION_KEY,
1760                   "POC task that runs every 15 minutes")),
1761               /* run every 15 minutes */ "0 0/15 * 1/1 * ? *"));
1762       logger.info(
1763           "Created hourly task with key: {}",
1764           taskManagerService.createTask(
1765               PocTask.class,
1766               new TMJobDataMap(Map.of(
1767                   Task.TYPE_KEY,
1768                   TaskType.POC.getValue(),
1769                   "foo",
1770                   "bar",
1771                   Task.DESCRIPTION_KEY,
1772                   "POC task that runs every hour",
1773                   Task.PRIORITY_KEY,
1774                   10)),
1775               /* run every hour */ "0 0 0/1 1/1 * ? *"));
1776 
1777       logger.info(
1778           "Created hourly failing task with key: {}",
1779           taskManagerService.createTask(
1780               FailingPocTask.class,
1781               new TMJobDataMap(Map.of(
1782                   Task.TYPE_KEY,
1783                   TaskType.FAILINGPOC.getValue(),
1784                   Task.DESCRIPTION_KEY,
1785                   "POC task that fails every hour with low priority",
1786                   Task.PRIORITY_KEY,
1787                   100)),
1788               /* run every hour */ "0 0 0/1 1/1 * ? *"));
1789       logger.info(
1790           "Created daily task with key: {}",
1791           taskManagerService.createTask(
1792               InterruptablePocTask.class,
1793               new TMJobDataMap(Map.of(
1794                   Task.TYPE_KEY,
1795                   TaskType.INTERRUPTABLEPOC.getValue(),
1796                   Task.DESCRIPTION_KEY,
1797                   "Interruptable POC task that runs every 15 minutes",
1798                   Task.PRIORITY_KEY,
1799                   5)),
1800               /* run every 15 minutes */ "0 0/15 * 1/1 * ? *"));
1801     } catch (SchedulerException e) {
1802       logger.error("Error creating scheduled one or more poc tasks", e);
1803     }
1804 
1805     if (categories.contains("search-index")) {
1806       logger.info("Creating INDEX tasks");
1807       List.of("Begroeidterreindeel", "kadastraal_perceel")
1808           .forEach(name -> searchIndexRepository.findByName(name).ifPresent(index -> {
1809             index.setSchedule(new TaskSchedule()
1810                 /* hour */
1811                 .cronExpression("0 0 0/1 1/1 * ? *")
1812                 // /* 15 min */
1813                 // .cronExpression("0 0/15 * 1/1 * ? *")
1814                 .description("Update Solr index \" " + name + "\" every hour"));
1815             try {
1816               final UUID uuid = taskManagerService.createTask(
1817                   IndexTask.class,
1818                   new TMJobDataMap(Map.of(
1819                       Task.TYPE_KEY,
1820                       TaskType.INDEX,
1821                       Task.DESCRIPTION_KEY,
1822                       index.getSchedule().getDescription(),
1823                       IndexTask.INDEX_KEY,
1824                       index.getId().toString(),
1825                       Task.PRIORITY_KEY,
1826                       10)),
1827                   index.getSchedule().getCronExpression());
1828 
1829               index.getSchedule().setUuid(uuid);
1830               searchIndexRepository.save(index);
1831 
1832               logger.info("Created task to update Solr index with key: {}", uuid);
1833             } catch (SchedulerException e) {
1834               logger.error("Error creating scheduled solr index task", e);
1835             }
1836           }));
1837     }
1838   }
1839 
1840   private void createPages() throws IOException {
1841     Upload logo = new Upload()
1842         .setCategory(Upload.CATEGORY_PORTAL_IMAGE)
1843         .setFilename("gradient.svg")
1844         .setMimeType("image/svg+xml")
1845         .setContent(new ClassPathResource("test/gradient-logo.svg").getContentAsByteArray())
1846         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
1847     uploadRepository.save(logo);
1848 
1849     Page about = new Page();
1850     about.setName("about");
1851     about.setType("page");
1852     about.setContent("About Tailormap");
1853     about.setContent("""
1854 # About Tailormap
1855 
1856 This is a page about *Tailormap*. It doesn't say much yet.
1857 """);
1858     pageRepository.save(about);
1859 
1860     Page page = new Page();
1861     page.setName("home");
1862     page.setType("page");
1863     page.setTitle("Tailormap - Home");
1864     page.setContent(
1865         """
1866 # Welcome to Tailormap!
1867 
1868 This page is only visible when you implement a frontend to display pages, or get it (including a simple CMS)
1869 from [B3Partners](https://www.b3partners.nl)!
1870 """);
1871     page.setClassName(null);
1872     page.setTiles(List.of(
1873         new PageTile()
1874             .id(UUID.randomUUID().toString())
1875             .title("Default app")
1876             .applicationId(Optional.ofNullable(applicationRepository.findByName("default"))
1877                 .map(Application::getId)
1878                 .orElse(null))
1879             .image(logo.getId().toString())
1880             .content("*Default app* tile content")
1881             .filterRequireAuthorization(false)
1882             .openInNewWindow(false),
1883         new PageTile()
1884             .id(UUID.randomUUID().toString())
1885             .title("Secured app")
1886             .applicationId(Optional.ofNullable(applicationRepository.findByName("secured"))
1887                 .map(Application::getId)
1888                 .orElse(null))
1889             .filterRequireAuthorization(true)
1890             .content("Secure app, only shown if user has authorization")
1891             .openInNewWindow(false),
1892         new PageTile()
1893             .id(UUID.randomUUID().toString())
1894             .title("Secured app (unfiltered)")
1895             .applicationId(Optional.ofNullable(applicationRepository.findByName("secured"))
1896                 .map(Application::getId)
1897                 .orElse(null))
1898             .filterRequireAuthorization(false)
1899             .content("Secure app, tile shown to everyone")
1900             .openInNewWindow(false),
1901         new PageTile()
1902             .id(UUID.randomUUID().toString())
1903             .title("About")
1904             .pageId(about.getId())
1905             .openInNewWindow(false),
1906         new PageTile()
1907             .id(UUID.randomUUID().toString())
1908             .title("B3Partners")
1909             .url("https://www.b3partners.nl/")
1910             .openInNewWindow(true)));
1911     pageRepository.save(page);
1912 
1913     Configuration c = new Configuration();
1914     c.setKey(HOME_PAGE);
1915     c.setValue(page.getId().toString());
1916     configurationRepository.save(c);
1917 
1918     List<MenuItem> globalMenuItems = List.of(
1919         new MenuItem().pageId(about.getId()).label("About").openInNewWindow(false),
1920         new MenuItem()
1921             .label("B3Partners website")
1922             .url("https://www.b3partners.nl/")
1923             .openInNewWindow(true)
1924             .exclusiveOnPageId(about.getId()));
1925     c = new Configuration();
1926     c.setKey(PORTAL_MENU);
1927     c.setJsonValue(new ObjectMapper().valueToTree(globalMenuItems));
1928     configurationRepository.save(c);
1929   }
1930 
1931   private void insertTestDrawing() {
1932     // note that the drawing uuid is hardcoded and used in the DrawingControllerIntegrationTest
1933     try {
1934       this.jdbcClient
1935           .sql(
1936               """
1937 INSERT INTO data.drawing (id,name,description,domain_data,"access",created_by,created_at,updated_by,updated_at,srid,"version") VALUES
1938 ('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);
1939 """)
1940           .update();
1941 
1942       this.jdbcClient
1943           .sql(
1944               """
1945 INSERT INTO data.drawing_feature (drawing_id,id,geometry,properties) VALUES
1946 ('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),
1947 ('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),
1948 ('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)
1949 """)
1950           .update();
1951     } catch (Exception any) {
1952       logger.error("Error inserting test drawing in data schema, some tests may fail", any);
1953     }
1954   }
1955 
1956   private void insertDrawingStyle() throws IOException {
1957     Upload upload = new Upload()
1958         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
1959         .setMimeType("image/svg+xml")
1960         .setFilename("ISO_7001_PI_PF_007.svg")
1961         .setContent(new ClassPathResource("test/ISO_7001_PI_PF_007.svg").getContentAsByteArray())
1962         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
1963     upload = uploadRepository.save(upload);
1964     UUID drinkwaterImageId = upload.getId();
1965 
1966     upload = new Upload()
1967         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
1968         .setMimeType("image/svg+xml")
1969         .setFilename("lichtpunt.svg")
1970         .setContent(new ClassPathResource("test/lichtpunt.svg").getContentAsByteArray())
1971         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
1972     upload = uploadRepository.save(upload);
1973     UUID lichtpuntImageId = upload.getId();
1974 
1975     upload = new Upload()
1976         .setCategory(Upload.CATEGORY_DRAWING_STYLE_IMAGE)
1977         .setMimeType("image/svg+xml")
1978         .setFilename("ISO_7010_E003_-_First_aid_sign.svg")
1979         .setContent(new ClassPathResource("test/ISO_7010_E003_-_First_aid_sign.svg").getContentAsByteArray())
1980         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
1981     upload = uploadRepository.save(upload);
1982     UUID firstAidImageId = upload.getId();
1983 
1984     Properties props = new Properties();
1985     props.putAll(Map.of(
1986         "water-uuid", drinkwaterImageId.toString(),
1987         "lichtpunt-uuid", lichtpuntImageId.toString(),
1988         "first-aid-uuid", firstAidImageId.toString()));
1989 
1990     String drawingStyles =
1991         new ClassPathResource("test/object-drawing-style.json").getContentAsString(StandardCharsets.UTF_8);
1992     drawingStyles = new PropertyPlaceholderHelper("${", "}").replacePlaceholders(drawingStyles, props);
1993 
1994     // save the updated drawing style
1995     upload = new Upload()
1996         .setCategory(Upload.CATEGORY_DRAWING_STYLE)
1997         .setMimeType("application/json")
1998         .setFilename("object-drawing-style.json")
1999         .setContent(drawingStyles.getBytes(StandardCharsets.UTF_8))
2000         .setLastModified(OffsetDateTime.now(ZoneId.systemDefault()));
2001     uploadRepository.save(upload);
2002   }
2003 }