diff --git a/pom.xml b/pom.xml index 1849179..3d90867 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,16 @@ spring-data-commons 3.3.0 + + org.springframework.boot + spring-boot-starter-cache + + + + com.github.ben-manes.caffeine + caffeine + 3.1.8 + org.postgresql diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/DictionaryApplication.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/DictionaryApplication.java index c12b6eb..40fee56 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/DictionaryApplication.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/DictionaryApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication +@EnableCaching public class DictionaryApplication { public static void main(String[] args) { diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptService.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptService.java index f22b65e..edd1e4d 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptService.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptService.java @@ -6,6 +6,7 @@ import edu.harvard.dbmi.avillach.dictionary.concept.model.ContinuousConcept; import edu.harvard.dbmi.avillach.dictionary.filter.Filter; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -26,6 +27,7 @@ public ConceptService(ConceptRepository conceptRepository, ConceptDecoratorServi this.conceptDecoratorService = conceptDecoratorService; } + @Cacheable("concepts") public List listConcepts(Filter filter, Pageable page) { return conceptRepository.getConcepts(filter, page); } @@ -40,6 +42,7 @@ public List listDetailedConcepts(Filter filter, Pageable page) { }).toList(); } + @Cacheable("concepts_count") public long countConcepts(Filter filter) { return conceptRepository.countConcepts(filter); } diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetService.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetService.java index b330865..3ad2aac 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetService.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetService.java @@ -2,6 +2,7 @@ import edu.harvard.dbmi.avillach.dictionary.filter.Filter; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.List; @@ -17,6 +18,7 @@ public FacetService(FacetRepository repository) { this.repository = repository; } + @Cacheable("facets") public List getFacets(Filter filter) { return repository.getFacets(filter); } diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FilterPreProcessor.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FilterPreProcessor.java index 483dc02..98357dc 100644 --- a/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FilterPreProcessor.java +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FilterPreProcessor.java @@ -10,6 +10,10 @@ import java.io.IOException; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; @ControllerAdvice public class FilterPreProcessor implements RequestBodyAdvice { @@ -30,8 +34,23 @@ public Object afterBodyRead( Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class> converterType ) { - if (body instanceof Filter filter && StringUtils.hasLength(filter.search())) { - return new Filter(filter.facets(), filter.search().replaceAll("_", "/"), filter.consents()); + if (body instanceof Filter filter) { + List newFacets = filter.facets(); + List newConsents = filter.consents(); + if (filter.facets() != null) { + newFacets = new ArrayList<>(filter.facets()); + newFacets.sort(Comparator.comparing(Facet::name)); + } + if (filter.consents() != null) { + newConsents = new ArrayList<>(newConsents); + newConsents.sort(Comparator.comparing(Function.identity())); + } + filter = new Filter(newFacets, filter.search(), newConsents); + + if (StringUtils.hasLength(filter.search())) { + filter = new Filter(filter.facets(), filter.search().replaceAll("_", "/"), filter.consents()); + } + return filter; } return body; } diff --git a/src/main/java/edu/harvard/dbmi/avillach/dictionary/util/CacheConfig.java b/src/main/java/edu/harvard/dbmi/avillach/dictionary/util/CacheConfig.java new file mode 100644 index 0000000..a383929 --- /dev/null +++ b/src/main/java/edu/harvard/dbmi/avillach/dictionary/util/CacheConfig.java @@ -0,0 +1,24 @@ +package edu.harvard.dbmi.avillach.dictionary.util; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +@Configuration +public class CacheConfig { + @Bean + public Caffeine caffeineConfig() { + return Caffeine.newBuilder().expireAfterAccess(15, TimeUnit.MINUTES).maximumSize(5000); + } + + @Bean + public CacheManager cacheManager(Caffeine caffeine) { + CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); + caffeineCacheManager.setCaffeine(caffeine); + return caffeineCacheManager; + } +} diff --git a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptServiceTest.java b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptServiceTest.java index ba1b9cb..0238e0c 100644 --- a/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptServiceTest.java +++ b/src/test/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptServiceTest.java @@ -6,6 +6,7 @@ import edu.harvard.dbmi.avillach.dictionary.concept.model.ContinuousConcept; import edu.harvard.dbmi.avillach.dictionary.filter.Filter; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; @@ -38,6 +39,8 @@ void shouldListConcepts() { Mockito.when(repository.getConcepts(filter, page)).thenReturn(expected); List actual = subject.listConcepts(filter, page); + subject.listConcepts(filter, page); + Mockito.verify(repository, Mockito.times(1)).getConcepts(filter, page); Assertions.assertEquals(expected, actual); } @@ -48,7 +51,9 @@ void shouldCountConcepts() { Mockito.when(repository.countConcepts(filter)).thenReturn(1L); long actual = subject.countConcepts(filter); + subject.countConcepts(filter); + Mockito.verify(repository, Mockito.times(1)).countConcepts(filter); Assertions.assertEquals(1L, actual); } diff --git a/src/test/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetServiceTest.java b/src/test/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetServiceTest.java index 84da8a8..e9c3aab 100644 --- a/src/test/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetServiceTest.java +++ b/src/test/java/edu/harvard/dbmi/avillach/dictionary/facet/FacetServiceTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.mockito.internal.verification.Times; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -28,6 +29,8 @@ void shouldGetFacets() { Mockito.when(repository.getFacets(filter)).thenReturn(expected); List actual = subject.getFacets(filter); + subject.getFacets(filter); + Mockito.verify(repository, Mockito.times(1)).getFacets(filter); Assertions.assertEquals(expected, actual); } diff --git a/src/test/java/edu/harvard/dbmi/avillach/dictionary/facet/FilterPreProcessorTest.java b/src/test/java/edu/harvard/dbmi/avillach/dictionary/facet/FilterPreProcessorTest.java index 5fa6150..6d3d452 100644 --- a/src/test/java/edu/harvard/dbmi/avillach/dictionary/facet/FilterPreProcessorTest.java +++ b/src/test/java/edu/harvard/dbmi/avillach/dictionary/facet/FilterPreProcessorTest.java @@ -18,6 +18,19 @@ class FilterPreProcessorTest { @Autowired private FilterPreProcessor subject; + @Test + void shouldSortFilter() { + Filter filter = new Filter(List.of(new Facet("b", ""), new Facet("a", "")), "", List.of("c", "b", "a")); + Filter actual = (Filter) subject.afterBodyRead( + filter, Mockito.mock(HttpInputMessage.class), Mockito.mock(MethodParameter.class), SimpleType.constructUnsafe(Filter.class), + null + ); + + Filter expected = new Filter(List.of(new Facet("a", ""), new Facet("b", "")), "", List.of("a", "b", "c")); + Assertions.assertEquals(expected, actual); + + } + @Test void shouldProcessFilter() { Object processedFilter = subject.afterBodyRead(