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 9d63faade..bc49af9ba 100644 --- a/src/main/java/org/tailormap/api/controller/admin/SolrAdminController.java +++ b/src/main/java/org/tailormap/api/controller/admin/SolrAdminController.java @@ -47,7 +47,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; @@ -75,7 +74,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 7fe9fc88e..e50bb3033 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 000000000..c14c61ed7 --- /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 5e4b902d8..d4d7c9841 100644 --- a/src/main/java/org/tailormap/api/solr/SolrHelper.java +++ b/src/main/java/org/tailormap/api/solr/SolrHelper.java @@ -107,7 +107,7 @@ public class SolrHelper implements AutoCloseable, Constants { /** * Create a configured {@code SolrHelper} object. * - * @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 341dea5ff..778dc5f3a 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"); + } }