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