From caa029997ccf7404c848bffcabca36e0cda128da Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Mon, 23 Jul 2018 17:16:25 -0700 Subject: [PATCH 1/7] [SHIBUI-660] First pass at switching our Spring Lucene configuration around to use multiple Directories and IndexWriters. Updated existing tests accordingly to make sure I didn't break anything. Note that this does not yet include writing to lucene as new resolvers are added. --- .../CoreShibUiConfiguration.java | 10 ++-- .../MetadataResolverConfiguration.java | 14 +++++- .../ui/configuration/SearchConfiguration.java | 44 ++++++++++++++--- .../controller/EntityIdsSearchController.java | 7 ++- .../admin/ui/service/DirectoryService.java | 16 +++++++ .../ui/service/EntityIdsSearchService.java | 3 +- .../admin/ui/service/IndexWriterService.java | 18 +++++++ .../ui/configuration/TestConfiguration.groovy | 14 ++++-- .../EntityIdsSearchControllerTests.groovy | 48 +++++++++---------- .../EntityIdsSearchServiceTests.groovy | 12 +++-- 10 files changed, 138 insertions(+), 48 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterService.java diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index edcfc4ccb..077ac1843 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -9,12 +9,14 @@ import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; +import org.apache.lucene.store.RAMDirectory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -28,7 +30,9 @@ import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Configuration public class CoreShibUiConfiguration { @@ -76,8 +80,7 @@ public AttributeUtility attributeUtility() { Analyzer fullTokenAnalyzer; @Autowired - Directory directory; - + DirectoryService directoryService; @Bean public EntityDescriptorFilesScheduledTasks entityDescriptorFilesScheduledTasks(EntityDescriptorRepository entityDescriptorRepository) { @@ -86,8 +89,9 @@ public EntityDescriptorFilesScheduledTasks entityDescriptorFilesScheduledTasks(E @Bean public EntityIdsSearchService entityIdsSearchService() { - return (term, limit) -> { + return (resourceId, term, limit) -> { List entityIds = new ArrayList<>(); + Directory directory = directoryService.getDirectory(resourceId); try { IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(directory)); QueryParser parser = new QueryParser("content", fullTokenAnalyzer); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java index 1968841c1..cdcd54f02 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java @@ -2,6 +2,7 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.resolver.ResolverException; import org.apache.http.HttpResponse; @@ -38,7 +39,7 @@ public class MetadataResolverConfiguration { OpenSamlObjects openSamlObjects; @Autowired - IndexWriter indexWriter; + IndexWriterService indexWriterService; @Autowired MetadataResolverRepository metadataResolverRepository; @@ -50,13 +51,22 @@ public MetadataResolver metadataResolver() throws ResolverException, ComponentIn List resolvers = new ArrayList<>(); + String incommonMRId = "incommonmd"; // TODO: remove this later when we allow for creation of arbitrary metadata resolvers FileBackedHTTPMetadataResolver incommonMR = new FileBackedHTTPMetadataResolver(HttpClients.createMinimal(), "http://md.incommon.org/InCommon/InCommon-metadata.xml", "/tmp/incommonmd.xml"){ @Override protected void initMetadataResolver() throws ComponentInitializationException { super.initMetadataResolver(); + IndexWriter indexWriter; + try { + indexWriter = indexWriterService.getIndexWriter(incommonMRId); + } catch (IOException e) { + throw new ComponentInitializationException(e); + } + for (String entityId: this.getBackingStore().getIndexedDescriptors().keySet()) { + Document document = new Document(); document.add(new StringField("id", entityId, Field.Store.YES)); document.add(new TextField("content", entityId, Field.Store.YES)); // TODO: change entityId to be content of entity descriptor block @@ -86,7 +96,7 @@ protected void processConditionalRetrievalHeaders(HttpResponse response) { // let's do nothing 'cause we want to allow a refresh } }; - incommonMR.setId("incommonmd"); + incommonMR.setId(incommonMRId); incommonMR.setParserPool(openSamlObjects.getParserPool()); incommonMR.setMetadataFilter(new MetadataFilterChain()); incommonMR.initialize(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java index ac5d1efb7..0ae1cc79f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java @@ -1,5 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; +import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService; +import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.LowerCaseFilter; import org.apache.lucene.analysis.StopFilter; @@ -11,17 +13,18 @@ import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.store.Directory; import org.apache.lucene.store.RAMDirectory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; @Configuration public class SearchConfiguration { - @Bean - Directory directory() { - return new RAMDirectory(); - } + @Autowired + DirectoryService directoryService; @Bean Analyzer analyzer() { @@ -54,10 +57,37 @@ protected TokenStreamComponents createComponents(String fieldName) { }; } - @Bean - IndexWriter indexWriter() throws IOException { + private IndexWriter createIndexWriter(Directory directory) throws IOException { IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer()); indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); - return new IndexWriter(directory(), indexWriterConfig); + return new IndexWriter(directory, indexWriterConfig); + } + + @Bean + public IndexWriterService indexWriterService() { + Map indexWriterMap = new HashMap<>(); + + return resourceId -> { + IndexWriter indexWriter = indexWriterMap.get(resourceId); + if (indexWriter == null) { + indexWriter = createIndexWriter(directoryService.getDirectory(resourceId)); + indexWriterMap.put(resourceId, indexWriter); + } + return indexWriter; + }; + } + + @Bean + public DirectoryService directoryService() { + Map directoryMap = new HashMap<>(); + + return resourceId -> { + Directory directory = directoryMap.get(resourceId); + if (directory == null) { + directory = new RAMDirectory(); + directoryMap.put(resourceId, directory); + } + return directory; + }; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java index 643e626f4..8728b838d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java @@ -18,10 +18,13 @@ public EntityIdsSearchController(EntityIdsSearchService entityIdsSearchService) this.entityIdsSearchService = entityIdsSearchService; } + // TODO Change this to include a metadata resolver id so we can pull out the right index to search @GetMapping - ResponseEntity search(@RequestParam String term, @RequestParam(required = false) Integer limit) { + ResponseEntity search(@RequestParam String resourceId, + @RequestParam String term, + @RequestParam(required = false) Integer limit) { //Zero indicates no-limit final int resultLimit = (limit != null ? limit : 10); - return ResponseEntity.ok(this.entityIdsSearchService.findBySearchTermAndOptionalLimit(term, resultLimit)); + return ResponseEntity.ok(this.entityIdsSearchService.findBySearchTermAndOptionalLimit(resourceId, term, resultLimit)); } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java new file mode 100644 index 000000000..720a64ca0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java @@ -0,0 +1,16 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import org.apache.lucene.store.Directory; + +/** + * API component responsible for entity ids search. + */ +@FunctionalInterface +public interface DirectoryService { + /** + * Return a Directory for a given resource id. If one is not found, it will be created. + * @param resourceId the resource to get the Directory for + * @return Directory + */ + Directory getDirectory(String resourceId); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchService.java index 81b8a4b1c..6639d5b1a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchService.java @@ -15,9 +15,10 @@ public interface EntityIdsSearchService { /** * Find a list of entity ids + * @param resourceId the id of the resource to search within * @param searchTerm for the query * @param limit optional limit of query results to return. Zero or less value indicates no limit. * @return EntityIdsSearchResultRepresentation */ - EntityIdsSearchResultRepresentation findBySearchTermAndOptionalLimit(String searchTerm, int limit); + EntityIdsSearchResultRepresentation findBySearchTermAndOptionalLimit(String resourceId, String searchTerm, int limit); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterService.java new file mode 100644 index 000000000..1e314ca4b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterService.java @@ -0,0 +1,18 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import org.apache.lucene.index.IndexWriter; + +import java.io.IOException; + +/** + * API component responsible for entity ids search. + */ +@FunctionalInterface +public interface IndexWriterService { + /** + * Return a (possibly cached) index writer for a given resource id. + * @param resourceId the resource to create the IndexWriter for + * @return IndexWriter + */ + IndexWriter getIndexWriter(String resourceId) throws IOException; +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy index 73cf58307..2cb3c7d9d 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService import net.shibboleth.ext.spring.resource.ResourceHelper import net.shibboleth.utilities.java.support.component.ComponentInitializationException import org.apache.lucene.document.Document @@ -14,20 +15,22 @@ import org.opensaml.saml.metadata.resolver.MetadataResolver import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver import org.slf4j.Logger import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.io.ClassPathResource @Configuration class TestConfiguration { + @Autowired + IndexWriterService indexWriterService + final OpenSamlObjects openSamlObjects - final IndexWriter indexWriter final MetadataResolverRepository metadataResolverRepository final Logger logger = LoggerFactory.getLogger(TestConfiguration.class); - TestConfiguration(final OpenSamlObjects openSamlObjects, final IndexWriter indexWriter, final MetadataResolverRepository metadataResolverRepository) { + TestConfiguration(final OpenSamlObjects openSamlObjects, final MetadataResolverRepository metadataResolverRepository) { this.openSamlObjects =openSamlObjects - this.indexWriter = indexWriter this.metadataResolverRepository = metadataResolverRepository } @@ -35,6 +38,9 @@ class TestConfiguration { MetadataResolver metadataResolver() { ChainingMetadataResolver metadataResolver = new ChainingMetadataResolver() metadataResolver.setId("chain") + String resolverId = "test" + + IndexWriter indexWriter = indexWriterService.getIndexWriter(resolverId) def shortIncommon = new ResourceBackedMetadataResolver(ResourceHelper.of(new ClassPathResource('/metadata/incommon-short.xml'))){ @Override @@ -58,7 +64,7 @@ class TestConfiguration { } } }.with { - it.id = 'test' + it.id = resolverId TestConfiguration p = owner it.parserPool = p.openSamlObjects.parserPool it.initialize() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchControllerTests.groovy index a24ed433f..77ea5ddd0 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchControllerTests.groovy @@ -25,11 +25,14 @@ class EntityIdsSearchControllerTests extends Specification { def mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + static final String RESOURCE_ID = "resourceId" + static final String RESOURCE_ID_VALUE = "test" + static final String TERM = "term" + static final String LIMIT = "limit" + def "GET /api/EntityIds/search with unicon and limit 5"() { given: - def term = "term" def termValue = "unicon" - def limit = "limit" def limitValue = "5" def expectedEntityIdsFromSearchService = new EntityIdsSearchResultRepresentation(["http://unicon.instructure.com/saml2", "https://idp.unicon.net/idp/shibboleth"]) def expectedHttpResponseStatus = status().isOk() @@ -45,12 +48,13 @@ class EntityIdsSearchControllerTests extends Specification { when: def result = mockMvc.perform(get('/api/EntityIds/search') - .param(term, termValue) - .param(limit, limitValue)) + .param(RESOURCE_ID, RESOURCE_ID_VALUE) + .param(TERM, termValue) + .param(LIMIT, limitValue)) then: result.andExpect(expectedHttpResponseStatus) - 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService + 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(RESOURCE_ID_VALUE, termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedResponseBody, true)) @@ -58,9 +62,7 @@ class EntityIdsSearchControllerTests extends Specification { def "GET /api/EntityIds/search with unicon and limit 1"() { given: - def term = "term" def termValue = "unicon" - def limit = "limit" def limitValue = "1" def expectedEntityIdsFromSearchService = new EntityIdsSearchResultRepresentation(["http://unicon.instructure.com/saml2"]) def expectedHttpResponseStatus = status().isOk() @@ -75,12 +77,13 @@ class EntityIdsSearchControllerTests extends Specification { when: def result = mockMvc.perform(get('/api/EntityIds/search') - .param(term, termValue) - .param(limit, limitValue)) + .param(RESOURCE_ID, RESOURCE_ID_VALUE) + .param(TERM, termValue) + .param(LIMIT, limitValue)) then: result.andExpect(expectedHttpResponseStatus) - 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService + 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(RESOURCE_ID_VALUE, termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedResponseBody, true)) @@ -88,9 +91,7 @@ class EntityIdsSearchControllerTests extends Specification { def "GET /api/EntityIds/search with shib and no limit"() { given: - def term = "term" def termValue = "shib" - def limit = "limit" def limitValue = NO_LIMIT def expectedEntityIdsFromSearchService = new EntityIdsSearchResultRepresentation(["https://shib.ucanr.org/shibboleth", "https://shibboleth2sp.tf.semcs.net/shibboleth", @@ -123,11 +124,12 @@ class EntityIdsSearchControllerTests extends Specification { when: def result = mockMvc.perform(get('/api/EntityIds/search') - .param(term, termValue)) + .param(RESOURCE_ID, RESOURCE_ID_VALUE) + .param(TERM, termValue)) then: result.andExpect(expectedHttpResponseStatus) - 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService + 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(RESOURCE_ID_VALUE, termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedResponseBody, true)) @@ -135,9 +137,7 @@ class EntityIdsSearchControllerTests extends Specification { def "GET /api/EntityIds/search with empty term and limit 5"() { given: - def term = "term" def termValue = "" - def limit = "limit" def limitValue = "5" def expectedEntityIdsFromSearchService = new EntityIdsSearchResultRepresentation([]) def expectedHttpResponseStatus = status().isOk() @@ -150,12 +150,13 @@ class EntityIdsSearchControllerTests extends Specification { when: def result = mockMvc.perform(get('/api/EntityIds/search') - .param(term, termValue) - .param(limit, limitValue)) + .param(RESOURCE_ID, RESOURCE_ID_VALUE) + .param(TERM, termValue) + .param(LIMIT, limitValue)) then: result.andExpect(expectedHttpResponseStatus) - 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService + 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(RESOURCE_ID_VALUE, termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedResponseBody, true)) @@ -163,9 +164,7 @@ class EntityIdsSearchControllerTests extends Specification { def "GET /api/EntityIds/search with empty term and no limit"() { given: - def term = "term" def termValue = "" - def limit = "limit" def limitValue = NO_LIMIT def expectedEntityIdsFromSearchService = new EntityIdsSearchResultRepresentation([]) def expectedHttpResponseStatus = status().isOk() @@ -178,12 +177,13 @@ class EntityIdsSearchControllerTests extends Specification { when: def result = mockMvc.perform(get('/api/EntityIds/search') - .param(term, termValue) - .param(limit, limitValue)) + .param(RESOURCE_ID, RESOURCE_ID_VALUE) + .param(TERM, termValue) + .param(LIMIT, limitValue)) then: result.andExpect(expectedHttpResponseStatus) - 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService + 1 * entityIdsSearchService.findBySearchTermAndOptionalLimit(RESOURCE_ID_VALUE, termValue, Integer.valueOf(limitValue)) >> expectedEntityIdsFromSearchService result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedResponseBody, true)) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy index 55777e6ec..833e67156 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy @@ -22,6 +22,8 @@ class EntityIdsSearchServiceTests extends Specification { @Autowired EntityIdsSearchService entityIdsSearchService + def test = "test" + def "searching for carmen produces one result"() { setup: def searchTerm = "carmen" @@ -30,7 +32,7 @@ class EntityIdsSearchServiceTests extends Specification { def expectedResultItem = "https://carmenwiki.osu.edu/shibboleth" when: - def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(searchTerm, searchLimit) + def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(test, searchTerm, searchLimit) then: expectedResultSize == actualResults.entityIds.size() @@ -45,7 +47,7 @@ class EntityIdsSearchServiceTests extends Specification { def expectedResults = Arrays.asList(["http://unicon.instructure.com/saml2", "https://idp.unicon.net/idp/shibboleth"]) when: - def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(searchTerm, searchLimit) + def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(test, searchTerm, searchLimit) then: expectedResultSize == actualResults.entityIds.size() @@ -59,7 +61,7 @@ class EntityIdsSearchServiceTests extends Specification { def expectedResultSize = 0 when: - def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(searchTerm, searchLimit) + def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(test, searchTerm, searchLimit) then: expectedResultSize == actualResults.entityIds.size() @@ -73,7 +75,7 @@ class EntityIdsSearchServiceTests extends Specification { def expectedResults = Arrays.asList(["http://unicon.instructure.com/saml2"]) when: - def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(searchTerm, searchLimit) + def actualResults = entityIdsSearchService.findBySearchTermAndOptionalLimit(test, searchTerm, searchLimit) then: expectedResultSize == actualResults.entityIds.size() @@ -86,7 +88,7 @@ class EntityIdsSearchServiceTests extends Specification { def searchLimit = 0 when: - entityIdsSearchService.findBySearchTermAndOptionalLimit(searchTerm, searchLimit) + entityIdsSearchService.findBySearchTermAndOptionalLimit(test, searchTerm, searchLimit) then: thrown IllegalArgumentException From 14623679f0974bfb59524076fe801a1bc5935eea Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Tue, 24 Jul 2018 11:00:48 -0700 Subject: [PATCH 2/7] [SHIBUI-660] Lucene updates WIP. --- .../MetadataResolversController.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index 9705ddacc..cc8f45a76 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -4,8 +4,14 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; import lombok.extern.slf4j.Slf4j; +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.IndexWriter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -47,6 +53,9 @@ public class MetadataResolversController { @Autowired MetadataResolverService metadataResolverService; + @Autowired + IndexWriterService indexWriterService; + @ExceptionHandler({InvalidTypeIdException.class, IOException.class, HttpMessageNotReadableException.class}) public ResponseEntity unableToParseJson(Exception ex) { return ResponseEntity.badRequest().body(new ErrorResponse(HttpStatus.BAD_REQUEST.toString(), ex.getMessage())); @@ -150,4 +159,41 @@ private static URI getResourceUriFor(MetadataResolver resolver) { .build() .toUri(); } + + private void updateLucene(MetadataResolver resolver) throws ComponentInitializationException { + IndexWriter indexWriter = null; + try { + indexWriter = indexWriterService.getIndexWriter(resolver.getResourceId()); + } catch (IOException e) { + throw new ComponentInitializationException(e); + } + + // add documents to indexWriter .. for each what? + /* + for () { + Document document = new Document(); + document.add(new StringField("id", entityId, Field.Store.YES)); + document.add(new TextField("content", entityId, Field.Store.YES)); // TODO: change entityId to be content of entity descriptor block + try { + indexWriter.addDocument(document); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + */ + + Document document = new Document(); + document.add(new StringField("id", resolver.getResourceId(), Field.Store.YES)); + try { + indexWriter.addDocument(document); + } catch (IOException e) { + throw new ComponentInitializationException(e); + } + + try { + indexWriter.commit(); + } catch (IOException e) { + throw new ComponentInitializationException(e); + } + } } From e6f5566019b8ce4c86b181c760f24f1a1abe815e Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Tue, 31 Jul 2018 10:34:02 -0700 Subject: [PATCH 3/7] [SHIBUI-660] Added some comments for future Bill. --- .../admin/ui/controller/MetadataResolversController.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index cc8f45a76..ac1339201 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -182,8 +182,12 @@ private void updateLucene(MetadataResolver resolver) throws ComponentInitializat } */ + // if document exists + // indexWriter.updateDocument(term, document) <-- what's the term? + // else, create... Document document = new Document(); document.add(new StringField("id", resolver.getResourceId(), Field.Store.YES)); + try { indexWriter.addDocument(document); } catch (IOException e) { From aa4aba9fece633e82781068d99d0e98e6d13271d Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Mon, 20 Aug 2018 11:26:22 -0700 Subject: [PATCH 4/7] [SHIBUI-660] Removed unneeded method. Updating lucene is handled in 723. --- .../MetadataResolversController.java | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index ea70a4fab..b6e1f0538 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -156,45 +156,4 @@ private static URI getResourceUriFor(MetadataResolver resolver) { .build() .toUri(); } - - private void updateLucene(MetadataResolver resolver) throws ComponentInitializationException { - IndexWriter indexWriter = null; - try { - indexWriter = indexWriterService.getIndexWriter(resolver.getResourceId()); - } catch (IOException e) { - throw new ComponentInitializationException(e); - } - - // add documents to indexWriter .. for each what? - /* - for () { - Document document = new Document(); - document.add(new StringField("id", entityId, Field.Store.YES)); - document.add(new TextField("content", entityId, Field.Store.YES)); // TODO: change entityId to be content of entity descriptor block - try { - indexWriter.addDocument(document); - } catch (IOException e) { - logger.error(e.getMessage(), e); - } - } - */ - - // if document exists - // indexWriter.updateDocument(term, document) <-- what's the term? - // else, create... - Document document = new Document(); - document.add(new StringField("id", resolver.getResourceId(), Field.Store.YES)); - - try { - indexWriter.addDocument(document); - } catch (IOException e) { - throw new ComponentInitializationException(e); - } - - try { - indexWriter.commit(); - } catch (IOException e) { - throw new ComponentInitializationException(e); - } - } } From d6afdd0df1febbbb47ab6246c62095415a86a03b Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Mon, 20 Aug 2018 11:35:19 -0700 Subject: [PATCH 5/7] [SHIBUI-660] Removed outdated TODO. --- .../admin/ui/controller/EntityIdsSearchController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java index 8728b838d..2e910f365 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java @@ -18,7 +18,6 @@ public EntityIdsSearchController(EntityIdsSearchService entityIdsSearchService) this.entityIdsSearchService = entityIdsSearchService; } - // TODO Change this to include a metadata resolver id so we can pull out the right index to search @GetMapping ResponseEntity search(@RequestParam String resourceId, @RequestParam String term, From d8e283080fcd57b041daaa44df06cf762a71332d Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Mon, 20 Aug 2018 14:33:12 -0700 Subject: [PATCH 6/7] [SHIBUI-660] Added LuceneUtility for getting an IndexReader, given a resourceId. Converted DirectoryService from a functional interface to a plain service. Added support for search by empty resourceId (search all). Updated everything tests to use this new code. --- .../CoreShibUiConfiguration.java | 42 ++++++++++++----- .../ui/configuration/SearchConfiguration.java | 15 ------- .../controller/EntityIdsSearchController.java | 2 +- .../admin/ui/service/DirectoryService.java | 5 ++- .../ui/service/DirectoryServiceImpl.java | 30 +++++++++++++ .../shibboleth/admin/util/LuceneUtility.java | 45 +++++++++++++++++++ 6 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryServiceImpl.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/LuceneUtility.java diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index 5b9d330bf..18a8a4953 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -1,25 +1,36 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityIdsSearchResultRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; import edu.internet2.tier.shibboleth.admin.ui.scheduled.EntityDescriptorFilesScheduledTasks; -import edu.internet2.tier.shibboleth.admin.ui.service.*; +import edu.internet2.tier.shibboleth.admin.ui.service.DefaultMetadataResolversPositionOrderContainerService; +import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService; +import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityIdsSearchService; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService; +import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; +import edu.internet2.tier.shibboleth.admin.ui.service.FilterTargetService; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterTargetServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.JPAMetadataResolverServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; +import edu.internet2.tier.shibboleth.admin.util.LuceneUtility; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; -import org.apache.lucene.index.DirectoryReader; -import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexReader; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; -import org.apache.lucene.store.Directory; -import org.apache.lucene.store.RAMDirectory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -37,9 +48,7 @@ import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; @Configuration public class CoreShibUiConfiguration { @@ -95,6 +104,9 @@ public AttributeUtility attributeUtility() { @Autowired ResourceBundleMessageSource messageSource; + @Autowired + LuceneUtility luceneUtility; + @Bean public EntityDescriptorFilesScheduledTasks entityDescriptorFilesScheduledTasks(EntityDescriptorRepository entityDescriptorRepository) { return new EntityDescriptorFilesScheduledTasks(this.metadataDir, entityDescriptorRepository, openSamlObjects()); @@ -104,9 +116,9 @@ public EntityDescriptorFilesScheduledTasks entityDescriptorFilesScheduledTasks(E public EntityIdsSearchService entityIdsSearchService() { return (resourceId, term, limit) -> { List entityIds = new ArrayList<>(); - Directory directory = directoryService.getDirectory(resourceId); try { - IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(directory)); + IndexReader indexReader = luceneUtility.getIndexReader(resourceId); + IndexSearcher searcher = new IndexSearcher(indexReader); QueryParser parser = new QueryParser("content", fullTokenAnalyzer); TopDocs topDocs = searcher.search(parser.parse(term.trim()), limit); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { @@ -180,4 +192,14 @@ public void addInterceptors(InterceptorRegistry registry) { return new DefaultMetadataResolversPositionOrderContainerService(positionOrderContainerRepository, resolverRepository); } + + @Bean + public DirectoryService directoryService() { + return new DirectoryServiceImpl(); + } + + @Bean + public LuceneUtility luceneUtility() { + return new LuceneUtility(); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java index 0ae1cc79f..fab39d551 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/SearchConfiguration.java @@ -12,7 +12,6 @@ import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.store.Directory; -import org.apache.lucene.store.RAMDirectory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -76,18 +75,4 @@ public IndexWriterService indexWriterService() { return indexWriter; }; } - - @Bean - public DirectoryService directoryService() { - Map directoryMap = new HashMap<>(); - - return resourceId -> { - Directory directory = directoryMap.get(resourceId); - if (directory == null) { - directory = new RAMDirectory(); - directoryMap.put(resourceId, directory); - } - return directory; - }; - } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java index 2e910f365..034556c92 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityIdsSearchController.java @@ -19,7 +19,7 @@ public EntityIdsSearchController(EntityIdsSearchService entityIdsSearchService) } @GetMapping - ResponseEntity search(@RequestParam String resourceId, + ResponseEntity search(@RequestParam(required = false) String resourceId, @RequestParam String term, @RequestParam(required = false) Integer limit) { //Zero indicates no-limit diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java index 720a64ca0..a3cf9fe2f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryService.java @@ -2,10 +2,11 @@ import org.apache.lucene.store.Directory; +import java.util.List; + /** * API component responsible for entity ids search. */ -@FunctionalInterface public interface DirectoryService { /** * Return a Directory for a given resource id. If one is not found, it will be created. @@ -13,4 +14,6 @@ public interface DirectoryService { * @return Directory */ Directory getDirectory(String resourceId); + + List getDirectories(); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryServiceImpl.java new file mode 100644 index 000000000..553af3094 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DirectoryServiceImpl.java @@ -0,0 +1,30 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.RAMDirectory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +public class DirectoryServiceImpl implements DirectoryService { + private Map directoryMap = new HashMap<>(); + + @Override + public Directory getDirectory(String resourceId) { + Directory directory = directoryMap.get(resourceId); + if (directory == null) { + directory = new RAMDirectory(); + directoryMap.put(resourceId, directory); + } + return directory; + } + + @Override + public List getDirectories() { + return (List) directoryMap.values(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/LuceneUtility.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/LuceneUtility.java new file mode 100644 index 000000000..9b53ebdbb --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/LuceneUtility.java @@ -0,0 +1,45 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService; +import org.apache.commons.lang.StringUtils; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.MultiReader; +import org.apache.lucene.store.Directory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +public class LuceneUtility { + private static final Logger logger = LoggerFactory.getLogger(LuceneUtility.class); + + @Autowired + private DirectoryService directoryService; + + public IndexReader getIndexReader(String resourceId) throws IOException { + IndexReader indexReader; + if (StringUtils.isBlank(resourceId)) { + List directories = directoryService.getDirectories(); + List indexReaderList = new ArrayList<>(); + directories.forEach(it -> { + try { + indexReaderList.add(DirectoryReader.open(it)); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + }); + IndexReader[] indexReaders = (IndexReader[]) indexReaderList.toArray(); + indexReader = new MultiReader(indexReaders, true); + } else { + indexReader = DirectoryReader.open(directoryService.getDirectory(resourceId)); + } + return indexReader; + } +} From b9259cfc239ff989fbafee7d26f607f8b08ef1d6 Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Wed, 22 Aug 2018 10:09:57 -0700 Subject: [PATCH 7/7] [SHIBUI-660] Added simple test to check for index writer hashmap usage. Scope appears to be as intended! --- .../ui/service/IndexWriterServiceTests.groovy | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy new file mode 100644 index 000000000..65ec74bc1 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy @@ -0,0 +1,37 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class IndexWriterServiceTests extends Specification { + + @Autowired + IndexWriterService service + + def "retrieving index writer for the same resource id multiple times results in the same index writer being returned"() { + given: + def resourceId = "12345" + + when: + def firstIndexWriter = service.getIndexWriter(resourceId) // causes new index writer to be created and added to map + def secondIndexWriter = service.getIndexWriter(resourceId) // retrieves the same index writer from above + + then: + firstIndexWriter == secondIndexWriter + } +}