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