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