From a4fd212b277f75fd5021ff78bf453cf68bf39b19 Mon Sep 17 00:00:00 2001 From: Mark Prins <1165786+mprins@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:08:49 +0200 Subject: [PATCH] WIP [HTM-1076][HTM-1077] TMFeatureType event handler to propagate changes to solr indexes fixup after API changes in https://github.com/B3Partners/tailormap-api/pull/876 move testcases and remove now useless test, use solr service inject fix rebase issues --- .../controller/admin/SolrAdminController.java | 3 +- .../api/repository/ApplicationRepository.java | 16 ++ .../events/SolrTMFeatureTypeEventHandler.java | 139 ++++++++++++++++++ .../org/tailormap/api/solr/SolrHelper.java | 2 +- .../ApplicationRepositoryIntegrationTest.java | 15 ++ 5 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/tailormap/api/repository/events/SolrTMFeatureTypeEventHandler.java diff --git a/src/main/java/org/tailormap/api/controller/admin/SolrAdminController.java b/src/main/java/org/tailormap/api/controller/admin/SolrAdminController.java index 00bb4f8ae4..0b1512e20d 100644 --- a/src/main/java/org/tailormap/api/controller/admin/SolrAdminController.java +++ b/src/main/java/org/tailormap/api/controller/admin/SolrAdminController.java @@ -46,7 +46,6 @@ public class SolrAdminController { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final FeatureSourceFactoryHelper featureSourceFactoryHelper; - private final FeatureTypeRepository featureTypeRepository; private final SearchIndexRepository searchIndexRepository; private final SolrService solrService; @@ -65,7 +64,7 @@ public SolrAdminController( /** * Ping solr. * - * @return the response entity (ok or an error response) + * @return the response entity (OK or an error response) */ @Operation(summary = "Ping Solr", description = "Ping Solr to check if it is available") @ApiResponse( diff --git a/src/main/java/org/tailormap/api/repository/ApplicationRepository.java b/src/main/java/org/tailormap/api/repository/ApplicationRepository.java index 7fe9fc88eb..e50bb30334 100644 --- a/src/main/java/org/tailormap/api/repository/ApplicationRepository.java +++ b/src/main/java/org/tailormap/api/repository/ApplicationRepository.java @@ -5,8 +5,11 @@ */ package org.tailormap.api.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.lang.NonNull; import org.springframework.security.access.prepost.PreAuthorize; import org.tailormap.api.persistence.Application; @@ -21,4 +24,17 @@ public interface ApplicationRepository extends JpaRepository @Override @NonNull Optional findById(@NonNull Long aLong); + + /** + * Find all applications that have a layer that is linked to a specific (Solr) index. + * + * @param indexId The index id to search for + */ + @NonNull + @PreAuthorize("permitAll()") + @Query( + value = + "select * from application app, lateral jsonb_path_query(app.settings, ('$.layerSettings.**{1}.searchIndexId ? (@ == '||:indexId||')')::jsonpath)", + nativeQuery = true) + List findByIndexId(@Param("indexId") @NonNull Long indexId); } diff --git a/src/main/java/org/tailormap/api/repository/events/SolrTMFeatureTypeEventHandler.java b/src/main/java/org/tailormap/api/repository/events/SolrTMFeatureTypeEventHandler.java new file mode 100644 index 0000000000..c14c61ed71 --- /dev/null +++ b/src/main/java/org/tailormap/api/repository/events/SolrTMFeatureTypeEventHandler.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2024 B3Partners B.V. + * + * SPDX-License-Identifier: MIT + */ +package org.tailormap.api.repository.events; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.common.SolrException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.rest.core.annotation.HandleAfterDelete; +import org.springframework.data.rest.core.annotation.HandleBeforeSave; +import org.springframework.data.rest.core.annotation.RepositoryEventHandler; +import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper; +import org.tailormap.api.persistence.SearchIndex; +import org.tailormap.api.persistence.TMFeatureType; +import org.tailormap.api.persistence.json.AppLayerSettings; +import org.tailormap.api.repository.ApplicationRepository; +import org.tailormap.api.repository.SearchIndexRepository; +import org.tailormap.api.solr.SolrHelper; +import org.tailormap.api.solr.SolrService; + +/** Event handler for Solr indexes when a {@code TMFeatureType} is updated or deleted. */ +@RepositoryEventHandler +public class SolrTMFeatureTypeEventHandler { + + private static final Logger logger = + LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final SearchIndexRepository searchIndexRepository; + private final SolrService solrService; + private final FeatureSourceFactoryHelper featureSourceFactoryHelper; + private final ApplicationRepository applicationRepository; + + public SolrTMFeatureTypeEventHandler( + SearchIndexRepository searchIndexRepository, + SolrService solrService, + FeatureSourceFactoryHelper featureSourceFactoryHelper, + ApplicationRepository applicationRepository) { + this.searchIndexRepository = searchIndexRepository; + this.solrService = solrService; + this.featureSourceFactoryHelper = featureSourceFactoryHelper; + this.applicationRepository = applicationRepository; + } + + /** + * Handle the update of a TMFeatureType. + * + * @param tmFeatureType the TMFeatureType to handle + */ + @HandleBeforeSave + public void handleTMFeatureTypeUpdate(TMFeatureType tmFeatureType) { + logger.debug("Handling TMFeatureType save event for: {}", tmFeatureType); + // determine if it is a new FT or an update + if (null == tmFeatureType.getId()) { + // do nothing as there is no index defined for a new feature type + logger.debug("New TMFeatureType: {}", tmFeatureType); + } else { + logger.debug("Updated TMFeatureType: {}", tmFeatureType); + searchIndexRepository.findByFeatureTypeId(tmFeatureType.getId()).stream() + .findAny() + .ifPresent( + searchIndex -> { + logger.debug( + "Updating search index {} for feature type: {}", + searchIndex.getName(), + searchIndex); + + try (SolrHelper solrHelper = + new SolrHelper(solrService.getSolrClientForIndexing())) { + solrHelper.addFeatureTypeIndex( + searchIndex, + tmFeatureType, + featureSourceFactoryHelper, + searchIndexRepository); + } catch (UnsupportedOperationException + | IOException + | SolrServerException + | SolrException e) { + logger.error("Error re-indexing", e); + searchIndex.setStatus(SearchIndex.Status.ERROR); + searchIndexRepository.save(searchIndex); + } + }); + } + } + + /** + * Handle the deletion of a TMFeatureType. + * + * @param tmFeatureType the TMFeatureType to handle + */ + @HandleAfterDelete + public void handleTMFeatureTypeDeleteForSolr(TMFeatureType tmFeatureType) { + logger.debug("Handling TMFeatureType delete event for: {}", tmFeatureType); + searchIndexRepository.findByFeatureTypeId(tmFeatureType.getId()).stream() + .findAny() + .ifPresent( + searchIndex -> { + logger.info( + "Deleting search index {} for feature type: {}", + searchIndex.getName(), + searchIndex); + + try (SolrHelper solrHelper = new SolrHelper(solrService.getSolrClientForIndexing())) { + solrHelper.clearIndexForLayer(searchIndex.getId()); + searchIndexRepository.delete(searchIndex); + // find any application layers that use this index clear the index from them + applicationRepository + .findByIndexId(searchIndex.getId()) + .forEach( + application -> { + application + .getAllAppTreeLayerNode() + .forEach( + appTreeLayerNode -> { + AppLayerSettings appLayerSettings = + application.getAppLayerSettings(appTreeLayerNode); + if (null != appLayerSettings.getSearchIndexId() + && appLayerSettings + .getSearchIndexId() + .equals(searchIndex.getId())) { + appLayerSettings.setSearchIndexId(null); + } + }); + applicationRepository.save(application); + }); + } catch (UnsupportedOperationException + | IOException + | SolrServerException + | SolrException e) { + logger.error("Error deleting index for {}", searchIndex, e); + } + }); + } +} diff --git a/src/main/java/org/tailormap/api/solr/SolrHelper.java b/src/main/java/org/tailormap/api/solr/SolrHelper.java index e3a29951f4..7d9167c05a 100644 --- a/src/main/java/org/tailormap/api/solr/SolrHelper.java +++ b/src/main/java/org/tailormap/api/solr/SolrHelper.java @@ -61,7 +61,7 @@ public class SolrHelper implements AutoCloseable, Constants { /** * Constructor * - * @param solrClient the Solr client, this will be closed when this class is closed + * @param solrClient the Solr client, this client will be closed when this class is closed. */ public SolrHelper(@NotNull SolrClient solrClient) { this.solrClient = solrClient; diff --git a/src/test/java/org/tailormap/api/repository/ApplicationRepositoryIntegrationTest.java b/src/test/java/org/tailormap/api/repository/ApplicationRepositoryIntegrationTest.java index 341dea5ff4..778dc5f3ab 100644 --- a/src/test/java/org/tailormap/api/repository/ApplicationRepositoryIntegrationTest.java +++ b/src/test/java/org/tailormap/api/repository/ApplicationRepositoryIntegrationTest.java @@ -8,7 +8,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.tailormap.api.annotation.PostgresIntegrationTest; @@ -30,4 +32,17 @@ void should_not_find_application_by_nonexistent_name() { Application a = applicationRepository.findByName("does-not-exist"); assertNull(a); } + + @Test + void it_should_find_application_using_findByIndexId_with_valid_ID() { + final Application application = applicationRepository.findByIndexId(2L).get(0); + assertNotNull(application, "application should not be null"); + assertEquals("default", application.getName(), "application name is incorrect"); + } + + @Test + void it_should_not_find_applications_findByIndexId_with_invalid_ID() { + final List applications = applicationRepository.findByIndexId(-2L); + assertTrue(applications.isEmpty(), "applications should be empty"); + } }