diff --git a/.editorconfig b/.editorconfig
index 88bf97c508..eaad2c21c8 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -4,7 +4,7 @@
root = true
[*]
-indent_style = space
+indent_style = tab
indent_size = 2
end_of_line = crlf
charset = utf-8
diff --git a/pom.xml b/pom.xml
index 184483c9e7..222100829e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -236,6 +236,10 @@
info
warn
+
+ jcache
+
+
10
20
2147483647
@@ -272,6 +276,7 @@
3
true
+ true
47
@@ -744,6 +749,10 @@
provided
+ javax.cache
+ cache-api
+ 1.1.1
+
org.ohdsi.sql
SqlRender
${SqlRender.version}
@@ -1202,6 +1211,10 @@
1.1.7
+ org.ehcache
+ ehcache
+ 3.9.11
+
com.opentable.components
otj-pg-embedded
0.13.1
diff --git a/src/main/java/org/ohdsi/webapi/CacheConfig.java b/src/main/java/org/ohdsi/webapi/CacheConfig.java
new file mode 100644
index 0000000000..8e9eecf418
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/CacheConfig.java
@@ -0,0 +1,10 @@
+package org.ohdsi.webapi;
+
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableCaching
+public class CacheConfig {
+
+}
diff --git a/src/main/java/org/ohdsi/webapi/JerseyConfig.java b/src/main/java/org/ohdsi/webapi/JerseyConfig.java
index 91dbd2d15f..eabc3f8189 100644
--- a/src/main/java/org/ohdsi/webapi/JerseyConfig.java
+++ b/src/main/java/org/ohdsi/webapi/JerseyConfig.java
@@ -37,6 +37,7 @@
import javax.inject.Singleton;
import javax.ws.rs.ext.RuntimeDelegate;
+import org.ohdsi.webapi.cache.CacheService;
/**
*
@@ -59,31 +60,32 @@ public JerseyConfig() {
public void afterPropertiesSet() throws Exception {
packages(this.rootPackage);
register(ActivityService.class);
+ register(CacheService.class);
+ register(CcController.class);
register(CDMResultsService.class);
register(CohortAnalysisService.class);
register(CohortDefinitionService.class);
register(CohortResultsService.class);
register(CohortService.class);
register(ConceptSetService.class);
+ register(DDLService.class);
register(EvidenceService.class);
register(FeasibilityService.class);
+ register(FeatureExtractionService.class);
register(InfoService.class);
register(IRAnalysisResource.class);
register(JobService.class);
+ register(MultiPartFeature.class);
+ register(PermissionController.class);
register(PersonService.class);
+ register(ScriptExecutionController.class);
+ register(ScriptExecutionCallbackController.class);
register(SourceController.class);
register(SqlRenderService.class);
- register(DDLService.class);
+ register(SSOController.class);
register(TherapyPathResultsService.class);
register(UserService.class);
register(VocabularyService.class);
- register(ScriptExecutionController.class);
- register(ScriptExecutionCallbackController.class);
- register(MultiPartFeature.class);
- register(FeatureExtractionService.class);
- register(CcController.class);
- register(SSOController.class);
- register(PermissionController.class);
register(new AbstractBinder() {
@Override
protected void configure() {
diff --git a/src/main/java/org/ohdsi/webapi/cache/CacheInfo.java b/src/main/java/org/ohdsi/webapi/cache/CacheInfo.java
new file mode 100644
index 0000000000..d1ebd357d8
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/cache/CacheInfo.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 cknoll1.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.ohdsi.webapi.cache;
+
+import java.io.Serializable;
+import java.util.Optional;
+import javax.cache.management.CacheStatisticsMXBean;
+
+/**
+ *
+ * @author cknoll1
+ */
+public class CacheInfo implements Serializable{
+ public String cacheName;
+ public Long entries;
+ public CacheStatisticsMXBean cacheStatistics;
+}
diff --git a/src/main/java/org/ohdsi/webapi/cache/CacheService.java b/src/main/java/org/ohdsi/webapi/cache/CacheService.java
new file mode 100644
index 0000000000..86875fbd51
--- /dev/null
+++ b/src/main/java/org/ohdsi/webapi/cache/CacheService.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2019 cknoll1.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.ohdsi.webapi.cache;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.StreamSupport;
+import javax.cache.Cache;
+import javax.cache.CacheManager;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import org.ohdsi.webapi.util.CacheHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ * @author cknoll1
+ */
+@Path("/cache")
+@Component
+public class CacheService {
+
+ public static class ClearCacheResult {
+
+ public List clearedCaches;
+
+ private ClearCacheResult() {
+ this.clearedCaches = new ArrayList<>();
+ }
+ }
+
+ private CacheManager cacheManager;
+
+ @Autowired(required = false)
+ public CacheService(CacheManager cacheManager) {
+
+ this.cacheManager = cacheManager;
+ }
+
+ public CacheService() {
+ }
+
+
+ @GET
+ @Path("/")
+ @Produces(MediaType.APPLICATION_JSON)
+ public List getCacheInfoList() {
+ List caches = new ArrayList<>();
+
+ if (cacheManager == null) return caches; //caching is disabled
+
+ for (String cacheName : cacheManager.getCacheNames()) {
+ Cache cache = cacheManager.getCache(cacheName);
+ CacheInfo info = new CacheInfo();
+ info.cacheName = cacheName;
+ info.entries = StreamSupport.stream(cache.spliterator(), false).count();
+ info.cacheStatistics = CacheHelper.getCacheStats(cacheManager , cacheName);
+ caches.add(info);
+ }
+ return caches;
+ }
+ @GET
+ @Path("/clear")
+ @Produces(MediaType.APPLICATION_JSON)
+ public ClearCacheResult clearAll() {
+ ClearCacheResult result = new ClearCacheResult();
+
+ for (String cacheName : cacheManager.getCacheNames()) {
+ Cache cache = cacheManager.getCache(cacheName);
+ CacheInfo info = new CacheInfo();
+ info.cacheName = cacheName;
+ info.entries = StreamSupport.stream(cache.spliterator(), false).count();
+ result.clearedCaches.add(info);
+ cache.clear();
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/org/ohdsi/webapi/security/PermissionService.java b/src/main/java/org/ohdsi/webapi/security/PermissionService.java
index cc510579dd..2d6aa35c50 100644
--- a/src/main/java/org/ohdsi/webapi/security/PermissionService.java
+++ b/src/main/java/org/ohdsi/webapi/security/PermissionService.java
@@ -62,6 +62,9 @@ public class PermissionService {
@Value("#{!'${security.provider}'.equals('DisabledSecurity')}")
private boolean securityEnabled;
+ @Value("${security.defaultGlobalReadPermissions}")
+ private boolean defaultGlobalReadPermissions;
+
private final EntityGraph PERMISSION_ENTITY_GRAPH = EntityGraphUtils.fromAttributePaths("rolePermissions", "rolePermissions.role");
public PermissionService(
@@ -227,4 +230,17 @@ public void fillReadAccess(CommonEntity entity, CommonEntityDTO entityDTO) {
public boolean isSecurityEnabled() {
return this.securityEnabled;
}
+
+ // Use this key for cache (asset lists) that may be associated to a user or shared across users.
+ public String getAssetListCacheKey() {
+ if (this.isSecurityEnabled() && !defaultGlobalReadPermissions)
+ return permissionManager.getSubjectName();
+ else
+ return "ALL_USERS";
+ }
+
+ // use this cache key when the cache is associated to a user
+ public String getSubjectCacheKey() {
+ return this.isSecurityEnabled() ? permissionManager.getSubjectName() : "ALL_USERS";
+ }
}
diff --git a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java
index 4de4e872e6..867635d64c 100644
--- a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java
+++ b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java
@@ -131,6 +131,8 @@
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
+import javax.cache.CacheManager;
+import javax.cache.configuration.MutableConfiguration;
import javax.ws.rs.core.Response.ResponseBuilder;
import static org.ohdsi.webapi.Constants.Params.COHORT_DEFINITION_ID;
@@ -138,6 +140,9 @@
import static org.ohdsi.webapi.Constants.Params.SOURCE_ID;
import org.ohdsi.webapi.source.SourceService;
import static org.ohdsi.webapi.util.SecurityUtils.whitelist;
+import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
/**
* Provides REST services for working with cohort definitions.
@@ -149,6 +154,24 @@
@Component
public class CohortDefinitionService extends AbstractDaoService implements HasTags {
+ //create cache
+ @Component
+ public static class CachingSetup implements JCacheManagerCustomizer {
+
+ public static final String COHORT_DEFINITION_LIST_CACHE = "cohortDefinitionList";
+
+ @Override
+ public void customize(CacheManager cacheManager) {
+ // Evict when a cohort definition is created or updated, or permissions, or tags
+ if (!CacheHelper.getCacheNames(cacheManager).contains(COHORT_DEFINITION_LIST_CACHE)) {
+ cacheManager.createCache(COHORT_DEFINITION_LIST_CACHE, new MutableConfiguration>()
+ .setTypes(String.class, (Class>) (Class>) List.class)
+ .setStoreByValue(false)
+ .setStatisticsEnabled(true));
+ }
+ }
+ }
+
private static final CohortExpressionQueryBuilder queryBuilder = new CohortExpressionQueryBuilder();
@Autowired
@@ -205,7 +228,7 @@ public class CohortDefinitionService extends AbstractDaoService implements HasTa
@Autowired
private VersionService versionService;
- @Value("${security.defaultGlobalReadPermissions}")
+ @Value("${security.defaultGlobalReadPermissions}")
private boolean defaultGlobalReadPermissions;
private final MarkdownRender markdownPF = new MarkdownRender();
@@ -408,6 +431,7 @@ public GenerateSqlResult generateSql(GenerateSqlRequest request) {
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@Transactional
+ @Cacheable(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()")
public List getCohortDefinitionList() {
List definitions = cohortDefinitionRepository.list();
return definitions.stream()
@@ -436,6 +460,7 @@ public List getCohortDefinitionList() {
@Transactional
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
+ @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
public CohortDTO createCohortDefinition(CohortDTO dto) {
Date currentTime = Calendar.getInstance().getTime();
@@ -538,6 +563,7 @@ public int getCountCDefWithSameName(@PathParam("id") @DefaultValue("0") final in
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Transactional
+ @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
public CohortDTO saveCohortDefinition(@PathParam("id") final int id, CohortDTO def) {
Date currentTime = Calendar.getInstance().getTime();
@@ -670,6 +696,7 @@ public List getInfo(@PathParam("id") final int id) {
@Produces(MediaType.APPLICATION_JSON)
@Path("/{id}/copy")
@Transactional
+ @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
public CohortDTO copy(@PathParam("id") final int id) {
CohortDTO sourceDef = getCohortDefinition(id);
sourceDef.setId(null); // clear the ID
@@ -954,6 +981,7 @@ private Response printFrindly(String markdown, String format) {
@POST
@Produces(MediaType.APPLICATION_JSON)
@Path("/{id}/tag/")
+ @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
@Transactional
public void assignTag(@PathParam("id") final Integer id, final int tagId) {
CohortDefinition entity = cohortDefinitionRepository.findOne(id);
@@ -971,6 +999,7 @@ public void assignTag(@PathParam("id") final Integer id, final int tagId) {
@DELETE
@Produces(MediaType.APPLICATION_JSON)
@Path("/{id}/tag/{tagId}")
+ @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
@Transactional
public void unassignTag(@PathParam("id") final Integer id, @PathParam("tagId") final int tagId) {
CohortDefinition entity = cohortDefinitionRepository.findOne(id);
@@ -1106,6 +1135,7 @@ public void deleteVersion(@PathParam("id") final int id, @PathParam("version") f
@Produces(MediaType.APPLICATION_JSON)
@Path("/{id}/version/{version}/createAsset")
@Transactional
+ @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
public CohortDTO copyAssetFromVersion(@PathParam("id") final int id, @PathParam("version") final int version) {
checkVersion(id, version, false);
CohortVersion cohortVersion = versionService.getById(VersionType.COHORT, id, version);
diff --git a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java
index 84bc8e7787..1b92c4e769 100644
--- a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java
+++ b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java
@@ -27,6 +27,8 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
+import javax.cache.CacheManager;
+import javax.cache.configuration.MutableConfiguration;
import org.apache.shiro.authz.UnauthorizedException;
import org.ohdsi.circe.vocabulary.ConceptSetExpression;
@@ -57,6 +59,7 @@
import org.ohdsi.webapi.source.SourceService;
import org.ohdsi.webapi.tag.domain.HasTags;
import org.ohdsi.webapi.tag.dto.TagNameListRequestDTO;
+import org.ohdsi.webapi.util.CacheHelper;
import org.ohdsi.webapi.util.ExportUtil;
import org.ohdsi.webapi.util.NameUtils;
import org.ohdsi.webapi.util.ExceptionUtils;
@@ -69,6 +72,9 @@
import org.ohdsi.webapi.versioning.service.VersionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.stereotype.Component;
@@ -83,7 +89,26 @@
@Transactional
@Path("/conceptset/")
public class ConceptSetService extends AbstractDaoService implements HasTags {
-
+ //create cache
+ @Component
+ public static class CachingSetup implements JCacheManagerCustomizer {
+
+ public static final String CONCEPT_SET_LIST_CACHE = "conceptSetList";
+
+ @Override
+ public void customize(CacheManager cacheManager) {
+ // due to unit tests causing application contexts to reload cache manager caches, we
+ // have to check for the existance of a cache before creating it
+ Set cacheNames = CacheHelper.getCacheNames(cacheManager);
+ // Evict when a cohort definition is created or updated, or permissions, or tags
+ if (!cacheNames.contains(CONCEPT_SET_LIST_CACHE)) {
+ cacheManager.createCache(CONCEPT_SET_LIST_CACHE, new MutableConfiguration>()
+ .setTypes(String.class, (Class>) (Class>) List.class)
+ .setStoreByValue(false)
+ .setStatisticsEnabled(true));
+ }
+ }
+ }
@Autowired
private ConceptSetGenerationInfoRepository conceptSetGenerationInfoRepository;
@@ -151,7 +176,8 @@ public ConceptSetDTO getConceptSet(@PathParam("id") final int id) {
@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
- public Collection getConceptSets() {
+ @Cacheable(cacheNames = ConceptSetService.CachingSetup.CONCEPT_SET_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()")
+ public Collection getConceptSets() {
return getTransactionTemplate().execute(
transactionStatus -> StreamSupport.stream(getConceptSetRepository().findAll().spliterator(), false)
.filter(!defaultGlobalReadPermissions ? entity -> permissionService.hasReadAccess(entity) : entity -> true)
@@ -469,7 +495,8 @@ public Response exportConceptSetToCSV(@PathParam("id") final String id) throws E
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
- public ConceptSetDTO createConceptSet(ConceptSetDTO conceptSetDTO) {
+ @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true)
+ public ConceptSetDTO createConceptSet(ConceptSetDTO conceptSetDTO) {
UserEntity user = userRepository.findByLogin(security.getSubject());
ConceptSet conceptSet = conversionService.convert(conceptSetDTO, ConceptSet.class);
@@ -524,7 +551,8 @@ public List getNamesLike(String copyName) {
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
- public ConceptSetDTO updateConceptSet(@PathParam("id") final int id, ConceptSetDTO conceptSetDTO) throws Exception {
+ @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true)
+ public ConceptSetDTO updateConceptSet(@PathParam("id") final int id, ConceptSetDTO conceptSetDTO) throws Exception {
ConceptSet updated = getConceptSetRepository().findById(id);
if (updated == null) {
@@ -595,7 +623,8 @@ public Collection getConceptSetGenerationInfo(@PathPar
@DELETE
@Transactional(rollbackOn = Exception.class, dontRollbackOn = EmptyResultDataAccessException.class)
@Path("{id}")
- public void deleteConceptSet(@PathParam("id") final int id) {
+ @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true)
+ public void deleteConceptSet(@PathParam("id") final int id) {
// Remove any generation info
try {
this.conceptSetGenerationInfoRepository.deleteByConceptSetId(id);
@@ -642,7 +671,8 @@ public void deleteConceptSet(@PathParam("id") final int id) {
@Produces(MediaType.APPLICATION_JSON)
@Path("/{id}/tag/")
@Transactional
- public void assignTag(@PathParam("id") final Integer id, final int tagId) {
+ @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true)
+ public void assignTag(@PathParam("id") final Integer id, final int tagId) {
ConceptSet entity = getConceptSetRepository().findById(id);
checkOwnerOrAdminOrGranted(entity);
assignTag(entity, tagId);
@@ -811,6 +841,7 @@ public void deleteVersion(@PathParam("id") final int id, @PathParam("version") f
@Produces(MediaType.APPLICATION_JSON)
@Path("/{id}/version/{version}/createAsset")
@Transactional
+ @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true)
public ConceptSetDTO copyAssetFromVersion(@PathParam("id") final int id, @PathParam("version") final int version) {
checkVersion(id, version, false);
ConceptSetVersion conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version);
diff --git a/src/main/java/org/ohdsi/webapi/service/VocabularyService.java b/src/main/java/org/ohdsi/webapi/service/VocabularyService.java
index e034fb41c2..7a418de59a 100644
--- a/src/main/java/org/ohdsi/webapi/service/VocabularyService.java
+++ b/src/main/java/org/ohdsi/webapi/service/VocabularyService.java
@@ -11,6 +11,8 @@
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import javax.cache.CacheManager;
+import javax.cache.configuration.MutableConfiguration;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
@@ -51,6 +53,7 @@
import org.ohdsi.webapi.source.SourceService;
import org.ohdsi.webapi.source.SourceDaimon;
import org.ohdsi.webapi.source.SourceInfo;
+import org.ohdsi.webapi.util.CacheHelper;
import org.ohdsi.webapi.util.PreparedSqlRender;
import org.ohdsi.webapi.util.PreparedStatementRenderer;
import org.ohdsi.webapi.vocabulary.ConceptRecommendedNotInstalledException;
@@ -66,6 +69,10 @@
import org.ohdsi.webapi.vocabulary.VocabularySearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.Caching;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowCallbackHandler;
@@ -82,7 +89,42 @@
@Component
public class VocabularyService extends AbstractDaoService {
- private static Hashtable vocabularyInfoCache = null;
+ //create cache
+ @Component
+ public static class CachingSetup implements JCacheManagerCustomizer {
+
+ public static final String CONCEPT_DETAIL_CACHE = "conceptDetail";
+ public static final String CONCEPT_RELATED_CACHE = "conceptRelated";
+ public static final String CONCEPT_HIERARCHY_CACHE = "conceptHierarchy";
+
+ @Override
+ public void customize(CacheManager cacheManager) {
+ // due to unit tests causing application contexts to reload cache manager caches, we
+ // have to check for the existance of a cache before creating it
+ Set cacheNames = CacheHelper.getCacheNames(cacheManager);
+ // Evict when a cohort definition is created or updated, or permissions, or tags
+ if (!cacheNames.contains(CONCEPT_DETAIL_CACHE)) {
+ cacheManager.createCache(CONCEPT_DETAIL_CACHE, new MutableConfiguration()
+ .setTypes(String.class, Concept.class)
+ .setStoreByValue(false)
+ .setStatisticsEnabled(true));
+ }
+ if (!cacheNames.contains(CONCEPT_RELATED_CACHE)) {
+ cacheManager.createCache(CONCEPT_RELATED_CACHE, new MutableConfiguration>()
+ .setTypes(String.class, (Class>) (Class>) Collection.class)
+ .setStoreByValue(false)
+ .setStatisticsEnabled(true));
+ }
+ if (!cacheNames.contains(CONCEPT_HIERARCHY_CACHE)) {
+ cacheManager.createCache(CONCEPT_HIERARCHY_CACHE, new MutableConfiguration>()
+ .setTypes(String.class, (Class>) (Class>) Collection.class)
+ .setStoreByValue(false)
+ .setStatisticsEnabled(true));
+ }
+ }
+ }
+
+ private static Hashtable vocabularyInfoCache = null;
public static final String DEFAULT_SEARCH_ROWS = "20000";
@Autowired
@@ -717,6 +759,7 @@ public Collection executeSearch(@PathParam("query") String query) {
@GET
@Path("{sourceKey}/concept/{id}")
@Produces(MediaType.APPLICATION_JSON)
+ @Cacheable(cacheNames = CachingSetup.CONCEPT_DETAIL_CACHE, key = "#sourceKey.concat('/').concat(#id)")
public Concept getConcept(@PathParam("sourceKey") final String sourceKey, @PathParam("id") final long id) {
Source source = getSourceRepository().findBySourceKey(sourceKey);
String sqlPath = "/resources/vocabulary/sql/getConcept.sql";
@@ -768,6 +811,7 @@ public Concept getConcept(@PathParam("id") final long id) {
@GET
@Path("{sourceKey}/concept/{id}/related")
@Produces(MediaType.APPLICATION_JSON)
+ @Cacheable(cacheNames = CachingSetup.CONCEPT_RELATED_CACHE, key = "#sourceKey.concat('/').concat(#id)")
public Collection getRelatedConcepts(@PathParam("sourceKey") String sourceKey, @PathParam("id") final Long id) {
final Map concepts = new HashMap<>();
Source source = getSourceRepository().findBySourceKey(sourceKey);
@@ -795,6 +839,7 @@ public Collection getRelatedConcepts(@PathParam("sourceKey") Str
@GET
@Path("{sourceKey}/concept/{id}/ancestorAndDescendant")
@Produces(MediaType.APPLICATION_JSON)
+ @Cacheable(cacheNames = CachingSetup.CONCEPT_HIERARCHY_CACHE, key = "#sourceKey.concat('/').concat(#id)")
public Collection getConceptAncestorAndDescendant(@PathParam("sourceKey") String sourceKey, @PathParam("id") final Long id) {
final Map concepts = new HashMap<>();
Source source = getSourceRepository().findBySourceKey(sourceKey);
@@ -1211,9 +1256,19 @@ public VocabularyInfo mapRow(final ResultSet resultSet, final int arg1) throws S
return vocabularyInfoCache.get(sourceKey);
}
+ @Caching(evict = {
+ @CacheEvict(value=CachingSetup.CONCEPT_DETAIL_CACHE, allEntries = true),
+ @CacheEvict(value=CachingSetup.CONCEPT_RELATED_CACHE, allEntries = true),
+ @CacheEvict(value=CachingSetup.CONCEPT_RELATED_CACHE, allEntries = true)
+ })
public void clearVocabularyInfoCache() {
vocabularyInfoCache = null;
}
+
+
+ public void clearCaches() {
+
+ }
/**
* Get the descendant concepts of the selected ancestor vocabulary and
diff --git a/src/main/java/org/ohdsi/webapi/service/dto/CommonEntityDTO.java b/src/main/java/org/ohdsi/webapi/service/dto/CommonEntityDTO.java
index 287894aef9..ea820ecffc 100644
--- a/src/main/java/org/ohdsi/webapi/service/dto/CommonEntityDTO.java
+++ b/src/main/java/org/ohdsi/webapi/service/dto/CommonEntityDTO.java
@@ -2,13 +2,14 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
import org.ohdsi.webapi.user.dto.UserDTO;
import java.util.Date;
import org.ohdsi.webapi.CommonDTO;
@JsonInclude(JsonInclude.Include.NON_NULL)
-public abstract class CommonEntityDTO implements CommonDTO {
+public abstract class CommonEntityDTO implements CommonDTO, Serializable {
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private UserDTO createdBy;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
diff --git a/src/main/java/org/ohdsi/webapi/shiro/PermissionManager.java b/src/main/java/org/ohdsi/webapi/shiro/PermissionManager.java
index 43ff4ace64..7e30062ae6 100644
--- a/src/main/java/org/ohdsi/webapi/shiro/PermissionManager.java
+++ b/src/main/java/org/ohdsi/webapi/shiro/PermissionManager.java
@@ -35,11 +35,17 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
+import javax.cache.CacheManager;
+import javax.cache.configuration.MutableConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.permission.WildcardPermission;
import org.ohdsi.circe.helper.ResourceHelper;
+import org.ohdsi.webapi.util.CacheHelper;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.jdbc.core.JdbcTemplate;
/**
@@ -49,6 +55,26 @@
@Component
@Transactional
public class PermissionManager {
+ //create cache
+ @Component
+ public static class CachingSetup implements JCacheManagerCustomizer {
+
+ public static final String AUTH_INFO_CACHE = "authorizationInfo";
+
+ @Override
+ public void customize(CacheManager cacheManager) {
+ // due to unit tests causing application contexts to reload cache manager caches, we
+ // have to check for the existance of a cache before creating it
+ Set cacheNames = CacheHelper.getCacheNames(cacheManager);
+ // Evict when a user, role or permission is modified/deleted.
+ if (!cacheNames.contains(AUTH_INFO_CACHE)) {
+ cacheManager.createCache(AUTH_INFO_CACHE, new MutableConfiguration()
+ .setTypes(String.class, UserSimpleAuthorizationInfo.class)
+ .setStoreByValue(false)
+ .setStatisticsEnabled(true));
+ }
+ }
+ }
@Value("${datasource.ohdsi.schema}")
private String ohdsiSchema;
@@ -74,8 +100,8 @@ public class PermissionManager {
@Autowired
private JdbcTemplate jdbcTemplate;
- private ThreadLocal> authorizationInfoCache = ThreadLocal.withInitial(ConcurrentHashMap::new);
-
+ private ThreadLocal> authorizationInfoCache = ThreadLocal.withInitial(ConcurrentHashMap::new);
+
public static class PermissionsDTO {
public Map> permissions = null;
@@ -93,10 +119,12 @@ public RoleEntity addRole(String roleName, boolean isSystem) {
return role;
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, key = "#login")
public String addUserToRole(String roleName, String login) {
return addUserToRole(roleName, login, UserOrigin.SYSTEM);
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, key = "#login")
public String addUserToRole(String roleName, String login, UserOrigin userOrigin) {
Guard.checkNotEmpty(roleName);
Guard.checkNotEmpty(login);
@@ -108,6 +136,7 @@ public String addUserToRole(String roleName, String login, UserOrigin userOrigin
return userRole.getStatus();
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, key = "#login")
public void removeUserFromRole(String roleName, String login, UserOrigin origin) {
Guard.checkNotEmpty(roleName);
Guard.checkNotEmpty(login);
@@ -138,43 +167,45 @@ public Iterable getRoles(boolean includePersonalRoles) {
* @param login The login to fetch the authorization info
* @return A UserSimpleAuthorizationInfo containing roles and permissions.
*/
+ @Cacheable(cacheNames = CachingSetup.AUTH_INFO_CACHE)
public UserSimpleAuthorizationInfo getAuthorizationInfo(final String login) {
- return authorizationInfoCache.get().computeIfAbsent(login, newLogin -> {
- final UserSimpleAuthorizationInfo info = new UserSimpleAuthorizationInfo();
+ return authorizationInfoCache.get().computeIfAbsent(login, newLogin -> {
+ final UserSimpleAuthorizationInfo info = new UserSimpleAuthorizationInfo();
- final UserEntity userEntity = userRepository.findByLogin(newLogin);
- if(userEntity == null) {
- throw new UnknownAccountException("Account does not exist");
- }
+ final UserEntity userEntity = userRepository.findByLogin(login);
+ if(userEntity == null) {
+ throw new UnknownAccountException("Account does not exist");
+ }
- info.setUserId(userEntity.getId());
- info.setLogin(userEntity.getLogin());
+ info.setUserId(userEntity.getId());
+ info.setLogin(userEntity.getLogin());
- for (UserRoleEntity userRole: userEntity.getUserRoles()) {
- info.addRole(userRole.getRole().getName());
- }
+ for (UserRoleEntity userRole: userEntity.getUserRoles()) {
+ info.addRole(userRole.getRole().getName());
+ }
- // convert permission index from queryUserPermissions() into a map of WildcardPermissions
- Map> permsIdx = this.queryUserPermissions(newLogin).permissions;
- Map permissionMap = new HashMap>();
- Set permissionNames = new HashSet<>();
-
- for(String permIdxKey : permsIdx.keySet()) {
- List perms = permsIdx.get(permIdxKey);
- permissionNames.addAll(perms);
- // convert raw string permission into Wildcard perm for each element in this key's array.
- permissionMap.put(permIdxKey, perms.stream().map(perm -> new WildcardPermission(perm)).collect(Collectors.toList()));
- }
+ // convert permission index from queryUserPermissions() into a map of WildcardPermissions
+ Map> permsIdx = this.queryUserPermissions(login).permissions;
+ Map permissionMap = new HashMap>();
+ Set permissionNames = new HashSet<>();
- info.setStringPermissions(permissionNames);
- info.setPermissionIdx(permissionMap);
- return info;
- });
- }
+ for(String permIdxKey : permsIdx.keySet()) {
+ List perms = permsIdx.get(permIdxKey);
+ permissionNames.addAll(perms);
+ // convert raw string permission into Wildcard perm for each element in this key's array.
+ permissionMap.put(permIdxKey, perms.stream().map(perm -> new WildcardPermission(perm)).collect(Collectors.toList()));
+ }
+
+ info.setStringPermissions(permissionNames);
+ info.setPermissionIdx(permissionMap);
+ return info;
+ });
+ }
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
public void clearAuthorizationInfoCache() {
- this.authorizationInfoCache.set(new ConcurrentHashMap<>());
+ authorizationInfoCache.set(new ConcurrentHashMap<>());
}
@Transactional
@@ -261,6 +292,7 @@ public Set getUserPermissions(Long userId) {
return permissions;
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
public void removeRole(Long roleId) {
eventPublisher.publishEvent(new DeleteRoleEvent(this, roleId));
this.roleRepository.delete(roleId);
@@ -272,6 +304,7 @@ public Set getRolePermissions(Long roleId) {
return permissions;
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
public void addPermission(Long roleId, Long permissionId) {
PermissionEntity permission = this.getPermissionById(permissionId);
RoleEntity role = this.getRoleById(roleId);
@@ -279,10 +312,12 @@ public void addPermission(Long roleId, Long permissionId) {
this.addPermission(role, permission, null);
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
public void addPermission(RoleEntity role, PermissionEntity permission) {
this.addPermission(role, permission, null);
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
public void removePermission(Long permissionId, Long roleId) {
RolePermissionEntity rolePermission = this.rolePermissionRepository.findByRoleIdAndPermissionId(roleId, permissionId);
if (rolePermission != null)
@@ -295,6 +330,7 @@ public Set getRoleUsers(Long roleId) {
return users;
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
public void addUser(Long userId, Long roleId) {
UserEntity user = this.getUserById(userId);
RoleEntity role = this.getRoleById(roleId);
@@ -302,12 +338,14 @@ public void addUser(Long userId, Long roleId) {
this.addUser(user, role, UserOrigin.SYSTEM, null);
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
public void removeUser(Long userId, Long roleId) {
UserRoleEntity userRole = this.userRoleRepository.findByUserIdAndRoleId(userId, roleId);
if (userRole != null)
this.userRoleRepository.delete(userRole);
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
public void removePermission(String value) {
PermissionEntity permission = this.permissionRepository.findByValueIgnoreCase(value);
if (permission != null)
@@ -473,6 +511,7 @@ private PermissionEntity getPermissionById(Long permissionId) {
return permission;
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
private RolePermissionEntity addPermission(final RoleEntity role, final PermissionEntity permission, final String status) {
RolePermissionEntity relation = this.rolePermissionRepository.findByRoleAndPermission(role, permission);
if (relation == null) {
@@ -528,6 +567,7 @@ public RoleEntity updateRole(RoleEntity roleEntity) {
return this.roleRepository.save(roleEntity);
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
public void addPermissionsFromTemplate(RoleEntity roleEntity, Map template, String value) {
for (Map.Entry entry : template.entrySet()) {
String permission = String.format(entry.getKey(), value);
@@ -537,11 +577,13 @@ public void addPermissionsFromTemplate(RoleEntity roleEntity, Map template, String value) {
RoleEntity currentUserPersonalRole = getCurrentUserPersonalRole();
addPermissionsFromTemplate(currentUserPersonalRole, template, value);
}
+ @CacheEvict(cacheNames = CachingSetup.AUTH_INFO_CACHE, allEntries = true)
public void removePermissionsFromTemplate(Map template, String value) {
for (Map.Entry entry : template.entrySet()) {
String permission = String.format(entry.getKey(), value);
diff --git a/src/main/java/org/ohdsi/webapi/source/SourceController.java b/src/main/java/org/ohdsi/webapi/source/SourceController.java
index ee32d2f4de..3ebb2450c6 100644
--- a/src/main/java/org/ohdsi/webapi/source/SourceController.java
+++ b/src/main/java/org/ohdsi/webapi/source/SourceController.java
@@ -28,8 +28,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
-import java.util.function.Predicate;
import java.util.stream.Collectors;
+import org.springframework.cache.annotation.CacheEvict;
@Path("/source/")
@Component
@@ -157,6 +157,7 @@ public SourceDetails getSourceDetails(@PathParam("sourceId") Integer sourceId) {
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
+ @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true)
public SourceInfo createSource(@FormDataParam("keyfile") InputStream file, @FormDataParam("keyfile") FormDataContentDisposition fileDetail, @FormDataParam("source") SourceRequest request) throws Exception {
if (!securityEnabled) {
throw new NotAuthorizedException(SECURE_MODE_ERROR);
@@ -219,6 +220,7 @@ public SourceInfo createSource(@FormDataParam("keyfile") InputStream file, @Form
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
+ @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true)
public SourceInfo updateSource(@PathParam("sourceId") Integer sourceId, @FormDataParam("keyfile") InputStream file, @FormDataParam("keyfile") FormDataContentDisposition fileDetail, @FormDataParam("source") SourceRequest request) throws IOException {
if (!securityEnabled) {
throw new NotAuthorizedException(SECURE_MODE_ERROR);
@@ -318,6 +320,7 @@ private void setKeyfileData(Source updated, Source source, InputStream file) thr
@Path("{sourceId}")
@DELETE
@Transactional
+ @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true)
public Response delete(@PathParam("sourceId") Integer sourceId) throws Exception {
if (!securityEnabled){
return getInsecureModeResponse();
@@ -384,6 +387,7 @@ public Map getPriorityDaimons() {
@Path("{sourceKey}/daimons/{daimonType}/set-priority")
@POST
@Produces(MediaType.APPLICATION_JSON)
+ @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true)
public Response updateSourcePriority(
@PathParam("sourceKey") final String sourceKey,
@PathParam("daimonType") final String daimonTypeName
diff --git a/src/main/java/org/ohdsi/webapi/source/SourceService.java b/src/main/java/org/ohdsi/webapi/source/SourceService.java
index 65551781a2..77a749977b 100644
--- a/src/main/java/org/ohdsi/webapi/source/SourceService.java
+++ b/src/main/java/org/ohdsi/webapi/source/SourceService.java
@@ -17,12 +17,33 @@
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;
+import javax.cache.CacheManager;
+import javax.cache.configuration.MutableConfiguration;
+import org.ohdsi.webapi.util.CacheHelper;
+import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Component;
@Service
public class SourceService extends AbstractDaoService {
- private static Collection