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