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