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