From fc12ba525c71740053c9ad38fad1e041668eebbc Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:20:16 +0800 Subject: [PATCH 01/18] switch to spring mvc --- .../java/org/ohdsi/webapi/JerseyConfig.java | 81 - .../java/org/ohdsi/webapi/WebMvcConfig.java | 53 + .../webapi/i18n/mvc/LocaleInterceptor.java | 64 + .../java/org/ohdsi/webapi/info/BuildInfo.java | 24 + .../webapi/mvc/AbstractMvcController.java | 73 + .../webapi/mvc/CohortSampleMvcController.java | 216 +++ .../webapi/mvc/EvidenceMvcController.java | 1051 +++++++++++++ .../webapi/mvc/GlobalExceptionHandler.java | 217 +++ .../org/ohdsi/webapi/mvc/MigrationUtils.java | 56 + .../webapi/mvc/NotificationMvcController.java | 110 ++ .../mvc/OutputStreamMessageConverter.java | 60 + .../ohdsi/webapi/mvc/ResponseConverters.java | 61 + .../ohdsi/webapi/mvc/TagMvcController.java | 132 ++ .../ohdsi/webapi/mvc/ToolMvcController.java | 44 + .../mvc/controller/ActivityMvcController.java | 38 + .../controller/CDMResultsMvcController.java | 268 ++++ .../mvc/controller/CacheMvcController.java | 94 ++ .../CohortAnalysisMvcController.java | 127 ++ .../CohortDefinitionMvcController.java | 1134 ++++++++++++++ .../mvc/controller/CohortMvcController.java | 93 ++ .../CohortResultsMvcController.java | 1386 +++++++++++++++++ .../controller/ConceptSetMvcController.java | 1062 +++++++++++++ .../mvc/controller/DDLMvcController.java | 215 +++ .../controller/FeasibilityMvcController.java | 256 +++ .../mvc/controller/I18nMvcController.java | 83 + .../mvc/controller/InfoMvcController.java | 44 + .../mvc/controller/JobMvcController.java | 156 ++ .../controller/PermissionMvcController.java | 211 +++ .../mvc/controller/SSOMvcController.java | 81 + .../mvc/controller/SourceMvcController.java | 398 +++++ .../controller/SqlRenderMvcController.java | 49 + .../mvc/controller/UserMvcController.java | 368 +++++ .../controller/VocabularyMvcController.java | 715 +++++++++ .../reusable/ReusableMvcController.java | 358 +++++ .../controller/StatisticMvcController.java | 291 ++++ .../importer/UserImportJobMvcController.java | 186 +++ .../importer/UserImportMvcController.java | 249 +++ src/main/resources/application.properties | 8 +- .../java/org/ohdsi/webapi/test/ITStarter.java | 2 +- .../org/ohdsi/webapi/test/SecurityIT.java | 219 --- .../migration/DualRuntimeTestSupport.java | 94 ++ .../test/migration/MigrationPhase1IT.java | 50 + .../test/migration/MigrationPhase2IT.java | 94 ++ .../test/migration/MigrationPhase3IT.java | 165 ++ .../resources/application-test.properties | 2 +- 45 files changed, 10433 insertions(+), 305 deletions(-) delete mode 100644 src/main/java/org/ohdsi/webapi/JerseyConfig.java create mode 100644 src/main/java/org/ohdsi/webapi/WebMvcConfig.java create mode 100644 src/main/java/org/ohdsi/webapi/i18n/mvc/LocaleInterceptor.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/AbstractMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/CohortSampleMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/EvidenceMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/NotificationMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/OutputStreamMessageConverter.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/TagMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/ToolMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/ActivityMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CDMResultsMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CohortAnalysisMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CohortDefinitionMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CohortMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/ConceptSetMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/DDLMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/FeasibilityMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/I18nMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/InfoMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/JobMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/PermissionMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/SSOMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/SourceMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/SqlRenderMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/UserMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/VocabularyMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/reusable/ReusableMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/statistic/controller/StatisticMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/user/importer/UserImportJobMvcController.java create mode 100644 src/main/java/org/ohdsi/webapi/user/importer/UserImportMvcController.java delete mode 100644 src/test/java/org/ohdsi/webapi/test/SecurityIT.java create mode 100644 src/test/java/org/ohdsi/webapi/test/migration/DualRuntimeTestSupport.java create mode 100644 src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase1IT.java create mode 100644 src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase2IT.java create mode 100644 src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase3IT.java diff --git a/src/main/java/org/ohdsi/webapi/JerseyConfig.java b/src/main/java/org/ohdsi/webapi/JerseyConfig.java deleted file mode 100644 index a290cbd5c6..0000000000 --- a/src/main/java/org/ohdsi/webapi/JerseyConfig.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.ohdsi.webapi; - -import org.glassfish.hk2.utilities.binding.AbstractBinder; -import org.glassfish.jersey.media.multipart.MultiPartFeature; -import org.glassfish.jersey.message.GZipEncoder; -import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.server.filter.EncodingFilter; -import org.glassfish.jersey.server.spi.internal.ValueParamProvider; -import org.ohdsi.webapi.info.InfoService; -import org.ohdsi.webapi.security.PermissionController; -import org.ohdsi.webapi.security.SSOController; -import org.ohdsi.webapi.service.ActivityService; -import org.ohdsi.webapi.service.CDMResultsService; -import org.ohdsi.webapi.service.CohortAnalysisService; -import org.ohdsi.webapi.service.CohortDefinitionService; -import org.ohdsi.webapi.service.CohortResultsService; -import org.ohdsi.webapi.service.CohortService; -import org.ohdsi.webapi.service.ConceptSetService; -import org.ohdsi.webapi.service.DDLService; -import org.ohdsi.webapi.service.EvidenceService; -import org.ohdsi.webapi.service.FeasibilityService; -import org.ohdsi.webapi.service.JobService; -import org.ohdsi.webapi.service.SqlRenderService; -import org.ohdsi.webapi.service.UserService; -import org.ohdsi.webapi.service.VocabularyService; -import org.ohdsi.webapi.source.SourceController; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; - -import jakarta.inject.Singleton; -import jakarta.ws.rs.ApplicationPath; -import org.ohdsi.webapi.cache.CacheService; - -/** - * Jersey configuration for JAX-RS resources - */ -@Configuration -@ApplicationPath("/WebAPI") -public class JerseyConfig extends ResourceConfig { - - public JerseyConfig(@Value("${jersey.resources.root.package}") String rootPackage) { - // Register packages first - packages(rootPackage); - - // Register individual services - register(ActivityService.class); - register(CacheService.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(InfoService.class); - register(JobService.class); - register(MultiPartFeature.class); - register(PermissionController.class); - register(SourceController.class); - register(SqlRenderService.class); - register(SSOController.class); - register(UserService.class); - register(VocabularyService.class); - - // Register binder - register(new AbstractBinder() { - @Override - protected void configure() { - bind(PageableValueFactoryProvider.class) - .to(ValueParamProvider.class) - .in(Singleton.class); - } - }); - - // Register encoding filter - must be last - register(EncodingFilter.class); - register(GZipEncoder.class); - } -} diff --git a/src/main/java/org/ohdsi/webapi/WebMvcConfig.java b/src/main/java/org/ohdsi/webapi/WebMvcConfig.java new file mode 100644 index 0000000000..427c57adc8 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/WebMvcConfig.java @@ -0,0 +1,53 @@ +package org.ohdsi.webapi; + +import org.ohdsi.webapi.i18n.mvc.LocaleInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +/** + * Spring MVC Configuration. + * Jersey has been removed - Spring MVC now serves all endpoints. + * + * Spring MVC endpoints are served at: /WebAPI/* (via server.context-path=/WebAPI) + * + * NOTE: We don't use @EnableWebMvc because: + * - Spring Boot auto-configures Spring MVC by default + * - @EnableWebMvc would disable Spring Boot's auto-configuration + * - This would conflict with existing I18nConfig (duplicate localeResolver bean) + * - We only need to customize specific aspects, not override everything + * + * NOTE: We don't need a custom ServletRegistrationBean because: + * - Spring Boot's default DispatcherServlet already serves at context-path + /* + * - With server.context-path=/WebAPI, it automatically serves /WebAPI/* + * - @ComponentScan in WebApi.java finds controllers in org.ohdsi.webapi.mvc.controller + * + * @see org.ohdsi.webapi.I18nConfig + * @see org.ohdsi.webapi.WebApi + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired(required = false) + private LocaleInterceptor localeInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // Add locale interceptor if available (replaces Jersey LocaleFilter) + if (localeInterceptor != null) { + registry.addInterceptor(localeInterceptor) + .addPathPatterns("/**"); + } + } + + @Override + public void extendMessageConverters(List> converters) { + // Add custom OutputStreamMessageConverter (replaces Jersey's OutputStreamWriter) + // Spring Boot already configures Jackson converter, so we just extend the list + converters.add(new org.ohdsi.webapi.mvc.OutputStreamMessageConverter()); + } +} diff --git a/src/main/java/org/ohdsi/webapi/i18n/mvc/LocaleInterceptor.java b/src/main/java/org/ohdsi/webapi/i18n/mvc/LocaleInterceptor.java new file mode 100644 index 0000000000..9b121c0f3a --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/i18n/mvc/LocaleInterceptor.java @@ -0,0 +1,64 @@ +package org.ohdsi.webapi.i18n.mvc; + +import org.apache.commons.lang3.StringUtils; +import org.ohdsi.webapi.Constants; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Locale; + +/** + * Spring MVC HandlerInterceptor replacement for Jersey's LocaleFilter. + * Extracts locale from request headers and sets it in LocaleContextHolder. + * + * Migration Status: Replaces /i18n/LocaleFilter.java (JAX-RS ContainerRequestFilter) + */ +@Component +public class LocaleInterceptor implements HandlerInterceptor { + + private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language"; + private static final String LANG_PARAM = "lang"; + private static final String DEFAULT_LOCALE = "en"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + Locale locale = resolveLocale(request); + LocaleContextHolder.setLocale(locale); + // Store in request attribute for compatibility with existing code + request.setAttribute("language", locale); + return true; + } + + private Locale resolveLocale(HttpServletRequest request) { + // Priority 1: User-specific language header + String userHeader = request.getHeader(Constants.Headers.USER_LANGAUGE); + if (StringUtils.isNotBlank(userHeader)) { + return Locale.forLanguageTag(userHeader); + } + + // Priority 2: Query parameter 'lang' + String langParam = request.getParameter(LANG_PARAM); + if (StringUtils.isNotBlank(langParam)) { + return Locale.forLanguageTag(langParam); + } + + // Priority 3: Accept-Language header + String acceptLanguage = request.getHeader(ACCEPT_LANGUAGE_HEADER); + if (StringUtils.isNotBlank(acceptLanguage)) { + return Locale.forLanguageTag(acceptLanguage); + } + + // Default + return Locale.forLanguageTag(DEFAULT_LOCALE); + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) { + // Clean up locale context + LocaleContextHolder.resetLocaleContext(); + } +} diff --git a/src/main/java/org/ohdsi/webapi/info/BuildInfo.java b/src/main/java/org/ohdsi/webapi/info/BuildInfo.java index 49d6f4a502..d85014c220 100644 --- a/src/main/java/org/ohdsi/webapi/info/BuildInfo.java +++ b/src/main/java/org/ohdsi/webapi/info/BuildInfo.java @@ -1,6 +1,9 @@ package org.ohdsi.webapi.info; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.info.BuildProperties; import org.springframework.stereotype.Component; @@ -18,6 +21,8 @@ public class BuildInfo { private final RepositoryInfo atlasRepositoryInfo; private final RepositoryInfo webapiRepositoryInfo; + // Constructor for Spring dependency injection + @Autowired public BuildInfo(BuildProperties buildProperties, @Value("${build.number}") final String buildNumber) { this.artifactVersion = String.format("%s %s", buildProperties.getArtifact(), buildProperties.getVersion()); @@ -35,6 +40,25 @@ public BuildInfo(BuildProperties buildProperties, @Value("${build.number}") fina ); } + // Constructor for Jackson deserialization + @JsonCreator + public BuildInfo( + @JsonProperty("artifactVersion") String artifactVersion, + @JsonProperty("build") String build, + @JsonProperty("timestamp") String timestamp, + @JsonProperty("branch") String branch, + @JsonProperty("commitId") String commitId, + @JsonProperty("atlasRepositoryInfo") RepositoryInfo atlasRepositoryInfo, + @JsonProperty("webapiRepositoryInfo") RepositoryInfo webapiRepositoryInfo) { + this.artifactVersion = artifactVersion; + this.build = build; + this.timestamp = timestamp; + this.branch = branch; + this.commitId = commitId; + this.atlasRepositoryInfo = atlasRepositoryInfo; + this.webapiRepositoryInfo = webapiRepositoryInfo; + } + private Integer getAsInteger(BuildProperties properties, String key) { String value = properties.get(key); diff --git a/src/main/java/org/ohdsi/webapi/mvc/AbstractMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/AbstractMvcController.java new file mode 100644 index 0000000000..714c4fedab --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/AbstractMvcController.java @@ -0,0 +1,73 @@ +package org.ohdsi.webapi.mvc; + +import org.ohdsi.webapi.common.sensitiveinfo.AbstractAdminService; +import org.ohdsi.webapi.shiro.management.Security; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; + +/** + * Base class for Spring MVC controllers during Jersey migration. + * Provides common functionality and utilities for migrated controllers. + * Extends AbstractAdminService to inherit security helper methods (isSecured, isAdmin, isModerator). + */ +public abstract class AbstractMvcController extends AbstractAdminService { + + @Autowired + protected Security security; + + /** + * Get the current user's subject/login + */ + protected String getCurrentUser() { + return security.getSubject(); + } + + /** + * Create an OK response with body + */ + protected ResponseEntity ok(T body) { + return ResponseEntity.ok(body); + } + + /** + * Create an OK response without body + */ + protected ResponseEntity ok() { + return ResponseEntity.ok().build(); + } + + /** + * Create a NOT FOUND response + */ + protected ResponseEntity notFound() { + return ResponseEntity.notFound().build(); + } + + /** + * Create a BAD REQUEST response + */ + protected ResponseEntity badRequest() { + return ResponseEntity.badRequest().build(); + } + + /** + * Create a FORBIDDEN response + */ + protected ResponseEntity forbidden() { + return ResponseEntity.status(403).build(); + } + + /** + * Create an UNAUTHORIZED response + */ + protected ResponseEntity unauthorized() { + return ResponseEntity.status(401).build(); + } + + /** + * Create a NO CONTENT response + */ + protected ResponseEntity noContent() { + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/CohortSampleMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/CohortSampleMvcController.java new file mode 100644 index 0000000000..d61373903e --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/CohortSampleMvcController.java @@ -0,0 +1,216 @@ +package org.ohdsi.webapi.mvc; + +import org.ohdsi.webapi.GenerationStatus; +import org.ohdsi.webapi.cohortdefinition.CohortDefinitionRepository; +import org.ohdsi.webapi.cohortdefinition.CohortGenerationInfo; +import org.ohdsi.webapi.cohortdefinition.CohortGenerationInfoId; +import org.ohdsi.webapi.cohortdefinition.CohortGenerationInfoRepository; +import org.ohdsi.webapi.cohortsample.CohortSamplingService; +import org.ohdsi.webapi.cohortsample.dto.CohortSampleDTO; +import org.ohdsi.webapi.cohortsample.dto.CohortSampleListDTO; +import org.ohdsi.webapi.cohortsample.dto.SampleParametersDTO; +import org.ohdsi.webapi.source.Source; +import org.ohdsi.webapi.source.SourceRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.*; + +@RestController +@RequestMapping("/cohortsample") +public class CohortSampleMvcController extends AbstractMvcController { + private final CohortDefinitionRepository cohortDefinitionRepository; + private final CohortGenerationInfoRepository generationInfoRepository; + private final CohortSamplingService samplingService; + private final SourceRepository sourceRepository; + + @Autowired + public CohortSampleMvcController( + CohortSamplingService samplingService, + SourceRepository sourceRepository, + CohortDefinitionRepository cohortDefinitionRepository, + CohortGenerationInfoRepository generationInfoRepository + ) { + this.samplingService = samplingService; + this.sourceRepository = sourceRepository; + this.cohortDefinitionRepository = cohortDefinitionRepository; + this.generationInfoRepository = generationInfoRepository; + } + + /** + * Get information about cohort samples for a data source + * + * @param cohortDefinitionId The id for an existing cohort definition + * @param sourceKey + * @return JSON containing information about cohort samples + */ + @GetMapping("/{cohortDefinitionId}/{sourceKey}") + public ResponseEntity listCohortSamples( + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @PathVariable("sourceKey") String sourceKey + ) { + Source source = getSource(sourceKey); + CohortSampleListDTO result = new CohortSampleListDTO(); + + result.setCohortDefinitionId(cohortDefinitionId); + result.setSourceId(source.getId()); + + CohortGenerationInfo generationInfo = generationInfoRepository.findById( + new CohortGenerationInfoId(cohortDefinitionId, source.getId())).orElse(null); + result.setGenerationStatus(generationInfo != null ? generationInfo.getStatus() : null); + result.setIsValid(generationInfo != null && generationInfo.isIsValid()); + + result.setSamples(this.samplingService.listSamples(cohortDefinitionId, source.getId())); + + return ok(result); + } + + /** + * Get an existing cohort sample + * @param cohortDefinitionId + * @param sourceKey + * @param sampleId + * @param fields + * @return personId, gender, age of each person in the cohort sample + */ + @GetMapping("/{cohortDefinitionId}/{sourceKey}/{sampleId}") + public ResponseEntity getCohortSample( + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("sampleId") Integer sampleId, + @RequestParam(defaultValue = "") String fields + ) { + List returnFields = Arrays.asList(fields.split(",")); + boolean withRecordCounts = returnFields.contains("recordCount"); + return ok(this.samplingService.getSample(sampleId, withRecordCounts)); + } + + /** + * @summary Refresh a cohort sample + * Refresh a cohort sample for a given source key. This will re-sample persons from the cohort. + * @param cohortDefinitionId + * @param sourceKey + * @param sampleId + * @param fields + * @return A sample of persons from a cohort + */ + @PostMapping("/{cohortDefinitionId}/{sourceKey}/{sampleId}/refresh") + public ResponseEntity refreshCohortSample( + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("sampleId") Integer sampleId, + @RequestParam(defaultValue = "") String fields + ) { + List returnFields = Arrays.asList(fields.split(",")); + boolean withRecordCounts = returnFields.contains("recordCount"); + this.samplingService.refreshSample(sampleId); + return ok(this.samplingService.getSample(sampleId, withRecordCounts)); + } + + /** + * Does an existing cohort have samples? + * @param cohortDefinitionId + * @return true or false + */ + @GetMapping("/has-samples/{cohortDefinitionId}") + public ResponseEntity> hasSamples( + @PathVariable("cohortDefinitionId") int cohortDefinitionId + ) { + int nSamples = this.samplingService.countSamples(cohortDefinitionId); + return ok(Collections.singletonMap("hasSamples", nSamples > 0)); + } + + /** + * Does an existing cohort have samples from a particular source? + * @param sourceKey + * @param cohortDefinitionId + * @return true or false + */ + @GetMapping("/has-samples/{cohortDefinitionId}/{sourceKey}") + public ResponseEntity> hasSamplesForSource( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("cohortDefinitionId") int cohortDefinitionId + ) { + Source source = getSource(sourceKey); + int nSamples = this.samplingService.countSamples(cohortDefinitionId, source.getId()); + return ok(Collections.singletonMap("hasSamples", nSamples > 0)); + } + + /** + * Create a new cohort sample + * @param sourceKey + * @param cohortDefinitionId + * @param sampleParameters + * @return + */ + @PostMapping("/{cohortDefinitionId}/{sourceKey}") + public ResponseEntity createCohortSample( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @RequestBody SampleParametersDTO sampleParameters + ) { + sampleParameters.validate(); + Source source = getSource(sourceKey); + if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort definition " + cohortDefinitionId + " does not exist."); + } + CohortGenerationInfo generationInfo = generationInfoRepository.findById( + new CohortGenerationInfoId(cohortDefinitionId, source.getId())).orElse(null); + if (generationInfo == null || generationInfo.getStatus() != GenerationStatus.COMPLETE) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cohort is not yet generated"); + } + return ok(samplingService.createSample(source, cohortDefinitionId, sampleParameters)); + } + + /** + * Delete a cohort sample + * @param sourceKey + * @param cohortDefinitionId + * @param sampleId + * @return + */ + @DeleteMapping("/{cohortDefinitionId}/{sourceKey}/{sampleId}") + public ResponseEntity deleteCohortSample( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @PathVariable("sampleId") int sampleId + ) { + Source source = getSource(sourceKey); + if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort definition " + cohortDefinitionId + " does not exist."); + } + samplingService.deleteSample(cohortDefinitionId, source, sampleId); + return noContent(); + } + + /** + * Delete all samples for a cohort on a data source + * @param sourceKey + * @param cohortDefinitionId + * @return + */ + @DeleteMapping("/{cohortDefinitionId}/{sourceKey}") + public ResponseEntity deleteCohortSamples( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("cohortDefinitionId") int cohortDefinitionId + ) { + Source source = getSource(sourceKey); + if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort definition " + cohortDefinitionId + " does not exist."); + } + samplingService.launchDeleteSamplesTasklet(cohortDefinitionId, source.getId()); + return ResponseEntity.status(HttpStatus.ACCEPTED).build(); + } + + private Source getSource(String sourceKey) { + Source source = sourceRepository.findBySourceKey(sourceKey); + if (source == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Source " + sourceKey + " does not exist"); + } + return source; + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/EvidenceMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/EvidenceMvcController.java new file mode 100644 index 0000000000..d82b2bdd78 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/EvidenceMvcController.java @@ -0,0 +1,1051 @@ +package org.ohdsi.webapi.mvc; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collection; +import java.util.ArrayList; +import java.util.List; +import java.io.IOException; +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.ohdsi.circe.helper.ResourceHelper; +import org.ohdsi.circe.vocabulary.ConceptSetExpression; +import org.ohdsi.circe.vocabulary.ConceptSetExpressionQueryBuilder; +import org.ohdsi.sql.SqlRender; +import org.ohdsi.sql.SqlTranslate; +import org.ohdsi.webapi.conceptset.ConceptSetGenerationInfoRepository; +import org.ohdsi.webapi.evidence.CohortStudyMapping; +import org.ohdsi.webapi.evidence.CohortStudyMappingRepository; +import org.ohdsi.webapi.evidence.ConceptCohortMapping; +import org.ohdsi.webapi.evidence.ConceptCohortMappingRepository; +import org.ohdsi.webapi.evidence.ConceptOfInterestMapping; +import org.ohdsi.webapi.evidence.ConceptOfInterestMappingRepository; +import org.ohdsi.webapi.evidence.DrugEvidence; +import org.ohdsi.webapi.evidence.EvidenceDetails; +import org.ohdsi.webapi.evidence.EvidenceSummary; +import org.ohdsi.webapi.evidence.EvidenceUniverse; +import org.ohdsi.webapi.evidence.HoiEvidence; +import org.ohdsi.webapi.evidence.DrugHoiEvidence; +import org.ohdsi.webapi.evidence.DrugLabel; +import org.ohdsi.webapi.evidence.DrugLabelInfo; +import org.ohdsi.webapi.evidence.DrugLabelRepository; +import org.ohdsi.webapi.evidence.EvidenceInfo; +import org.ohdsi.webapi.evidence.DrugRollUpEvidence; +import org.ohdsi.webapi.evidence.Evidence; +import org.ohdsi.webapi.evidence.SpontaneousReport; +import org.ohdsi.webapi.evidence.EvidenceSearch; +import org.ohdsi.webapi.evidence.negativecontrols.NegativeControlDTO; +import org.ohdsi.webapi.evidence.negativecontrols.NegativeControlMapper; +import org.ohdsi.webapi.evidence.negativecontrols.NegativeControlTaskParameters; +import org.ohdsi.webapi.evidence.negativecontrols.NegativeControlTasklet; +import org.ohdsi.webapi.job.GeneratesNotification; +import org.ohdsi.webapi.job.JobExecutionResource; +import org.ohdsi.webapi.job.JobTemplate; +import org.ohdsi.webapi.service.EvidenceService; +import org.ohdsi.webapi.service.ConceptSetService; +import org.ohdsi.webapi.source.Source; +import org.ohdsi.webapi.source.SourceDaimon; +import org.ohdsi.webapi.util.PreparedSqlRender; +import org.ohdsi.webapi.util.PreparedStatementRenderer; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.web.bind.annotation.*; + +/** + * Provides REST services for querying the Common Evidence Model + * + * @summary REST services for querying the Common Evidence Model See + * https://github.com/OHDSI/CommonEvidenceModel + */ +@RestController +@RequestMapping("/evidence") +public class EvidenceMvcController extends AbstractMvcController implements GeneratesNotification { + + private static final String NAME = "negativeControlsAnalysisJob"; + + @Autowired + private JobTemplate jobTemplate; + + @Autowired + private DrugLabelRepository drugLabelRepository; + + @Autowired + private ConceptCohortMappingRepository mappingRepository; + + @Autowired + private ConceptOfInterestMappingRepository conceptOfInterestMappingRepository; + + @Autowired + private CohortStudyMappingRepository cohortStudyMappingRepository; + + @Autowired + private ConceptSetGenerationInfoRepository conceptSetGenerationInfoRepository; + + @Autowired + private ConceptSetService conceptSetService; + + @Autowired + private EvidenceService daoService; + + private final RowMapper drugLabelRowMapper = new RowMapper() { + @Override + public DrugLabelInfo mapRow(final ResultSet rs, final int arg1) throws SQLException { + final DrugLabelInfo returnVal = new DrugLabelInfo(); + returnVal.conceptId = rs.getString("CONCEPT_ID"); + returnVal.conceptName = rs.getString("CONCEPT_NAME"); + returnVal.usaProductLabelExists = rs.getInt("US_SPL_LABEL"); + return returnVal; + } + }; + + public static class DrugConditionSourceSearchParams { + + @JsonProperty("targetDomain") + public String targetDomain = "CONDITION"; + @JsonProperty("drugConceptIds") + public int[] drugConceptIds; + @JsonProperty("conditionConceptIds") + public int[] conditionConceptIds; + @JsonProperty("sourceIds") + public String[] sourceIds; + + public String getDrugConceptIds() { + return StringUtils.join(drugConceptIds, ','); + } + + public String getConditionConceptIds() { + return StringUtils.join(conditionConceptIds, ','); + } + + public String getSourceIds() { + if (sourceIds != null) { + List ids = Arrays.stream(sourceIds) + .map(sourceId -> sourceId.replaceAll("(\"|')", "")) + .collect(Collectors.toList()); + return "'" + StringUtils.join(ids, "','") + "'"; + } + return "''"; + } + } + + /** + * PENELOPE function: search + * the cohort_study table for the selected cohortId in the WebAPI DB + * + * @summary Find studies for a cohort - will be depreciated + * @deprecated + * @param cohortId The cohort Id + * @return A list of studies related to the cohort + */ + @GetMapping("/study/{cohortId}") + public ResponseEntity> getCohortStudyMapping(@PathVariable("cohortId") int cohortId) { + return ok(cohortStudyMappingRepository.findByCohortDefinitionId(cohortId)); + } + + /** + * PENELOPE function: search + * the COHORT_CONCEPT_MAP for the selected cohortId in the WebAPI DB + * + * @summary Find cohorts for a concept - will be depreciated + * @deprecated + * @param conceptId The concept Id of interest + * @return A list of cohorts for the specified conceptId + */ + @GetMapping("/mapping/{conceptId}") + public ResponseEntity> getConceptCohortMapping(@PathVariable("conceptId") int conceptId) { + return ok(mappingRepository.findByConceptId(conceptId)); + } + + /** + * PENELOPE function: + * reference to a manually curated table related concept_of_interest in + * WebAPI for use with PENELOPE. This will be depreciated in a future + * release. + * + * @summary Find a custom concept mapping - will be depreciated + * @deprecated + * @param conceptId The conceptId of interest + * @return A list of concepts based on the conceptId of interest + */ + @GetMapping("/conceptofinterest/{conceptId}") + public ResponseEntity> getConceptOfInterest(@PathVariable("conceptId") int conceptId) { + return ok(conceptOfInterestMappingRepository.findAllByConceptId(conceptId)); + } + + /** + * PENELOPE function: + * reference to the list of product labels in the WebAPI DRUG_LABELS table + * that associates a product label SET_ID to the RxNorm ingredient. This + * will be depreciated in a future release as this can be found using the + * OMOP vocabulary + * + * @summary Find a drug label - will be depreciated + * @deprecated + * @param setid The drug label setId + * @return The set of drug labels that match the setId specified. + */ + @GetMapping("/label/{setid}") + public ResponseEntity> getDrugLabel(@PathVariable("setid") String setid) { + return ok(drugLabelRepository.findAllBySetid(setid)); + } + + /** + * PENELOPE function: search + * the DRUG_LABELS.search_name for the searchTerm + * + * @summary Search for a drug label - will be depreciated + * @deprecated + * @param searchTerm The search term + * @return A list of drug labels matching the search term + */ + @GetMapping("/labelsearch/{searchTerm}") + public ResponseEntity> searchDrugLabels(@PathVariable("searchTerm") String searchTerm) { + return ok(drugLabelRepository.searchNameContainsTerm(searchTerm)); + } + + /** + * Provides a high level description of the information found in the Common + * Evidence Model (CEM). + * + * @summary Get summary of the Common Evidence Model (CEM) contents + * @param sourceKey The source key containing the CEM daimon + * @return A collection of evidence information stored in CEM + */ + @GetMapping("/{sourceKey}/info") + public ResponseEntity> getInfo(@PathVariable("sourceKey") String sourceKey) { + Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); + String sqlPath = "/resources/evidence/sql/getInfo.sql"; + String tqName = "cem_schema"; + String tqValue = source.getTableQualifier(SourceDaimon.DaimonType.CEM); + PreparedStatementRenderer psr = new PreparedStatementRenderer(source, sqlPath, tqName, tqValue); + return ok(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { + + EvidenceInfo info = new EvidenceInfo(); + info.title = rs.getString("TITLE"); + info.description = rs.getString("DESCRIPTION"); + info.provenance = rs.getString("PROVENANCE"); + info.contributor = rs.getString("CONTRIBUTOR"); + info.contactName = rs.getString("CONTACT_NAME"); + info.creationDate = rs.getDate("CREATION_DATE"); + info.coverageStartDate = rs.getDate("COVERAGE_START_DATE"); + info.coverageEndDate = rs.getDate("COVERAGE_END_DATE"); + info.versionIdentifier = rs.getString("VERSION_IDENTIFIER"); + return info; + })); + } + + /** + * Searches the evidence base for evidence related to one ore more drug and + * condition combinations for the source(s) specified + * + * @param sourceKey The source key containing the CEM daimon + * @param searchParams + * @return + */ + @PostMapping("/{sourceKey}/drugconditionpairs") + public ResponseEntity> getDrugConditionPairs(@PathVariable("sourceKey") String sourceKey, @RequestBody DrugConditionSourceSearchParams searchParams) { + Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); + String sql = getDrugHoiEvidenceSQL(source, searchParams); + return ok(daoService.getSourceJdbcTemplate(source).query(sql, (rs, rowNum) -> { + String evidenceSource = rs.getString("SOURCE_ID"); + String mappingType = rs.getString("MAPPING_TYPE"); + String drugConceptId = rs.getString("DRUG_CONCEPT_ID"); + String drugConceptName = rs.getString("DRUG_CONCEPT_NAME"); + String conditionConceptId = rs.getString("CONDITION_CONCEPT_ID"); + String conditionConceptName = rs.getString("CONDITION_CONCEPT_NAME"); + String uniqueIdentifier = rs.getString("UNIQUE_IDENTIFIER"); + + DrugHoiEvidence evidence = new DrugHoiEvidence(); + evidence.evidenceSource = evidenceSource; + evidence.mappingType = mappingType; + evidence.drugConceptId = drugConceptId; + evidence.drugConceptName = drugConceptName; + evidence.hoiConceptId = conditionConceptId; + evidence.hoiConceptName = conditionConceptName; + evidence.uniqueIdentifier = uniqueIdentifier; + + return evidence; + })); + } + + /** + * Retrieves a list of evidence for the specified drug conceptId + * + * @summary Get Evidence For Drug + * @param sourceKey The source key containing the CEM daimon + * @param id - An RxNorm Drug Concept Id + * @return A list of evidence + */ + @GetMapping("/{sourceKey}/drug/{id}") + public ResponseEntity> getDrugEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id) { + Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); + PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); + return ok(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { + String evidenceSource = rs.getString("SOURCE_ID"); + String hoi = rs.getString("CONCEPT_ID_2"); + String hoiName = rs.getString("CONCEPT_ID_2_NAME"); + String statType = rs.getString("STATISTIC_VALUE_TYPE"); + BigDecimal statVal = rs.getBigDecimal("STATISTIC_VALUE"); + String relationshipType = rs.getString("RELATIONSHIP_ID"); + String uniqueIdentifier = rs.getString("UNIQUE_IDENTIFIER"); + String uniqueIdentifierType = rs.getString("UNIQUE_IDENTIFIER_TYPE"); + + DrugEvidence evidence = new DrugEvidence(); + evidence.evidenceSource = evidenceSource; + evidence.hoiConceptId = hoi; + evidence.hoiConceptName = hoiName; + evidence.relationshipType = relationshipType; + evidence.statisticType = statType; + evidence.statisticValue = statVal; + evidence.uniqueIdentifier = uniqueIdentifier; + evidence.uniqueIdentifierType = uniqueIdentifierType; + + return evidence; + })); + } + + /** + * Retrieves a list of evidence for the specified health outcome of interest + * (hoi) conceptId + * + * @summary Get Evidence For Health Outcome + * @param sourceKey The source key containing the CEM daimon + * @param id The conceptId for the health outcome of interest + * @return A list of evidence + */ + @GetMapping("/{sourceKey}/hoi/{id}") + public ResponseEntity> getHoiEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id) { + Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); + PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); + return ok(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { + String evidenceSource = rs.getString("SOURCE_ID"); + String drug = rs.getString("CONCEPT_ID_1"); + String drugName = rs.getString("CONCEPT_ID_1_NAME"); + String statType = rs.getString("STATISTIC_VALUE_TYPE"); + BigDecimal statVal = rs.getBigDecimal("STATISTIC_VALUE"); + String relationshipType = rs.getString("RELATIONSHIP_ID"); + String uniqueIdentifier = rs.getString("UNIQUE_IDENTIFIER"); + String uniqueIdentifierType = rs.getString("UNIQUE_IDENTIFIER_TYPE"); + + HoiEvidence evidence = new HoiEvidence(); + evidence.evidenceSource = evidenceSource; + evidence.drugConceptId = drug; + evidence.drugConceptName = drugName; + evidence.relationshipType = relationshipType; + evidence.statisticType = statType; + evidence.statisticValue = statVal; + evidence.uniqueIdentifier = uniqueIdentifier; + evidence.uniqueIdentifierType = uniqueIdentifierType; + + return evidence; + })); + } + + /** + * Retrieves a list of RxNorm ingredients from the concept set and + * determines if we have label evidence for them. + * + * @summary Get Drug Labels For RxNorm Ingredients + * @param sourceKey The source key of the CEM daimon + * @param identifiers The list of RxNorm Ingredients concepts or ancestors + * @return A list of evidence for the drug and HOI + */ + @PostMapping("/{sourceKey}/druglabel") + public ResponseEntity> getDrugIngredientLabel(@PathVariable("sourceKey") String sourceKey, @RequestBody long[] identifiers) { + Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); + return ok(executeGetDrugLabels(identifiers, source)); + } + + /** + * Retrieves a list of evidence for the specified health outcome of interest + * and drug as defined in the key parameter. + * + * @summary Get Evidence For Drug & Health Outcome + * @param sourceKey The source key of the CEM daimon + * @param key The key must be structured as {drugConceptId}-{hoiConceptId} + * @return A list of evidence for the drug and HOI + */ + @GetMapping("/{sourceKey}/drughoi/{key}") + public ResponseEntity> getDrugHoiEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("key") final String key) { + Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); + PreparedStatementRenderer psr = prepareGetDrugHoiEvidence(key, source); + return ok(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { + String evidenceSource = rs.getString("SOURCE_ID"); + String drug = rs.getString("CONCEPT_ID_1"); + String drugName = rs.getString("CONCEPT_ID_1_NAME"); + String hoi = rs.getString("CONCEPT_ID_2"); + String hoiName = rs.getString("CONCEPT_ID_2_NAME"); + String statType = rs.getString("STATISTIC_VALUE_TYPE"); + BigDecimal statVal = rs.getBigDecimal("STATISTIC_VALUE"); + String relationshipType = rs.getString("RELATIONSHIP_ID"); + String uniqueIdentifier = rs.getString("UNIQUE_IDENTIFIER"); + String uniqueIdentifierType = rs.getString("UNIQUE_IDENTIFIER_TYPE"); + + DrugHoiEvidence evidence = new DrugHoiEvidence(); + evidence.evidenceSource = evidenceSource; + evidence.drugConceptId = drug; + evidence.drugConceptName = drugName; + evidence.hoiConceptId = hoi; + evidence.hoiConceptName = hoiName; + evidence.relationshipType = relationshipType; + evidence.statisticType = statType; + evidence.statisticValue = statVal; + evidence.uniqueIdentifier = uniqueIdentifier; + evidence.uniqueIdentifierType = uniqueIdentifierType; + + return evidence; + })); + } + + /** + * Originally provided a roll up of evidence from LAERTES + * + * @summary Depreciated + * @deprecated + * @param sourceKey The source key of the CEM daimon + * @param id The RxNorm drug conceptId + * @param filter Specified the type of rollup level (ingredient, clinical + * drug, branded drug) + * @return A list of evidence rolled up + */ + @GetMapping("/{sourceKey}/drugrollup/{filter}/{id}") + public ResponseEntity> getDrugRollupIngredientEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id, @PathVariable("filter") final String filter) { + String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; + ArrayList evidence = new ArrayList<>(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(evidence); + } + + /** + * Retrieve all evidence from Common Evidence Model (CEM) for a given + * conceptId + * + * @summary Get evidence for a concept + * @param sourceKey The source key of the CEM daimon + * @param id The conceptId of interest + * @return A list of evidence matching the conceptId of interest + */ + @GetMapping("/{sourceKey}/{id}") + public ResponseEntity> getEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id) { + Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); + PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); + return ok(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { + String evidenceSource = rs.getString("SOURCE_ID"); + String drug = rs.getString("CONCEPT_ID_1"); + String drugName = rs.getString("CONCEPT_ID_1_NAME"); + String hoi = rs.getString("CONCEPT_ID_2"); + String hoiName = rs.getString("CONCEPT_ID_2_NAME"); + String statType = rs.getString("STATISTIC_VALUE_TYPE"); + BigDecimal statVal = rs.getBigDecimal("STATISTIC_VALUE"); + String relationshipType = rs.getString("RELATIONSHIP_ID"); + String uniqueIdentifier = rs.getString("UNIQUE_IDENTIFIER"); + String uniqueIdentifierType = rs.getString("UNIQUE_IDENTIFIER_TYPE"); + + Evidence evidence = new Evidence(); + evidence.evidenceSource = evidenceSource; + evidence.drugConceptId = drug; + evidence.drugConceptName = drugName; + evidence.hoiConceptId = hoi; + evidence.hoiConceptName = hoiName; + evidence.relationshipType = relationshipType; + evidence.statisticType = statType; + evidence.statisticValue = statVal; + evidence.uniqueIdentifier = uniqueIdentifier; + evidence.uniqueIdentifierType = uniqueIdentifierType; + + return evidence; + })); + } + + /** + * Originally provided an evidence summary from LAERTES + * + * @summary Depreciated + * @deprecated + * @param sourceKey The source key of the CEM daimon + * @param conditionID The condition conceptId + * @param drugID The drug conceptId + * @param evidenceGroup The evidence group + * @return A summary of evidence + */ + @GetMapping("/{sourceKey}/evidencesummary") + public ResponseEntity> getEvidenceSummaryBySource(@PathVariable("sourceKey") String sourceKey, @RequestParam(required = false) String conditionID, @RequestParam(required = false) String drugID, @RequestParam(required = false) String evidenceGroup) { + String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; + ArrayList evidenceSummary = new ArrayList<>(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(evidenceSummary); + } + + /** + * Originally provided an evidence details from LAERTES + * + * @summary Depreciated + * @deprecated + * @param sourceKey The source key of the CEM daimon + * @param conditionID The condition conceptId + * @param drugID The drug conceptId + * @param evidenceType The evidence type + * @return A list of evidence details + * @throws org.codehaus.jettison.json.JSONException + * @throws java.io.IOException + */ + @GetMapping("/{sourceKey}/evidencedetails") + public ResponseEntity> getEvidenceDetails(@PathVariable("sourceKey") String sourceKey, + @RequestParam(required = false) String conditionID, + @RequestParam(required = false) String drugID, + @RequestParam(required = false) String evidenceType) + throws JSONException, IOException { + String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; + ArrayList evidenceDetails = new ArrayList<>(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(evidenceDetails); + } + + /** + * Originally provided an summary from spontaneous reports from LAERTES + * + * @summary Depreciated + * @deprecated + * @param sourceKey The source key of the CEM daimon + * @param search The search term + * @return A list of spontaneous report summaries + * @throws JSONException + * @throws IOException + */ + @PostMapping("/{sourceKey}/spontaneousreports") + public ResponseEntity> getSpontaneousReports(@PathVariable("sourceKey") String sourceKey, @RequestBody EvidenceSearch search) throws JSONException, IOException { + String warningMessage = "This method will be deprecated in the next release."; + ArrayList returnVal = new ArrayList<>(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(returnVal); + } + + /** + * Originally provided an evidence search from LAERTES + * + * @summary Depreciated + * @deprecated + * @param sourceKey The source key of the CEM daimon + * @param search The search term + * @return A list of evidence + * @throws JSONException + * @throws IOException + */ + @PostMapping("/{sourceKey}/evidencesearch") + public ResponseEntity> evidenceSearch(@PathVariable("sourceKey") String sourceKey, @RequestBody EvidenceSearch search) throws JSONException, IOException { + String warningMessage = "This method will be deprecated in the next release."; + ArrayList returnVal = new ArrayList<>(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(returnVal); + } + + /** + * Originally provided a label evidence search from LAERTES + * + * @summary Depreciated + * @deprecated + * @param sourceKey The source key of the CEM daimon + * @param search The search term + * @return A list of evidence + * @throws JSONException + * @throws IOException + */ + @PostMapping("/{sourceKey}/labelevidence") + public ResponseEntity> labelEvidence(@PathVariable("sourceKey") String sourceKey, @RequestBody EvidenceSearch search) throws JSONException, IOException { + String warningMessage = "This method will be deprecated in the next release."; + ArrayList returnVal = new ArrayList<>(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(returnVal); + } + + /** + * Queues up a negative control generation task to compute negative controls + * using Common Evidence Model (CEM) + * + * @summary Generate negative controls + * @param sourceKey The source key of the CEM daimon + * @param task - The negative control task with parameters + * @return information about the negative control job + * @throws Exception + */ + @PostMapping("/{sourceKey}/negativecontrols") + public ResponseEntity queueNegativeControlsJob(@PathVariable("sourceKey") String sourceKey, @RequestBody NegativeControlTaskParameters task) throws Exception { + if (task == null) { + return ok(null); + } + JobParametersBuilder builder = new JobParametersBuilder(); + + // Get a JDBC template for the OHDSI source repository + // and the source dialect for use when we write the results + // back to the OHDSI repository + JdbcTemplate jdbcTemplate = daoService.getJdbcTemplate(); + task.setJdbcTemplate(jdbcTemplate); + String ohdsiDatasourceSourceDialect = daoService.getSourceDialect(); + task.setSourceDialect(ohdsiDatasourceSourceDialect); + task.setOhdsiSchema(daoService.getOhdsiSchema()); + + // source key comes from the client, we look it up here and hand it off to the tasklet + Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); + // Verify the source has both the evidence & results daimon configured + // and throw an exception if either is missing + String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); + String cemResultsSchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.CEMResults); + if (cemSchema == null) { + throw new RuntimeException("Evidence daimon not configured for source."); + } + if (cemResultsSchema == null) { + throw new RuntimeException("Results daimon not configured for source."); + } + + task.setSource(source); + + if (!StringUtils.isEmpty(task.getJobName())) { + builder.addString("jobName", limitJobParams(task.getJobName())); + } + builder.addString("concept_set_id", ("" + task.getConceptSetId())); + builder.addString("concept_set_name", task.getConceptSetName()); + builder.addString("concept_domain_id", task.getConceptDomainId()); + builder.addString("source_id", ("" + source.getSourceId())); + + // Create a set of parameters to store with the generation info + JSONObject params = new JSONObject(); + params.put("csToInclude", task.getCsToInclude()); + params.put("csToExclude", task.getCsToExclude()); + builder.addString("params", params.toString()); + + // Resolve the concept set expressions for the included and excluded + // concept sets if specified + ConceptSetExpressionQueryBuilder csBuilder = new ConceptSetExpressionQueryBuilder(); + ConceptSetExpression csExpression; + String csSQL = ""; + if (task.getCsToInclude() > 0) { + try { + csExpression = conceptSetService.getConceptSetExpression(task.getCsToInclude()); + csSQL = csBuilder.buildExpressionQuery(csExpression); + } catch (Exception e) { + // log warning would go here if logger was available + } + } + task.setCsToIncludeSQL(csSQL); + csSQL = ""; + if (task.getCsToExclude() > 0) { + try { + csExpression = conceptSetService.getConceptSetExpression(task.getCsToExclude()); + csSQL = csBuilder.buildExpressionQuery(csExpression); + } catch (Exception e) { + // log warning would go here if logger was available + } + } + task.setCsToExcludeSQL(csSQL); + + final JobParameters jobParameters = builder.toJobParameters(); + + NegativeControlTasklet tasklet = new NegativeControlTasklet(task, daoService.getSourceJdbcTemplate(task.getSource()), task.getJdbcTemplate(), + daoService.getTransactionTemplate(), this.conceptSetGenerationInfoRepository, daoService.getSourceDialect()); + + return ok(this.jobTemplate.launchTasklet(NAME, "negativeControlsAnalysisStep", tasklet, jobParameters)); + } + + /** + * Retrieves the negative controls for a concept set + * + * @summary Retrieve negative controls + * @param sourceKey The source key of the CEM daimon + * @param conceptSetId The concept set id + * @return The list of negative controls + */ + @GetMapping("/{sourceKey}/negativecontrols/{conceptsetid}") + public ResponseEntity> getNegativeControls(@PathVariable("sourceKey") String sourceKey, @PathVariable("conceptsetid") int conceptSetId) throws Exception { + Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); + PreparedStatementRenderer psr = this.prepareGetNegativeControls(source, conceptSetId); + final List recs = daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), new NegativeControlMapper()); + return ok(recs); + } + + /** + * Retrieves parameterized SQL used to generate negative controls + * + * @summary Retrieves parameterized SQL used to generate negative controls + * @param sourceKey The source key of the CEM daimon + * @return The list of negative controls + */ + @GetMapping(value = "/{sourceKey}/negativecontrols/sql", produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity getNegativeControlsSqlStatement(@PathVariable("sourceKey") String sourceKey, + @RequestParam(defaultValue = "CONDITION") String conceptDomain, + @RequestParam(defaultValue = "DRUG") String targetDomain, + @RequestParam(defaultValue = "192671") String conceptOfInterest) { + NegativeControlTaskParameters task = new NegativeControlTaskParameters(); + Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); + task.setSource(source); + task.setCsToIncludeSQL(""); + task.setCsToExcludeSQL(""); + task.setConceptDomainId(conceptDomain); + task.setOutcomeOfInterest(targetDomain); + CharSequence csCommaDelimited = ","; + if (conceptOfInterest.contains(csCommaDelimited)) { + task.setConceptsOfInterest(conceptOfInterest.split(",")); + } else { + task.setConceptsOfInterest(new String[]{conceptOfInterest}); + } + return ok(getNegativeControlSql(task)); + } + + @Override + public String getJobName() { + return NAME; + } + + @Override + public String getExecutionFoldingKey() { + return "concept_set_id"; + } + + /** + * Retrieve the SQL used to generate negative controls + * + * @summary Get negative control SQL + * @param task The task containing the parameters for generating negative + * controls + * @return The SQL script for generating negative controls + */ + public static String getNegativeControlSql(NegativeControlTaskParameters task) { + StringBuilder sb = new StringBuilder(); + String resourceRoot = "/resources/evidence/sql/negativecontrols/"; + Source source = task.getSource(); + String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); + String cemResultsSchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.CEMResults); + String vocabularySchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.Vocabulary); + if (vocabularySchema == null) { + vocabularySchema = cemSchema; + } + String translatedSchema = task.getTranslatedSchema(); + if (translatedSchema == null) { + translatedSchema = cemSchema; + } + + String csToExcludeSQL = SqlRender.renderSql(task.getCsToExcludeSQL(), + new String[]{"vocabulary_database_schema"}, + new String[]{vocabularySchema} + ); + String csToIncludeSQL = SqlRender.renderSql(task.getCsToIncludeSQL(), + new String[]{"vocabulary_database_schema"}, + new String[]{vocabularySchema} + ); + + String outcomeOfInterest = task.getOutcomeOfInterest().toLowerCase(); + String conceptsOfInterest = JoinArray(task.getConceptsOfInterest()); + String csToInclude = String.valueOf(task.getCsToInclude()); + String csToExclude = String.valueOf(task.getCsToExclude()); + String medlineWinnenburgTable = translatedSchema + ".MEDLINE_WINNENBURG"; + String splicerTable = translatedSchema + ".SPLICER"; + String aeolusTable = translatedSchema + ".AEOLUS"; + String conceptsToExcludeData = "#NC_EXCLUDED_CONCEPTS"; + String conceptsToIncludeData = "#NC_INCLUDED_CONCEPTS"; + String broadConceptsData = cemSchema + ".NC_LU_BROAD_CONCEPTS"; + String drugInducedConditionsData = cemSchema + ".NC_LU_DRUG_INDUCED_CONDITIONS"; + String pregnancyConditionData = cemSchema + ".NC_LU_PREGNANCY_CONDITIONS"; + + String[] params = new String[]{"outcomeOfInterest", "conceptsOfInterest", "vocabulary", "cem_schema", "cem_results_schema", "translatedSchema"}; + String[] values = new String[]{outcomeOfInterest, conceptsOfInterest, vocabularySchema, cemSchema, cemResultsSchema, translatedSchema}; + + String sqlFile = "findConceptUniverse.sql"; + sb.append("-- ").append(sqlFile).append("\n\n"); + String sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, params, values); + sb.append(sql + "\n\n"); + + sqlFile = "findDrugIndications.sql"; + sb.append("-- ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, params, values); + sb.append(sql + "\n\n"); + + sqlFile = "findConcepts.sql"; + sb.append("-- User excluded - ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, + ArrayUtils.addAll(params, new String[]{"storeData", "conceptSetId", "conceptSetExpression"}), + ArrayUtils.addAll(values, new String[]{conceptsToExcludeData, csToExclude, csToExcludeSQL}) + ); + sb.append(sql + "\n\n"); + + sqlFile = "findConcepts.sql"; + sb.append("-- User included - ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, + ArrayUtils.addAll(params, new String[]{"storeData", "conceptSetId", "conceptSetExpression"}), + ArrayUtils.addAll(values, new String[]{conceptsToIncludeData, csToInclude, csToIncludeSQL}) + ); + sb.append(sql + "\n\n"); + + sqlFile = "pullEvidencePrep.sql"; + sb.append("-- ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, params, values); + sb.append(sql + "\n\n"); + + sqlFile = "pullEvidence.sql"; + sb.append("-- MEDLINE_WINNENBURG -- ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, + ArrayUtils.addAll(params, new String[]{"adeType", "adeData"}), + ArrayUtils.addAll(values, new String[]{"MEDLINE_WINNENBURG", medlineWinnenburgTable}) + ); + sb.append(sql + "\n\n"); + + sqlFile = "pullEvidence.sql"; + sb.append("-- SPLICER -- ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, + ArrayUtils.addAll(params, new String[]{"adeType", "adeData"}), + ArrayUtils.addAll(values, new String[]{"SPLICER", splicerTable}) + ); + sb.append(sql + "\n\n"); + + sqlFile = "pullEvidence.sql"; + sb.append("-- AEOLUS -- ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, + ArrayUtils.addAll(params, new String[]{"adeType", "adeData"}), + ArrayUtils.addAll(values, new String[]{"AEOLUS", aeolusTable}) + ); + sb.append(sql + "\n\n"); + + sqlFile = "pullEvidencePost.sql"; + sb.append("-- ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, params, values); + sb.append(sql + "\n\n"); + + sqlFile = "summarizeEvidence.sql"; + sb.append("-- ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, + ArrayUtils.addAll(params, new String[]{"broadConceptsData", "drugInducedConditionsData", "pregnancyConditionData", "conceptsToExclude", "conceptsToInclude"}), + ArrayUtils.addAll(values, new String[]{broadConceptsData, drugInducedConditionsData, pregnancyConditionData, conceptsToExcludeData, conceptsToIncludeData}) + ); + sb.append(sql + "\n\n"); + + sqlFile = "optimizeEvidence.sql"; + sb.append("-- ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, params, values); + sb.append(sql + "\n\n"); + + sqlFile = "deleteJobResults.sql"; + sb.append("-- ").append(sqlFile).append("\n\n"); + sql = getJobResultsDeleteStatementSql(cemResultsSchema, task.getConceptSetId()); + sb.append(sql + "\n\n"); + + sqlFile = "exportNegativeControls.sql"; + sb.append("-- ").append(sqlFile).append("\n\n"); + sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); + sql = SqlRender.renderSql(sql, + ArrayUtils.addAll(params, new String[]{"conceptSetId"}), + ArrayUtils.addAll(values, new String[]{Integer.toString(task.getConceptSetId())}) + ); + sb.append(sql + "\n\n"); + + sql = SqlTranslate.translateSql(sb.toString(), source.getSourceDialect()); + + return sql; + } + + /** + * SQL to delete negative controls job results + * + * @summary SQL to delete negative controls job results + * @param cemResultsSchema The CEM results schema + * @param conceptSetId The concept set ID + * @return The SQL statement + */ + public static String getJobResultsDeleteStatementSql(String cemResultsSchema, int conceptSetId) { + String sql = ResourceHelper.GetResourceAsString("/resources/evidence/sql/negativecontrols/deleteJobResults.sql"); + sql = SqlRender.renderSql(sql, + (new String[]{"cem_results_schema", "conceptSetId"}), + (new String[]{cemResultsSchema, Integer.toString(conceptSetId)}) + ); + return sql; + } + + /** + * SQL to insert negative controls + * + * @summary SQL to insert negative controls + * @param task The negative control task and parameters + * @return The SQL statement + */ + public static String getNegativeControlInsertStatementSql(NegativeControlTaskParameters task) { + String sql = ResourceHelper.GetResourceAsString("/resources/evidence/sql/negativecontrols/insertNegativeControls.sql"); + sql = SqlRender.renderSql(sql, new String[]{"ohdsiSchema"}, new String[]{task.getOhdsiSchema()}); + sql = SqlTranslate.translateSql(sql, task.getSourceDialect()); + + return sql; + } + + protected PreparedStatementRenderer prepareExecuteGetDrugLabels(long[] identifiers, Source source) { + String sqlPath = "/resources/evidence/sql/getDrugLabelForIngredients.sql"; + String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); + String vocabularySchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.Vocabulary); + if (vocabularySchema == null) { + vocabularySchema = cemSchema; + } + String[] tableQualifierNames = new String[]{"cem_schema", "vocabularySchema"}; + String[] tableQualifierValues = new String[]{cemSchema, vocabularySchema}; + return new PreparedStatementRenderer(source, sqlPath, tableQualifierNames, tableQualifierValues, "conceptIds", identifiers); + } + + /** + * Get the SQL for obtaining product label evidence from a set of RxNorm + * Ingredients + * + * @summary SQL for obtaining product label evidence + * @param identifiers The list of RxNorm Ingredient conceptIds + * @param source The source that contains the CEM daimon + * @return A prepared SQL statement + */ + protected Collection executeGetDrugLabels(long[] identifiers, Source source) { + Collection info = new ArrayList<>(); + if (identifiers.length == 0) { + return info; + } else { + int parameterLimit = PreparedSqlRender.getParameterLimit(source); + if (parameterLimit > 0 && identifiers.length > parameterLimit) { + info = executeGetDrugLabels(Arrays.copyOfRange(identifiers, parameterLimit, identifiers.length), source); + identifiers = Arrays.copyOfRange(identifiers, 0, parameterLimit); + } + PreparedStatementRenderer psr = prepareExecuteGetDrugLabels(identifiers, source); + info.addAll(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), this.drugLabelRowMapper)); + return info; + } + } + + /** + * Get the SQL for obtaining evidence for a drug/condition pair for source + * ids + * + * @summary SQL for obtaining evidence for a drug/hoi pair by source + * @param source The source that contains the CEM daimon + * @return A prepared SQL statement + */ + protected String getDrugHoiEvidenceSQL(Source source, DrugConditionSourceSearchParams searchParams) { + String sqlPath = "/resources/evidence/sql/getDrugConditionPairBySourceId.sql"; + String sql = ResourceHelper.GetResourceAsString(sqlPath); + String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); + String vocabularySchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.Vocabulary); + if (vocabularySchema == null) { + vocabularySchema = cemSchema; + } + String[] params = new String[]{"cem_schema", "vocabularySchema", "targetDomain", "sourceIdList", "drugList", "conditionList"}; + String[] values = new String[]{cemSchema, vocabularySchema, searchParams.targetDomain.toUpperCase(), searchParams.getSourceIds(), searchParams.getDrugConceptIds(), searchParams.getConditionConceptIds()}; + sql = SqlRender.renderSql(sql, params, values); + sql = SqlTranslate.translateSql(sql, source.getSourceDialect()); + return sql; + } + + /** + * Get the SQL for obtaining evidence for a drug/hoi combination + * + * @summary SQL for obtaining evidence for a drug/hoi combination + * @param key The drug-hoi conceptId pair + * @param source The source that contains the CEM daimon + * @return A prepared SQL statement + */ + protected PreparedStatementRenderer prepareGetDrugHoiEvidence(final String key, Source source) { + String[] par = key.split("-"); + String drug_id = par[0]; + String hoi_id = par[1]; + String sqlPath = "/resources/evidence/sql/getDrugHoiEvidence.sql"; + String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); + String vocabularySchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.Vocabulary); + if (vocabularySchema == null) { + vocabularySchema = cemSchema; + } + String[] tableQualifierNames = new String[]{"cem_schema", "vocabularySchema"}; + String[] tableQualifierValues = new String[]{cemSchema, vocabularySchema}; + String[] names = new String[]{"drug_id", "hoi_id"}; + Object[] values = new Integer[]{Integer.parseInt(drug_id), Integer.parseInt(hoi_id)}; + return new PreparedStatementRenderer(source, sqlPath, tableQualifierNames, tableQualifierValues, names, values); + } + + /** + * Get the SQL for obtaining evidence for a concept + * + * @summary SQL for obtaining evidence for a concept + * @param source The source that contains the CEM daimon + * @param conceptId The conceptId of interest + * @return A prepared SQL statement + */ + protected PreparedStatementRenderer prepareGetEvidenceForConcept(Source source, Long conceptId) { + String sqlPath = "/resources/evidence/sql/getEvidenceForConcept.sql"; + String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); + String vocabularySchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.Vocabulary); + if (vocabularySchema == null) { + vocabularySchema = cemSchema; + } + String[] tableQualifierNames = new String[]{"cem_schema", "vocabularySchema"}; + String[] tableQualifierValues = new String[]{cemSchema, vocabularySchema}; + String[] names = new String[]{"id"}; + Object[] values = new Long[]{conceptId}; + return new PreparedStatementRenderer(source, sqlPath, tableQualifierNames, tableQualifierValues, names, values); + } + + /** + * Get the SQL for obtaining negative controls for the concept set specified + * + * @summary SQL for obtaining negative controls + * @param source The source that contains the CEM daimon + * @param conceptSetId The conceptSetId associated to the negative controls + * @return A prepared SQL statement + */ + protected PreparedStatementRenderer prepareGetNegativeControls(Source source, int conceptSetId) { + String sqlPath = "/resources/evidence/sql/negativecontrols/getNegativeControls.sql"; + String cemResultsSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEMResults); + String[] tableQualifierNames = new String[]{"cem_results_schema"}; + String[] tableQualifierValues = new String[]{cemResultsSchema}; + String[] names = new String[]{"conceptSetId"}; + Object[] values = new Object[]{conceptSetId}; + return new PreparedStatementRenderer(source, sqlPath, tableQualifierNames, tableQualifierValues, names, values); + } + + private static String JoinArray(final String[] array) { + String result = ""; + + for (int i = 0; i < array.length; i++) { + if (i > 0) { + result += ","; + } + + result += "'" + array[i] + "'"; + } + + return result; + } + + private static String limitJobParams(String param) { + if (param.length() >= 250) { + return param.substring(0, 245) + "..."; + } + return param; + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java b/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java new file mode 100644 index 0000000000..54dc105c39 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java @@ -0,0 +1,217 @@ +package org.ohdsi.webapi.mvc; + +import org.apache.shiro.authz.UnauthorizedException; +import org.ohdsi.webapi.arachne.logging.event.FailedDbConnectEvent; +import org.ohdsi.webapi.exception.BadRequestAtlasException; +import org.ohdsi.webapi.exception.ConceptNotExistException; +import org.ohdsi.webapi.exception.ConversionAtlasException; +import org.ohdsi.webapi.exception.UserException; +import org.ohdsi.webapi.vocabulary.ConceptRecommendedNotInstalledException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.NotFoundException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Objects; + +/** + * Spring MVC Global Exception Handler + * + * Replaces Jersey JAX-RS exception mappers: + * - GenericExceptionMapper.java (handles all throwables) + * - JdbcExceptionMapper.java (handles database connection failures) + * + * Migration Status: Replaces both JAX-RS @Provider exception mappers + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class); + private static final String DETAIL = "Detail: "; + + @Autowired + private ApplicationEventPublisher eventPublisher; + + /** + * Handle database connection failures + * Replaces: JdbcExceptionMapper + */ + @ExceptionHandler(CannotGetJdbcConnectionException.class) + public ResponseEntity handleDatabaseConnectionException(CannotGetJdbcConnectionException exception) { + eventPublisher.publishEvent(new FailedDbConnectEvent(this, exception.getMessage())); + return ResponseEntity.ok().build(); + } + + /** + * Handle data integrity violations (e.g., unique constraint violations) + */ + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolation(DataIntegrityViolationException ex) { + logException(ex); + + String cause = ex.getCause().getCause().getMessage(); + cause = cause.substring(cause.indexOf(DETAIL) + DETAIL.length()); + RuntimeException sanitizedException = new RuntimeException(cause); + sanitizedException.setStackTrace(new StackTraceElement[0]); + + ErrorMessage errorMessage = new ErrorMessage(sanitizedException); + return ResponseEntity.status(HttpStatus.CONFLICT).body(errorMessage); + } + + /** + * Handle authorization/permission exceptions + */ + @ExceptionHandler({UnauthorizedException.class, ForbiddenException.class}) + public ResponseEntity handleAuthorizationException(Exception ex) { + logException(ex); + ex.setStackTrace(new StackTraceElement[0]); + ErrorMessage errorMessage = new ErrorMessage(ex); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorMessage); + } + + /** + * Handle not found exceptions + */ + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException ex) { + logException(ex); + ex.setStackTrace(new StackTraceElement[0]); + ErrorMessage errorMessage = new ErrorMessage(ex); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorMessage); + } + + /** + * Handle Spring MVC resource not found exceptions + * (e.g., when no controller mapping exists for a URL) + */ + @ExceptionHandler({NoResourceFoundException.class, NoHandlerFoundException.class}) + public ResponseEntity handleResourceNotFoundException(Exception ex) { + logException(ex); + RuntimeException sanitizedException = new RuntimeException("Resource not found"); + sanitizedException.setStackTrace(new StackTraceElement[0]); + ErrorMessage errorMessage = new ErrorMessage(sanitizedException); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorMessage); + } + + /** + * Handle bad request exceptions + */ + @ExceptionHandler({BadRequestException.class, ConceptNotExistException.class}) + public ResponseEntity handleBadRequestException(Exception ex) { + logException(ex); + ex.setStackTrace(new StackTraceElement[0]); + ErrorMessage errorMessage = new ErrorMessage(ex); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMessage); + } + + /** + * Handle concept not installed exceptions + */ + @ExceptionHandler(ConceptRecommendedNotInstalledException.class) + public ResponseEntity handleConceptNotInstalled(ConceptRecommendedNotInstalledException ex) { + logException(ex); + ex.setStackTrace(new StackTraceElement[0]); + ErrorMessage errorMessage = new ErrorMessage(ex); + return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).body(errorMessage); + } + + /** + * Handle undeclared throwable exceptions (proxied exceptions) + */ + @ExceptionHandler(UndeclaredThrowableException.class) + public ResponseEntity handleUndeclaredThrowable(UndeclaredThrowableException ex) { + logException(ex); + + Throwable throwable = getThrowable(ex); + HttpStatus status; + Throwable responseException; + + if (Objects.nonNull(throwable)) { + if (throwable instanceof UnauthorizedException || throwable instanceof ForbiddenException) { + status = HttpStatus.FORBIDDEN; + responseException = throwable; + } else if (throwable instanceof BadRequestAtlasException || throwable instanceof ConceptNotExistException) { + status = HttpStatus.BAD_REQUEST; + responseException = throwable; + } else if (throwable instanceof ConversionAtlasException) { + status = HttpStatus.BAD_REQUEST; + // New exception must be created or direct self-reference exception will be thrown + responseException = new RuntimeException(throwable.getMessage()); + } else { + status = HttpStatus.INTERNAL_SERVER_ERROR; + responseException = new RuntimeException("An exception occurred: " + ex.getClass().getName()); + } + } else { + status = HttpStatus.INTERNAL_SERVER_ERROR; + responseException = new RuntimeException("An exception occurred: " + ex.getClass().getName()); + } + + responseException.setStackTrace(new StackTraceElement[0]); + ErrorMessage errorMessage = new ErrorMessage(responseException); + return ResponseEntity.status(status).body(errorMessage); + } + + /** + * Handle user exceptions + */ + @ExceptionHandler(UserException.class) + public ResponseEntity handleUserException(UserException ex) { + logException(ex); + // Create new message to prevent sending error information to client + RuntimeException sanitizedException = new RuntimeException(ex.getMessage()); + sanitizedException.setStackTrace(new StackTraceElement[0]); + ErrorMessage errorMessage = new ErrorMessage(sanitizedException); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorMessage); + } + + /** + * Handle all other exceptions (fallback) + * Replaces: GenericExceptionMapper default case + */ + @ExceptionHandler(Throwable.class) + public ResponseEntity handleGenericException(Throwable ex) { + logException(ex); + // Create new message to prevent sending error information to client + RuntimeException sanitizedException = new RuntimeException("An exception occurred: " + ex.getClass().getName()); + sanitizedException.setStackTrace(new StackTraceElement[0]); + ErrorMessage errorMessage = new ErrorMessage(sanitizedException); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorMessage); + } + + /** + * Extract the target exception from UndeclaredThrowableException + */ + private Throwable getThrowable(UndeclaredThrowableException ex) { + if (Objects.nonNull(ex.getUndeclaredThrowable()) && ex.getUndeclaredThrowable() instanceof InvocationTargetException) { + InvocationTargetException ite = (InvocationTargetException) ex.getUndeclaredThrowable(); + return ite.getTargetException(); + } + return null; + } + + /** + * Log exception with full stack trace + */ + private void logException(Throwable ex) { + StringWriter errorStackTrace = new StringWriter(); + ex.printStackTrace(new PrintWriter(errorStackTrace)); + LOGGER.error(errorStackTrace.toString()); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java b/src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java new file mode 100644 index 0000000000..928459f919 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java @@ -0,0 +1,56 @@ +package org.ohdsi.webapi.mvc; + +import jakarta.ws.rs.core.MediaType; + +/** + * Utility methods to assist with Jersey to Spring MVC migration. + */ +public class MigrationUtils { + + /** + * Convert JAX-RS MediaType constant to Spring media type string + */ + public static String toSpringMediaType(String jaxrsMediaType) { + // JAX-RS uses constants like MediaType.APPLICATION_JSON + // Spring uses constants like MediaType.APPLICATION_JSON_VALUE + // This is mainly for documentation/reference + return switch (jaxrsMediaType) { + case MediaType.APPLICATION_JSON -> org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + case MediaType.APPLICATION_XML -> org.springframework.http.MediaType.APPLICATION_XML_VALUE; + case MediaType.TEXT_PLAIN -> org.springframework.http.MediaType.TEXT_PLAIN_VALUE; + case MediaType.TEXT_HTML -> org.springframework.http.MediaType.TEXT_HTML_VALUE; + case MediaType.MULTIPART_FORM_DATA -> org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; + case MediaType.APPLICATION_FORM_URLENCODED -> org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; + default -> jaxrsMediaType; + }; + } + + /** + * Annotation mapping reference for migration + */ + public static class AnnotationMapping { + /* + * JAX-RS to Spring MVC Annotation Mapping Guide: + * + * @Path("/api") → @RequestMapping("/WebAPI/api") + * @GET → @GetMapping + * @POST → @PostMapping + * @PUT → @PutMapping + * @DELETE → @DeleteMapping + * @PathParam("id") → @PathVariable("id") + * @QueryParam("name") → @RequestParam(value="name") + * @FormParam("field") → @RequestParam("field") + * @FormDataParam("file") → @RequestPart("file") // for MultipartFile + * @Produces(APPLICATION_JSON) → produces = APPLICATION_JSON_VALUE + * @Consumes(APPLICATION_JSON) → consumes = APPLICATION_JSON_VALUE + * Response → ResponseEntity + * Response.ok(entity) → ResponseEntity.ok(entity) + * Response.status(404) → ResponseEntity.status(404) or ResponseEntity.notFound() + * + * Provider Classes: + * @Provider + ExceptionMapper → @ControllerAdvice + @ExceptionHandler + * @Provider + ContainerRequestFilter → HandlerInterceptor + * @Provider + MessageBodyWriter → HttpMessageConverter + */ + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/NotificationMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/NotificationMvcController.java new file mode 100644 index 0000000000..1abfa0e132 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/NotificationMvcController.java @@ -0,0 +1,110 @@ +package org.ohdsi.webapi.mvc; + +import org.apache.commons.lang3.StringUtils; +import org.ohdsi.webapi.job.JobExecutionInfo; +import org.ohdsi.webapi.job.JobExecutionResource; +import org.ohdsi.webapi.job.NotificationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.BatchStatus; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * REST Services related to working with the system notifications + * + * @summary Notifications + */ +@RestController +@RequestMapping("/notifications") +@Transactional +public class NotificationMvcController extends AbstractMvcController { + private static final Logger log = LoggerFactory.getLogger(NotificationMvcController.class); + + private final NotificationService service; + private final GenericConversionService conversionService; + + public NotificationMvcController(final NotificationService service, @Qualifier("conversionService") GenericConversionService conversionService) { + this.service = service; + this.conversionService = conversionService; + } + + /** + * Get the list of notifications + * + * @summary Get all notifications + * @param hideStatuses Used to filter statuses - passes as a comma-delimited + * list + * @param refreshJobs Boolean - when true, it will refresh the cache + * of notifications + * @return + */ + @GetMapping("/") + @Transactional(readOnly = true) + public ResponseEntity> list( + @RequestParam(value = "hide_statuses", required = false) String hideStatuses, + @RequestParam(value = "refreshJobs", defaultValue = "FALSE") Boolean refreshJobs) { + List statuses = new ArrayList<>(); + if (StringUtils.isNotEmpty(hideStatuses)) { + for (String status : hideStatuses.split(",")) { + try { + statuses.add(BatchStatus.valueOf(status)); + } catch (IllegalArgumentException e) { + log.warn("Invalid argument passed as batch status: {}", status); + } + } + } + List executionInfos; + if (refreshJobs) { + executionInfos = service.findRefreshCacheLastJobs(); + } else { + executionInfos = service.findLastJobs(statuses); + } + return ok(executionInfos.stream().map(this::toDTO).collect(Collectors.toList())); + } + + /** + * Gets the date when notifications were last viewed + * + * @summary Get notification last viewed date + * @return The date when notifications were last viewed + */ + @GetMapping("/viewed") + @Transactional(readOnly = true) + public ResponseEntity getLastViewedTime() { + try { + return ok(service.getLastViewedTime()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Sets the date when notifications were last viewed + * + * @summary Set notification last viewed date + * @param stamp + */ + @PostMapping("/viewed") + public ResponseEntity setLastViewedTime(@RequestBody Date stamp) { + try { + service.setLastViewedTime(stamp); + return ok(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private JobExecutionResource toDTO(JobExecutionInfo entity) { + return conversionService.convert(entity, JobExecutionResource.class); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/OutputStreamMessageConverter.java b/src/main/java/org/ohdsi/webapi/mvc/OutputStreamMessageConverter.java new file mode 100644 index 0000000000..eed1d22100 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/OutputStreamMessageConverter.java @@ -0,0 +1,60 @@ +package org.ohdsi.webapi.mvc; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** + * Spring MVC HttpMessageConverter for ByteArrayOutputStream + * + * Replaces Jersey JAX-RS MessageBodyWriter: + * - OutputStreamWriter.java + * + * This converter allows controllers to return ByteArrayOutputStream directly, + * which is useful for streaming/downloading generated content. + * + * Migration Status: Replaces JAX-RS @Provider MessageBodyWriter + */ +@Component +public class OutputStreamMessageConverter implements HttpMessageConverter { + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + // We don't support reading ByteArrayOutputStream from requests + return false; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return ByteArrayOutputStream.class.equals(clazz); + } + + @Override + public List getSupportedMediaTypes() { + // Support all media types (like Jersey's implementation) + return Collections.singletonList(MediaType.ALL); + } + + @Override + public ByteArrayOutputStream read(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + throw new UnsupportedOperationException("Reading ByteArrayOutputStream not supported"); + } + + @Override + public void write(ByteArrayOutputStream outputStream, MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + // Write the ByteArrayOutputStream contents to the response output stream + outputStream.writeTo(outputMessage.getBody()); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java b/src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java new file mode 100644 index 0000000000..dbaa401637 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java @@ -0,0 +1,61 @@ +package org.ohdsi.webapi.mvc; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import jakarta.ws.rs.core.Response; + +/** + * Utility class to convert JAX-RS Response objects to Spring ResponseEntity. + * Used during migration to facilitate gradual conversion of endpoints. + */ +public class ResponseConverters { + + /** + * Convert JAX-RS Response to Spring ResponseEntity + */ + public static ResponseEntity toResponseEntity(Response response) { + if (response == null) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + + HttpStatus status = HttpStatus.valueOf(response.getStatus()); + + @SuppressWarnings("unchecked") + T body = (T) response.getEntity(); + + if (body == null) { + return ResponseEntity.status(status).build(); + } + + return ResponseEntity.status(status).body(body); + } + + /** + * Convert JAX-RS Response.Status to Spring HttpStatus + */ + public static HttpStatus toHttpStatus(Response.Status status) { + return HttpStatus.valueOf(status.getStatusCode()); + } + + /** + * Convert JAX-RS Response.StatusType to Spring HttpStatus + */ + public static HttpStatus toHttpStatus(Response.StatusType statusType) { + return HttpStatus.valueOf(statusType.getStatusCode()); + } + + /** + * Create ResponseEntity from JAX-RS status and entity + */ + public static ResponseEntity fromJaxRs(Response.Status status, T entity) { + return ResponseEntity.status(toHttpStatus(status)).body(entity); + } + + /** + * Create ResponseEntity from status code and entity + */ + public static ResponseEntity fromStatusCode(int statusCode, T entity) { + return ResponseEntity.status(statusCode).body(entity); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/TagMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/TagMvcController.java new file mode 100644 index 0000000000..5b78e419a3 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/TagMvcController.java @@ -0,0 +1,132 @@ +package org.ohdsi.webapi.mvc; + +import org.apache.commons.lang3.StringUtils; +import org.ohdsi.webapi.tag.TagGroupService; +import org.ohdsi.webapi.tag.TagService; +import org.ohdsi.webapi.tag.dto.AssignmentPermissionsDTO; +import org.ohdsi.webapi.tag.dto.TagDTO; +import org.ohdsi.webapi.tag.dto.TagGroupSubscriptionDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Collections; +import java.util.List; + +@RestController +@RequestMapping("/tag") +public class TagMvcController extends AbstractMvcController { + private final TagService tagService; + private final TagGroupService tagGroupService; + + @Autowired + public TagMvcController(TagService tagService, + TagGroupService tagGroupService) { + this.tagService = tagService; + this.tagGroupService = tagGroupService; + } + + /** + * Creates a tag. + * + * @param dto + * @return + */ + @PostMapping("/") + public ResponseEntity create(@RequestBody final TagDTO dto) { + return ok(tagService.create(dto)); + } + + /** + * Returns list of tags, which names contain a provided substring. + * + * @summary Search tags by name part + * @param namePart + * @return + */ + @GetMapping("/search") + public ResponseEntity> search(@RequestParam("namePart") String namePart) { + if (StringUtils.isBlank(namePart)) { + return ok(Collections.emptyList()); + } + return ok(tagService.listInfoDTO(namePart)); + } + + /** + * Returns list of all tags. + * + * @return + */ + @GetMapping("/") + public ResponseEntity> list() { + return ok(tagService.listInfoDTO()); + } + + /** + * Updates tag with ID={id}. + * + * @param id + * @param dto + * @return + */ + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable("id") final Integer id, @RequestBody final TagDTO dto) { + return ok(tagService.update(id, dto)); + } + + /** + * Return tag by ID. + * + * @param id + * @return + */ + @GetMapping("/{id}") + public ResponseEntity get(@PathVariable("id") final Integer id) { + return ok(tagService.getDTOById(id)); + } + + /** + * Deletes tag with ID={id}. + * + * @param id + */ + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable("id") final Integer id) { + tagService.delete(id); + return ok(); + } + + /** + * Assignes group of tags to groups of assets. + * + * @param dto + * @return + */ + @PostMapping("/multiAssign") + public ResponseEntity assignGroup(@RequestBody final TagGroupSubscriptionDTO dto) { + tagGroupService.assignGroup(dto); + return ok(); + } + + /** + * Unassignes group of tags from groups of assets. + * + * @param dto + * @return + */ + @PostMapping("/multiUnassign") + public ResponseEntity unassignGroup(@RequestBody final TagGroupSubscriptionDTO dto) { + tagGroupService.unassignGroup(dto); + return ok(); + } + + /** + * Tags assignment permissions for current user + * + * @return + */ + @GetMapping("/assignmentPermissions") + public ResponseEntity assignmentPermissions() { + return ok(tagService.getAssignmentPermissions()); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/ToolMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/ToolMvcController.java new file mode 100644 index 0000000000..6ed7ebfac9 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/ToolMvcController.java @@ -0,0 +1,44 @@ +package org.ohdsi.webapi.mvc; + +import org.ohdsi.webapi.tool.ToolServiceImpl; +import org.ohdsi.webapi.tool.dto.ToolDTO; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/tool") +public class ToolMvcController extends AbstractMvcController { + private final ToolServiceImpl service; + + public ToolMvcController(ToolServiceImpl service) { + this.service = service; + } + + @GetMapping("") + public ResponseEntity> getTools() { + return ok(service.getTools()); + } + + @GetMapping("/{id}") + public ResponseEntity getToolById(@PathVariable("id") Integer id) { + return ok(service.getById(id)); + } + + @PostMapping("") + public ResponseEntity createTool(@RequestBody ToolDTO dto) { + return ok(service.saveTool(dto)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable("id") Integer id) { + service.delete(id); + return ok(); + } + + @PutMapping("") + public ResponseEntity updateTool(@RequestBody ToolDTO toolDTO) { + return ok(service.saveTool(toolDTO)); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/ActivityMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/ActivityMvcController.java new file mode 100644 index 0000000000..32973a899a --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/ActivityMvcController.java @@ -0,0 +1,38 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.ohdsi.webapi.activity.Tracker; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Spring MVC version of ActivityService + * + * Migration Status: Replaces /service/ActivityService.java (Jersey) + * Endpoints: 1 GET endpoint + * Complexity: Simple - deprecated, read-only + * + * @deprecated Example REST service - will be deprecated in a future release + */ +@RestController +@RequestMapping("/activity") +@Deprecated +public class ActivityMvcController extends AbstractMvcController { + + /** + * Get latest activity + * + * Jersey: GET /WebAPI/activity/latest + * Spring MVC: GET /WebAPI/v2/activity/latest + * + * @deprecated DO NOT USE - will be removed in future release + */ + @GetMapping(value = "/latest", produces = MediaType.APPLICATION_JSON_VALUE) + @Deprecated + public ResponseEntity getLatestActivity() { + return ok(Tracker.getActivity()); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CDMResultsMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CDMResultsMvcController.java new file mode 100644 index 0000000000..bd0dd86100 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/CDMResultsMvcController.java @@ -0,0 +1,268 @@ +package org.ohdsi.webapi.mvc.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.ohdsi.webapi.job.JobExecutionResource; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.report.CDMDashboard; +import org.ohdsi.webapi.report.CDMDataDensity; +import org.ohdsi.webapi.report.CDMDeath; +import org.ohdsi.webapi.report.CDMObservationPeriod; +import org.ohdsi.webapi.report.CDMPersonSummary; +import org.ohdsi.webapi.service.CDMResultsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.AbstractMap.SimpleEntry; +import java.util.List; + +/** + * Spring MVC version of CDMResultsService + * + * Migration Status: Replaces /service/CDMResultsService.java (Jersey) + * Endpoints: 11 endpoints (9 GET, 2 POST) + * Complexity: Medium - delegates to existing Jersey service for business logic + * + * This controller delegates to the existing Jersey CDMResultsService to preserve + * all business logic, caching, and job execution functionality. The Jersey service + * remains as a @Component for dependency injection but endpoints are now exposed + * via this Spring MVC controller. + */ +@RestController +@RequestMapping("/cdmresults") +public class CDMResultsMvcController extends AbstractMvcController { + + @Autowired + private CDMResultsService cdmResultsService; + + /** + * Get the record count and descendant record count for one or more concepts in a single CDM database + * + *

+ * This POST request accepts a json array containing one or more concept IDs. (e.g. [201826, 437827]) + *

+ * + * @param sourceKey The unique identifier for a CDM source (e.g. SYNPUF5PCT) + * @param identifiers List of concept IDs + * @return A list of concept IDs with their record counts and descendant record counts + * + *

+ * [ + * { + * "201826": [ + * 612861, + * 653173 + * ] + * }, + * { + * "437827": [ + * 224421, + * 224421 + * ] + * } + * ] + *

+ * + * Jersey: POST /WebAPI/cdmresults/{sourceKey}/conceptRecordCount + * Spring MVC: POST /WebAPI/v2/cdmresults/{sourceKey}/conceptRecordCount + */ + @PostMapping(value = "/{sourceKey}/conceptRecordCount", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity>>> getConceptRecordCount( + @PathVariable("sourceKey") String sourceKey, + @RequestBody List identifiers) { + List>> result = cdmResultsService.getConceptRecordCount(sourceKey, identifiers); + return ok(result); + } + + /** + * Queries for dashboard report for the sourceKey + * + * Jersey: GET /WebAPI/cdmresults/{sourceKey}/dashboard + * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/dashboard + * + * @param sourceKey The source key + * @return CDMDashboard + */ + @GetMapping(value = "/{sourceKey}/dashboard", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getDashboard(@PathVariable("sourceKey") String sourceKey) { + CDMDashboard dashboard = cdmResultsService.getDashboard(sourceKey); + return ok(dashboard); + } + + /** + * Queries for person report for the sourceKey + * + * Jersey: GET /WebAPI/cdmresults/{sourceKey}/person + * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/person + * + * @param sourceKey The source key + * @return CDMPersonSummary + */ + @GetMapping(value = "/{sourceKey}/person", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getPerson(@PathVariable("sourceKey") String sourceKey) { + CDMPersonSummary person = cdmResultsService.getPerson(sourceKey); + return ok(person); + } + + /** + * Warm the results cache for a selected source + * + * Jersey: GET /WebAPI/cdmresults/{sourceKey}/warmCache + * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/warmCache + * + * @summary Warm cache for source key + * @param sourceKey The source key + * @return The job execution information + */ + @GetMapping(value = "/{sourceKey}/warmCache", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity warmCache(@PathVariable("sourceKey") String sourceKey) { + JobExecutionResource jobExecution = cdmResultsService.warmCache(sourceKey); + return ok(jobExecution); + } + + /** + * Refresh the results cache for a selected source + * + * Jersey: GET /WebAPI/cdmresults/{sourceKey}/refreshCache + * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/refreshCache + * + * @summary Refresh results cache + * @param sourceKey The source key + * @return The job execution resource + */ + @GetMapping(value = "/{sourceKey}/refreshCache", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity refreshCache(@PathVariable("sourceKey") String sourceKey) { + JobExecutionResource jobExecution = cdmResultsService.refreshCache(sourceKey); + return ok(jobExecution); + } + + /** + * Clear the cdm_cache and achilles_cache for a specific source + * + * Jersey: POST /WebAPI/cdmresults/{sourceKey}/clearCache + * Spring MVC: POST /WebAPI/v2/cdmresults/{sourceKey}/clearCache + * + * @summary Clear the cdm_cache and achilles_cache for a source + * @param sourceKey The source key + * @return void + */ + @PostMapping(value = "/{sourceKey}/clearCache") + public ResponseEntity clearCacheForSource(@PathVariable("sourceKey") String sourceKey) { + if (!isSecured() || !isAdmin()) { + return forbidden(); + } + cdmResultsService.clearCacheForSource(sourceKey); + return ok(); + } + + /** + * Clear the cdm_cache and achilles_cache for all sources + * + * Jersey: POST /WebAPI/cdmresults/clearCache + * Spring MVC: POST /WebAPI/v2/cdmresults/clearCache + * + * @summary Clear the cdm_cache and achilles_cache for all sources + * @return void + */ + @PostMapping(value = "/clearCache") + public ResponseEntity clearCache() { + if (!isSecured() || !isAdmin()) { + return forbidden(); + } + cdmResultsService.clearCache(); + return ok(); + } + + /** + * Queries for data density report for the given sourceKey + * + * Jersey: GET /WebAPI/cdmresults/{sourceKey}/datadensity + * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/datadensity + * + * @param sourceKey The source key + * @return CDMDataDensity + */ + @GetMapping(value = "/{sourceKey}/datadensity", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getDataDensity(@PathVariable("sourceKey") String sourceKey) { + CDMDataDensity dataDensity = cdmResultsService.getDataDensity(sourceKey); + return ok(dataDensity); + } + + /** + * Queries for death report for the given sourceKey + * + * Jersey: GET /WebAPI/cdmresults/{sourceKey}/death + * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/death + * + * @param sourceKey The source key + * @return CDMDeath + */ + @GetMapping(value = "/{sourceKey}/death", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getDeath(@PathVariable("sourceKey") String sourceKey) { + CDMDeath death = cdmResultsService.getDeath(sourceKey); + return ok(death); + } + + /** + * Queries for observation period report for the given sourceKey + * + * Jersey: GET /WebAPI/cdmresults/{sourceKey}/observationPeriod + * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/observationPeriod + * + * @param sourceKey The source key + * @return CDMObservationPeriod + */ + @GetMapping(value = "/{sourceKey}/observationPeriod", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getObservationPeriod(@PathVariable("sourceKey") String sourceKey) { + CDMObservationPeriod observationPeriod = cdmResultsService.getObservationPeriod(sourceKey); + return ok(observationPeriod); + } + + /** + * Queries for domain treemap results + * + * Jersey: GET /WebAPI/cdmresults/{sourceKey}/{domain}/ + * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/{domain}/ + * + * @param sourceKey The source key + * @param domain The domain + * @return List + */ + @GetMapping(value = "/{sourceKey}/{domain}/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getTreemap( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("domain") String domain) { + ArrayNode treemap = cdmResultsService.getTreemap(domain, sourceKey); + return ok(treemap); + } + + /** + * Queries for drilldown results + * + * Jersey: GET /WebAPI/cdmresults/{sourceKey}/{domain}/{conceptId} + * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/{domain}/{conceptId} + * + * @param sourceKey The source key + * @param domain The domain for the drilldown + * @param conceptId The concept ID + * @return The JSON results + */ + @GetMapping(value = "/{sourceKey}/{domain}/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getDrilldown( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("domain") String domain, + @PathVariable("conceptId") int conceptId) { + JsonNode drilldown = cdmResultsService.getDrilldown(domain, conceptId, sourceKey); + return ok(drilldown); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java new file mode 100644 index 0000000000..f885bcb719 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java @@ -0,0 +1,94 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.ohdsi.webapi.cache.CacheInfo; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.util.CacheHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.StreamSupport; + +/** + * Spring MVC version of CacheService + * + * Migration Status: Replaces /cache/CacheService.java (Jersey) + * Endpoints: 2 GET endpoints + * Complexity: Simple - basic cache operations + */ +@RestController +@RequestMapping("/cache") +public class CacheMvcController extends AbstractMvcController { + + public static class ClearCacheResult { + public List clearedCaches; + + private ClearCacheResult() { + this.clearedCaches = new ArrayList<>(); + } + } + + private final CacheManager cacheManager; + + @Autowired(required = false) + public CacheMvcController(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * Get list of all caches with statistics + * + * Jersey: GET /WebAPI/cache/ + * Spring MVC: GET /WebAPI/v2/cache/ + */ + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCacheInfoList() { + List caches = new ArrayList<>(); + + if (cacheManager == null) { + return ok(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 ok(caches); + } + + /** + * Clear all caches + * + * Jersey: GET /WebAPI/cache/clear + * Spring MVC: GET /WebAPI/v2/cache/clear + */ + @GetMapping(value = "/clear", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity clearAll() { + ClearCacheResult result = new ClearCacheResult(); + + if (cacheManager == null) { + return ok(result); + } + + 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 ok(result); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortAnalysisMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortAnalysisMvcController.java new file mode 100644 index 0000000000..0aad050678 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortAnalysisMvcController.java @@ -0,0 +1,127 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.ohdsi.webapi.cohortanalysis.*; +import org.ohdsi.webapi.job.JobExecutionResource; +import org.ohdsi.webapi.model.results.Analysis; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * Spring MVC version of CohortAnalysisService + * + * Migration Status: Replaces /service/CohortAnalysisService.java (Jersey) + * Endpoints: 5 endpoints (3 GET, 2 POST) + * Complexity: Medium - business logic delegated to original service + */ +@RestController +@RequestMapping("/cohortanalysis") +public class CohortAnalysisMvcController extends AbstractMvcController { + + private final org.ohdsi.webapi.service.CohortAnalysisService cohortAnalysisService; + + public CohortAnalysisMvcController(org.ohdsi.webapi.service.CohortAnalysisService cohortAnalysisService) { + this.cohortAnalysisService = cohortAnalysisService; + } + + /** + * Returns all cohort analyses in the WebAPI database + * + * Jersey: GET /WebAPI/cohortanalysis/ + * Spring MVC: GET /WebAPI/v2/cohortanalysis + * + * @summary Get all cohort analyses + * @return List of all cohort analyses + */ + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCohortAnalyses() { + List analyses = cohortAnalysisService.getCohortAnalyses(); + return ok(analyses); + } + + /** + * Returns all cohort analyses in the WebAPI database + * for the given cohort_definition_id + * + * Jersey: GET /WebAPI/cohortanalysis/{id} + * Spring MVC: GET /WebAPI/v2/cohortanalysis/{id} + * + * @summary Get cohort analyses by cohort ID + * @param id The cohort definition identifier + * @return List of all cohort analyses and their statuses + * for the given cohort_definition_id + */ + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCohortAnalysesForCohortDefinition(@PathVariable("id") int id) { + List analyses = cohortAnalysisService.getCohortAnalysesForCohortDefinition(id); + return ok(analyses); + } + + /** + * Returns the summary for the cohort + * + * Jersey: GET /WebAPI/cohortanalysis/{id}/summary + * Spring MVC: GET /WebAPI/v2/cohortanalysis/{id}/summary + * + * @summary Cohort analysis summary + * @param id - the cohort_definition id + * @return Summary which includes the base cohort_definition, the cohort analyses list and their + * statuses for this cohort, and a base set of common cohort results that may or may not + * yet have been ran + */ + @GetMapping(value = "/{id}/summary", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortSummary(@PathVariable("id") int id) { + CohortSummary summary = cohortAnalysisService.getCohortSummary(id); + return ok(summary); + } + + /** + * Generates a preview of the cohort analysis SQL used to run + * the Cohort Analysis Job + * + * Jersey: POST /WebAPI/cohortanalysis/preview + * Spring MVC: POST /WebAPI/v2/cohortanalysis/preview + * + * @summary Cohort analysis SQL preview + * @param task - the CohortAnalysisTask, be sure to have a least one + * analysis_id and one cohort_definition id + * @return - SQL for the given CohortAnalysisTask translated and rendered to + * the current dialect + */ + @PostMapping( + value = "/preview", + produces = MediaType.TEXT_PLAIN_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity getRunCohortAnalysisSql(@RequestBody CohortAnalysisTask task) { + String sql = cohortAnalysisService.getRunCohortAnalysisSql(task); + return ok(sql); + } + + /** + * Queues up a cohort analysis task, that generates and translates SQL for the + * given cohort definitions, analysis ids and concept ids + * + * Jersey: POST /WebAPI/cohortanalysis/ + * Spring MVC: POST /WebAPI/v2/cohortanalysis + * + * @summary Queue cohort analysis job + * @param task The cohort analysis task to be ran + * @return information about the Cohort Analysis Job + * @throws Exception + */ + @PostMapping( + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity queueCohortAnalysisJob(@RequestBody CohortAnalysisTask task) throws Exception { + JobExecutionResource jobExecution = cohortAnalysisService.queueCohortAnalysisJob(task); + if (jobExecution == null) { + return notFound(); + } + return ok(jobExecution); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortDefinitionMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortDefinitionMvcController.java new file mode 100644 index 0000000000..7078a5def7 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortDefinitionMvcController.java @@ -0,0 +1,1134 @@ +package org.ohdsi.webapi.mvc.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.commonmark.Extension; +import org.commonmark.ext.gfm.tables.TablesExtension; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.hibernate.Hibernate; +import org.ohdsi.analysis.Utils; +import org.ohdsi.circe.check.Checker; +import org.ohdsi.circe.cohortdefinition.CohortExpression; +import org.ohdsi.circe.cohortdefinition.CohortExpressionQueryBuilder; +import org.ohdsi.circe.cohortdefinition.ConceptSet; +import org.ohdsi.circe.cohortdefinition.printfriendly.MarkdownRender; +import org.ohdsi.sql.SqlRender; +import org.ohdsi.webapi.Constants; +import org.ohdsi.webapi.check.CheckResult; +import org.ohdsi.webapi.check.checker.cohort.CohortChecker; +import org.ohdsi.webapi.check.warning.Warning; +import org.ohdsi.webapi.check.warning.WarningUtils; +import org.ohdsi.webapi.cohortdefinition.*; +import org.ohdsi.webapi.cohortdefinition.dto.*; +import org.ohdsi.webapi.cohortdefinition.event.CohortDefinitionChangedEvent; +import org.ohdsi.webapi.common.SourceMapKey; +import org.ohdsi.webapi.common.generation.GenerateSqlResult; +import org.ohdsi.webapi.common.sensitiveinfo.CohortGenerationSensitiveInfoService; +import org.ohdsi.webapi.conceptset.ConceptSetExport; +import org.ohdsi.webapi.job.JobExecutionResource; +import org.ohdsi.webapi.job.JobTemplate; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.security.PermissionService; +import org.ohdsi.webapi.service.*; +import org.ohdsi.webapi.service.dto.CheckResultDTO; +import org.ohdsi.webapi.shiro.Entities.UserEntity; +import org.ohdsi.webapi.shiro.Entities.UserRepository; +import org.ohdsi.webapi.shiro.management.datasource.SourceIdAccessor; +import org.ohdsi.webapi.source.Source; +import org.ohdsi.webapi.source.SourceDaimon; +import org.ohdsi.webapi.source.SourceInfo; +import org.ohdsi.webapi.source.SourceRepository; +import org.ohdsi.webapi.source.SourceService; +import org.ohdsi.webapi.tag.TagService; +import org.ohdsi.webapi.tag.dto.TagNameListRequestDTO; +import org.ohdsi.webapi.util.*; +import org.ohdsi.webapi.util.CancelableJdbcTemplate; +import org.ohdsi.webapi.versioning.domain.CohortVersion; +import org.ohdsi.webapi.versioning.domain.Version; +import org.ohdsi.webapi.versioning.domain.VersionBase; +import org.ohdsi.webapi.versioning.domain.VersionType; +import org.ohdsi.webapi.versioning.dto.VersionDTO; +import org.ohdsi.webapi.versioning.dto.VersionUpdateDTO; +import org.ohdsi.webapi.versioning.service.VersionService; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.job.builder.SimpleJobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.web.bind.annotation.*; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.io.ByteArrayOutputStream; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.ohdsi.webapi.Constants.Params.COHORT_DEFINITION_ID; +import static org.ohdsi.webapi.Constants.Params.JOB_NAME; +import static org.ohdsi.webapi.Constants.Params.SOURCE_ID; +import static org.ohdsi.webapi.util.SecurityUtils.whitelist; + +/** + * Spring MVC version of CohortDefinitionService + * + * Migration Status: Replaces /service/CohortDefinitionService.java (Jersey) + * Endpoints: 25+ endpoints for cohort definition management + * Complexity: High - comprehensive CRUD, versioning, generation, tags, validation + */ +@RestController +@RequestMapping("/cohortdefinition") +public class CohortDefinitionMvcController extends AbstractMvcController { + + private static final CohortExpressionQueryBuilder queryBuilder = new CohortExpressionQueryBuilder(); + + @Autowired + private CohortDefinitionRepository cohortDefinitionRepository; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private PlatformTransactionManager transactionManager; + + private TransactionTemplate transactionTemplate; + + @Autowired + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + this.transactionTemplate = new TransactionTemplate(transactionManager); + } + + @Autowired + private TransactionTemplate transactionTemplateRequiresNew; + + @Autowired + private TransactionTemplate transactionTemplateNoTransaction; + + @Autowired + private JobTemplate jobTemplate; + + @Autowired + private CohortGenerationService cohortGenerationService; + + @Autowired + private JobService jobService; + + @Autowired + private CohortGenerationSensitiveInfoService sensitiveInfoService; + + @Autowired + private SourceIdAccessor sourceIdAccessor; + + @Autowired + private ConversionService conversionService; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ApplicationEventPublisher eventPublisher; + + @Autowired + private SourceService sourceService; + + @Autowired + private VocabularyService vocabularyService; + + @Autowired + private PermissionService permissionService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private SourceRepository sourceRepository; + + @PersistenceContext + protected EntityManager entityManager; + + @Autowired + private CohortChecker cohortChecker; + + @Autowired + private VersionService versionService; + + @Value("${security.defaultGlobalReadPermissions}") + private boolean defaultGlobalReadPermissions; + + private final MarkdownRender markdownPF = new MarkdownRender(); + private final List extensions = Arrays.asList(TablesExtension.create()); + + private final RowMapper summaryMapper = (rs, rowNum) -> { + InclusionRuleReport.Summary summary = new InclusionRuleReport.Summary(); + summary.baseCount = rs.getLong("base_count"); + summary.finalCount = rs.getLong("final_count"); + summary.lostCount = rs.getLong("lost_count"); + + double matchRatio = (summary.baseCount > 0) ? ((double) summary.finalCount / (double) summary.baseCount) : 0.0; + summary.percentMatched = new BigDecimal(matchRatio * 100.0).setScale(2, RoundingMode.HALF_UP).toPlainString() + "%"; + return summary; + }; + + private final RowMapper inclusionRuleStatisticMapper = (rs, rowNum) -> { + InclusionRuleReport.InclusionRuleStatistic statistic = new InclusionRuleReport.InclusionRuleStatistic(); + statistic.id = rs.getInt("rule_sequence"); + statistic.name = rs.getString("name"); + statistic.countSatisfying = rs.getLong("person_count"); + long personTotal = rs.getLong("person_total"); + + long gainCount = rs.getLong("gain_count"); + double excludeRatio = personTotal > 0 ? (double) gainCount / (double) personTotal : 0.0; + String percentExcluded = new BigDecimal(excludeRatio * 100.0).setScale(2, RoundingMode.HALF_UP).toPlainString(); + statistic.percentExcluded = percentExcluded + "%"; + + long satisfyCount = rs.getLong("person_count"); + double satisfyRatio = personTotal > 0 ? (double) satisfyCount / (double) personTotal : 0.0; + String percentSatisfying = new BigDecimal(satisfyRatio * 100.0).setScale(2, RoundingMode.HALF_UP).toPlainString(); + statistic.percentSatisfying = percentSatisfying + "%"; + return statistic; + }; + + private final RowMapper inclusionRuleResultItemMapper = (rs, rowNum) -> { + Long[] resultItem = new Long[2]; + resultItem[0] = rs.getLong("inclusion_rule_mask"); + resultItem[1] = rs.getLong("person_count"); + return resultItem; + }; + + public static class GenerateSqlRequest { + @JsonProperty("expression") + public CohortExpression expression; + + @JsonProperty("options") + public CohortExpressionQueryBuilder.BuildExpressionQueryOptions options; + } + + /** + * Returns OHDSI template SQL for a given cohort definition + * + * Jersey: POST /WebAPI/cohortdefinition/sql + * Spring MVC: POST /WebAPI/v2/cohortdefinition/sql + * + * @summary Generate Sql + */ + @PostMapping(value = "/sql", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity generateSql(@RequestBody GenerateSqlRequest request) { + CohortExpressionQueryBuilder.BuildExpressionQueryOptions options = request.options; + GenerateSqlResult result = new GenerateSqlResult(); + if (options == null) { + options = new CohortExpressionQueryBuilder.BuildExpressionQueryOptions(); + } + String expressionSql = queryBuilder.buildExpressionQuery(request.expression, options); + result.templateSql = SqlRender.renderSql(expressionSql, null, null); + + return ok(result); + } + + /** + * Returns metadata about all cohort definitions in the WebAPI database + * + * Jersey: GET /WebAPI/cohortdefinition/ + * Spring MVC: GET /WebAPI/v2/cohortdefinition/ + * + * @summary List Cohort Definitions + */ + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + @Cacheable(cacheNames = "cohortDefinitionList", key = "@permissionService.getSubjectCacheKey()") + public ResponseEntity> getCohortDefinitionList() { + List definitions = cohortDefinitionRepository.list(); + List result = definitions.stream() + .filter(!defaultGlobalReadPermissions ? entity -> permissionService.hasReadAccess(entity) : entity -> true) + .map(def -> { + CohortMetadataDTO dto = conversionService.convert(def, CohortMetadataImplDTO.class); + permissionService.fillWriteAccess(def, dto); + permissionService.fillReadAccess(def, dto); + return dto; + }) + .collect(Collectors.toList()); + return ok(result); + } + + /** + * Creates a cohort definition in the WebAPI database + * + * Jersey: POST /WebAPI/cohortdefinition/ + * Spring MVC: POST /WebAPI/v2/cohortdefinition/ + * + * @summary Create Cohort Definition + */ + @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Transactional + @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) + public ResponseEntity createCohortDefinition(@RequestBody CohortDTO dto) { + Date currentTime = Calendar.getInstance().getTime(); + + UserEntity user = userRepository.findByLogin(security.getSubject()); + CohortDefinition newDef = new CohortDefinition(); + newDef.setName(StringUtils.trim(dto.getName())) + .setDescription(dto.getDescription()) + .setExpressionType(dto.getExpressionType()); + newDef.setCreatedBy(user); + newDef.setCreatedDate(currentTime); + + newDef = this.cohortDefinitionRepository.save(newDef); + + CohortDefinitionDetails details = new CohortDefinitionDetails(); + details.setCohortDefinition(newDef) + .setExpression(Utils.serialize(dto.getExpression())); + + newDef.setDetails(details); + + CohortDefinition createdDefinition = this.cohortDefinitionRepository.save(newDef); + return ok(conversionService.convert(createdDefinition, CohortDTO.class)); + } + + /** + * Returns the 'raw' cohort definition for the given id + * + * Jersey: GET /WebAPI/cohortdefinition/{id} + * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id} + * + * @summary Get Raw Cohort Definition + */ + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortDefinitionRaw(@PathVariable("id") final int id) { + CohortRawDTO result = transactionTemplate.execute(transactionStatus -> { + CohortDefinition d = this.cohortDefinitionRepository.findOneWithDetail(id); + ExceptionUtils.throwNotFoundExceptionIfNull(d, String.format("There is no cohort definition with id = %d.", id)); + return conversionService.convert(d, CohortRawDTO.class); + }); + return ok(result); + } + + /** + * Check that a cohort exists + * + * Jersey: GET /WebAPI/cohortdefinition/{id}/exists + * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/exists + * + * @summary Check Cohort Definition Name + */ + @GetMapping(value = "/{id}/exists", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCountCDefWithSameName( + @PathVariable("id") final int id, + @RequestParam(value = "name", required = false) String name) { + return ok(cohortDefinitionRepository.getCountCDefWithSameName(id, name)); + } + + /** + * Saves the cohort definition for the given id + * + * Jersey: PUT /WebAPI/cohortdefinition/{id} + * Spring MVC: PUT /WebAPI/v2/cohortdefinition/{id} + * + * @summary Save Cohort Definition + */ + @PutMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Transactional + @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) + public ResponseEntity saveCohortDefinition(@PathVariable("id") final int id, @RequestBody CohortDTO def) { + Date currentTime = Calendar.getInstance().getTime(); + + saveVersion(id); + + CohortDefinition currentDefinition = this.cohortDefinitionRepository.findOneWithDetail(id); + UserEntity modifier = userRepository.findByLogin(security.getSubject()); + + currentDefinition.setName(def.getName()) + .setDescription(def.getDescription()) + .setExpressionType(def.getExpressionType()) + .getDetails().setExpression(Utils.serialize(def.getExpression())); + currentDefinition.setModifiedBy(modifier); + currentDefinition.setModifiedDate(currentTime); + + currentDefinition = this.cohortDefinitionRepository.save(currentDefinition); + eventPublisher.publishEvent(new CohortDefinitionChangedEvent(currentDefinition)); + + CohortDTO result = getCohortDefinition(id); + return ok(result); + } + + /** + * Queues up a generate cohort task for the specified cohort definition id + * + * Jersey: GET /WebAPI/cohortdefinition/{id}/generate/{sourceKey} + * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/generate/{sourceKey} + * + * @summary Generate Cohort + */ + @GetMapping(value = "/{id}/generate/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity generateCohort( + @PathVariable("id") final int id, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "demographic", defaultValue = "false") boolean demographicStat) { + + Source source = transactionTemplate.execute(status -> { + Source s = sourceRepository.findBySourceKey(sourceKey); + if (s != null) { + Hibernate.initialize(s); + } + return s; + }); + + CohortDefinition currentDefinition = transactionTemplate.execute(status -> { + CohortDefinition cd = this.cohortDefinitionRepository.findOneWithDetail(id); + if (cd != null) { + if (cd.getDetails() != null) { + cd.getDetails().getExpression(); + } + Hibernate.initialize(cd.getGenerationInfoList()); + } + return cd; + }); + + UserEntity user = transactionTemplate.execute(status -> { + UserEntity u = userRepository.findByLogin(security.getSubject()); + if (u != null) { + Hibernate.initialize(u); + } + return u; + }); + + JobExecutionResource result = cohortGenerationService.generateCohortViaJob(user, currentDefinition, source, demographicStat); + return ok(result); + } + + /** + * Cancel a cohort generation task + * + * Jersey: GET /WebAPI/cohortdefinition/{id}/cancel/{sourceKey} + * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/cancel/{sourceKey} + * + * @summary Cancel Cohort Generation + */ + @GetMapping(value = "/{id}/cancel/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity cancelGenerateCohort( + @PathVariable("id") final int id, + @PathVariable("sourceKey") final String sourceKey) { + + final Source source = Optional.ofNullable(sourceRepository.findBySourceKey(sourceKey)) + .orElseThrow(() -> new RuntimeException("Source not found")); + + transactionTemplateRequiresNew.execute(status -> { + CohortDefinition currentDefinition = cohortDefinitionRepository.findById(id).orElse(null); + if (Objects.nonNull(currentDefinition)) { + CohortGenerationInfo info = findBySourceId(currentDefinition.getGenerationInfoList(), source.getSourceId()); + if (Objects.nonNull(info)) { + invalidateExecution(info); + cohortDefinitionRepository.save(currentDefinition); + } + } + return null; + }); + + jobService.cancelJobExecution(e -> { + JobParameters parameters = e.getJobParameters(); + String jobName = e.getJobInstance().getJobName(); + return Objects.equals(parameters.getString(COHORT_DEFINITION_ID), Integer.toString(id)) + && Objects.equals(parameters.getString(SOURCE_ID), Integer.toString(source.getSourceId())) + && Objects.equals(Constants.GENERATE_COHORT, jobName); + }); + + return ok(); + } + + /** + * Returns a list of cohort generation info objects + * + * Jersey: GET /WebAPI/cohortdefinition/{id}/info + * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/info + * + * @summary Get cohort generation info + */ + @GetMapping(value = "/{id}/info", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity> getInfo(@PathVariable("id") final int id) { + CohortDefinition def = this.cohortDefinitionRepository.findById(id).orElse(null); + ExceptionUtils.throwNotFoundExceptionIfNull(def, String.format("There is no cohort definition with id = %d.", id)); + + Set infoList = def.getGenerationInfoList(); + List result = infoList.stream() + .filter(genInfo -> sourceIdAccessor.hasAccess(genInfo.getId().getSourceId())) + .collect(Collectors.toList()); + + Map sourceMap = sourceService.getSourcesMap(SourceMapKey.BY_SOURCE_ID); + List filteredResult = sensitiveInfoService.filterSensitiveInfo(result, + gi -> Collections.singletonMap(Constants.Variables.SOURCE, sourceMap.get(gi.getId().getSourceId()))); + + List dtos = filteredResult.stream() + .map(t -> conversionService.convert(t, CohortGenerationInfoDTO.class)) + .collect(Collectors.toList()); + + return ok(dtos); + } + + /** + * Copies the specified cohort definition + * + * Jersey: GET /WebAPI/cohortdefinition/{id}/copy + * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/copy + * + * @summary Copy Cohort Definition + */ + @GetMapping(value = "/{id}/copy", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) + public ResponseEntity copy(@PathVariable("id") final int id) { + CohortDTO sourceDef = getCohortDefinition(id); + sourceDef.setId(null); + sourceDef.setTags(null); + sourceDef.setName(NameUtils.getNameForCopy(sourceDef.getName(), this::getNamesLike, + cohortDefinitionRepository.findByName(sourceDef.getName()))); + + CohortDTO copyDef = createCohortDefinition(sourceDef).getBody(); + return ok(copyDef); + } + + /** + * Deletes the specified cohort definition + * + * Jersey: DELETE /WebAPI/cohortdefinition/{id} + * Spring MVC: DELETE /WebAPI/v2/cohortdefinition/{id} + * + * @summary Delete Cohort Definition + */ + @DeleteMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) + public ResponseEntity delete(@PathVariable("id") final int id) { + transactionTemplateRequiresNew.execute(new TransactionCallbackWithoutResult() { + @Override + public void doInTransactionWithoutResult(final TransactionStatus status) { + CohortDefinition def = cohortDefinitionRepository.findById(id).orElse(null); + if (!Objects.isNull(def)) { + def.getGenerationInfoList().forEach(cohortGenerationInfo -> { + Integer sourceId = cohortGenerationInfo.getId().getSourceId(); + + jobService.cancelJobExecution(e -> { + JobParameters parameters = e.getJobParameters(); + String jobName = e.getJobInstance().getJobName(); + return Objects.equals(parameters.getString(COHORT_DEFINITION_ID), Integer.toString(id)) + && Objects.equals(parameters.getString(SOURCE_ID), Integer.toString(sourceId)) + && Objects.equals(Constants.GENERATE_COHORT, jobName); + }); + }); + cohortDefinitionRepository.delete(def); + } + } + }); + + JobParametersBuilder builder = new JobParametersBuilder(); + builder.addString(JOB_NAME, String.format("Cleanup cohort %d.", id)); + builder.addString(COHORT_DEFINITION_ID, ("" + id)); + + final JobParameters jobParameters = builder.toJobParameters(); + + CleanupCohortTasklet cleanupTasklet = new CleanupCohortTasklet(transactionTemplateNoTransaction, sourceRepository); + + Step cleanupStep = new StepBuilder("cohortDefinition.cleanupCohort", jobRepository) + .tasklet(cleanupTasklet, transactionManager) + .build(); + + SimpleJobBuilder cleanupJobBuilder = new JobBuilder("cleanupCohort", jobRepository) + .start(cleanupStep); + + Job cleanupCohortJob = cleanupJobBuilder.build(); + + jobTemplate.launch(cleanupCohortJob, jobParameters); + + return ok(); + } + + /** + * Return concept sets used in a cohort definition as a zip file + * + * Jersey: GET /WebAPI/cohortdefinition/{id}/export/conceptset + * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/export/conceptset + * + * @summary Export Concept Sets as ZIP + */ + @GetMapping(value = "/{id}/export/conceptset", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity exportConceptSets(@PathVariable("id") final int id) { + Source source = sourceService.getPriorityVocabularySource(); + if (Objects.isNull(source)) { + return forbidden(); + } + + CohortDefinition def = this.cohortDefinitionRepository.findOneWithDetail(id); + if (Objects.isNull(def)) { + return notFound(); + } + + List exports = getConceptSetExports(def, new SourceInfo(source)); + ByteArrayOutputStream exportStream = ExportUtil.writeConceptSetExportToCSVAndZip(exports); + + ByteArrayResource resource = new ByteArrayResource(exportStream.toByteArray()); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"cohortdefinition_" + def.getId() + "_export.zip\"") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(resource); + } + + /** + * Get the Inclusion Rule report for the specified source and mode + * + * Jersey: GET /WebAPI/cohortdefinition/{id}/report/{sourceKey} + * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/report/{sourceKey} + * + * @summary Get Inclusion Rule Report + */ + @GetMapping(value = "/{id}/report/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity getInclusionRuleReport( + @PathVariable("id") final int id, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "mode", defaultValue = "0") int modeId, + @RequestParam(value = "ccGenerateId", required = false) String ccGenerateId) { + + Source source = this.sourceRepository.findBySourceKey(sourceKey); + + InclusionRuleReport.Summary summary = getInclusionRuleReportSummary(whitelist(id), source, modeId); + List inclusionRuleStats = getInclusionRuleStatistics(whitelist(id), source, modeId); + String treemapData = getInclusionRuleTreemapData(whitelist(id), inclusionRuleStats.size(), source, modeId); + + InclusionRuleReport report = new InclusionRuleReport(); + report.summary = summary; + report.inclusionRuleStats = inclusionRuleStats; + report.treemapData = treemapData; + + return ok(report); + } + + /** + * Checks the cohort definition for logic issues + * + * Jersey: POST /WebAPI/cohortdefinition/check + * Spring MVC: POST /WebAPI/v2/cohortdefinition/check + * + * @summary Check Cohort Definition + */ + @PostMapping(value = "/check", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity runDiagnostics(@RequestBody CohortExpression expression) { + Checker checker = new Checker(); + return ok(new CheckResultDTO(checker.check(expression))); + } + + /** + * Checks the cohort definition for logic issues (V2 with tags) + * + * Jersey: POST /WebAPI/cohortdefinition/checkV2 + * Spring MVC: POST /WebAPI/v2/cohortdefinition/checkV2 + * + * @summary Check Cohort Definition V2 + */ + @PostMapping(value = "/checkV2", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity runDiagnosticsWithTags(@RequestBody CohortDTO cohortDTO) { + Checker checker = new Checker(); + CheckResultDTO checkResultDTO = new CheckResultDTO(checker.check(cohortDTO.getExpression())); + List circeWarnings = checkResultDTO.getWarnings().stream() + .map(WarningUtils::convertCirceWarning) + .collect(Collectors.toList()); + CheckResult checkResult = new CheckResult(cohortChecker.check(cohortDTO)); + checkResult.getWarnings().addAll(circeWarnings); + return ok(checkResult); + } + + /** + * Render a cohort expression in html or markdown form + * + * Jersey: POST /WebAPI/cohortdefinition/printfriendly/cohort + * Spring MVC: POST /WebAPI/v2/cohortdefinition/printfriendly/cohort + * + * @summary Cohort Print Friendly + */ + @PostMapping(value = "/printfriendly/cohort", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity cohortPrintFriendly( + @RequestBody CohortExpression expression, + @RequestParam(value = "format", defaultValue = "html") String format) { + String markdown = convertCohortExpressionToMarkdown(expression); + return printFriendly(markdown, format); + } + + /** + * Render a list of concept sets in html or markdown form + * + * Jersey: POST /WebAPI/cohortdefinition/printfriendly/conceptsets + * Spring MVC: POST /WebAPI/v2/cohortdefinition/printfriendly/conceptsets + * + * @summary Concept Set Print Friendly + */ + @PostMapping(value = "/printfriendly/conceptsets", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity conceptSetListPrintFriendly( + @RequestBody List conceptSetList, + @RequestParam(value = "format", defaultValue = "html") String format) { + String markdown = markdownPF.renderConceptSetList(conceptSetList.toArray(new ConceptSet[0])); + return printFriendly(markdown, format); + } + + /** + * Assign tag to Cohort Definition + * + * Jersey: POST /WebAPI/cohortdefinition/{id}/tag/ + * Spring MVC: POST /WebAPI/v2/cohortdefinition/{id}/tag/ + * + * @summary Assign Tag + */ + @PostMapping(value = "/{id}/tag", produces = MediaType.APPLICATION_JSON_VALUE) + @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) + @Transactional + public ResponseEntity assignTag(@PathVariable("id") final Integer id, @RequestBody int tagId) { + CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); + assignTag(entity, tagId); + return ok(); + } + + /** + * Unassign tag from Cohort Definition + * + * Jersey: DELETE /WebAPI/cohortdefinition/{id}/tag/{tagId} + * Spring MVC: DELETE /WebAPI/v2/cohortdefinition/{id}/tag/{tagId} + * + * @summary Unassign Tag + */ + @DeleteMapping(value = "/{id}/tag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) + @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) + @Transactional + public ResponseEntity unassignTag(@PathVariable("id") final Integer id, @PathVariable("tagId") final int tagId) { + CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); + unassignTag(entity, tagId); + return ok(); + } + + /** + * Assign protected tag to Cohort Definition + * + * Jersey: POST /WebAPI/cohortdefinition/{id}/protectedtag/ + * Spring MVC: POST /WebAPI/v2/cohortdefinition/{id}/protectedtag/ + * + * @summary Assign Protected Tag + */ + @PostMapping(value = "/{id}/protectedtag", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity assignPermissionProtectedTag(@PathVariable("id") final int id, @RequestBody int tagId) { + return assignTag(id, tagId); + } + + /** + * Unassign protected tag from Cohort Definition + * + * Jersey: DELETE /WebAPI/cohortdefinition/{id}/protectedtag/{tagId} + * Spring MVC: DELETE /WebAPI/v2/cohortdefinition/{id}/protectedtag/{tagId} + * + * @summary Unassign Protected Tag + */ + @DeleteMapping(value = "/{id}/protectedtag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity unassignPermissionProtectedTag(@PathVariable("id") final int id, @PathVariable("tagId") final int tagId) { + return unassignTag(id, tagId); + } + + /** + * Get list of versions of Cohort Definition + * + * Jersey: GET /WebAPI/cohortdefinition/{id}/version/ + * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/version/ + * + * @summary Get Cohort Definition Versions + */ + @GetMapping(value = "/{id}/version", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity> getVersions(@PathVariable("id") final long id) { + List versions = versionService.getVersions(VersionType.COHORT, id); + List dtos = versions.stream() + .map(v -> conversionService.convert(v, VersionDTO.class)) + .collect(Collectors.toList()); + return ok(dtos); + } + + /** + * Get version of Cohort Definition + * + * Jersey: GET /WebAPI/cohortdefinition/{id}/version/{version} + * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/version/{version} + * + * @summary Get Cohort Definition Version + */ + @GetMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity getVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version) { + checkVersion(id, version, false); + CohortVersion cohortVersion = versionService.getById(VersionType.COHORT, id, version); + return ok(conversionService.convert(cohortVersion, CohortVersionFullDTO.class)); + } + + /** + * Updates version of Cohort Definition + * + * Jersey: PUT /WebAPI/cohortdefinition/{id}/version/{version} + * Spring MVC: PUT /WebAPI/v2/cohortdefinition/{id}/version/{version} + * + * @summary Update Version + */ + @PutMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity updateVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version, + @RequestBody VersionUpdateDTO updateDTO) { + checkVersion(id, version); + updateDTO.setAssetId(id); + updateDTO.setVersion(version); + CohortVersion updated = versionService.update(VersionType.COHORT, updateDTO); + return ok(conversionService.convert(updated, VersionDTO.class)); + } + + /** + * Delete version of Cohort Definition + * + * Jersey: DELETE /WebAPI/cohortdefinition/{id}/version/{version} + * Spring MVC: DELETE /WebAPI/v2/cohortdefinition/{id}/version/{version} + * + * @summary Delete Cohort Definition Version + */ + @DeleteMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity deleteVersion(@PathVariable("id") final int id, @PathVariable("version") final int version) { + checkVersion(id, version); + versionService.delete(VersionType.COHORT, id, version); + return ok(); + } + + /** + * Create a new asset from version of Cohort Definition + * + * Jersey: PUT /WebAPI/cohortdefinition/{id}/version/{version}/createAsset + * Spring MVC: PUT /WebAPI/v2/cohortdefinition/{id}/version/{version}/createAsset + * + * @summary Create Cohort from Version + */ + @PutMapping(value = "/{id}/version/{version}/createAsset", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) + public ResponseEntity copyAssetFromVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version) { + checkVersion(id, version, false); + CohortVersion cohortVersion = versionService.getById(VersionType.COHORT, id, version); + CohortVersionFullDTO fullDTO = conversionService.convert(cohortVersion, CohortVersionFullDTO.class); + CohortDTO dto = conversionService.convert(fullDTO.getEntityDTO(), CohortDTO.class); + dto.setId(null); + dto.setTags(null); + dto.setName(NameUtils.getNameForCopy(dto.getName(), this::getNamesLike, + cohortDefinitionRepository.findByName(dto.getName()))); + CohortDTO created = createCohortDefinition(dto).getBody(); + return ok(created); + } + + /** + * Get list of cohort definitions with assigned tags + * + * Jersey: POST /WebAPI/cohortdefinition/byTags + * Spring MVC: POST /WebAPI/v2/cohortdefinition/byTags + * + * @summary List Cohorts By Tag + */ + @PostMapping(value = "/byTags", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Transactional + public ResponseEntity> listByTags(@RequestBody TagNameListRequestDTO requestDTO) { + if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { + return ok(Collections.emptyList()); + } + List names = requestDTO.getNames().stream() + .map(name -> name.toLowerCase(Locale.ROOT)) + .collect(Collectors.toList()); + List entities = cohortDefinitionRepository.findByTags(names); + List result = listByTags(entities, names, CohortDTO.class); + return ok(result); + } + + // Helper methods + + private CohortGenerationInfo findBySourceId(Set infoList, Integer sourceId) { + for (CohortGenerationInfo info : infoList) { + if (info.getId().getSourceId().equals(sourceId)) { + return info; + } + } + return null; + } + + private InclusionRuleReport.Summary getInclusionRuleReportSummary(int id, Source source, int modeId) { + String sql = "select cs.base_count, cs.final_count, cc.lost_count from @tableQualifier.cohort_summary_stats cs left join @tableQualifier.cohort_censor_stats cc " + + "on cc.cohort_definition_id = cs.cohort_definition_id where cs.cohort_definition_id = @id and cs.mode_id = @modeId"; + String tqName = "tableQualifier"; + String tqValue = source.getTableQualifier(SourceDaimon.DaimonType.Results); + String[] varNames = {"id", "modeId"}; + Object[] varValues = {whitelist(id), whitelist(modeId)}; + PreparedStatementRenderer psr = new PreparedStatementRenderer(source, sql, tqName, tqValue, varNames, varValues, SessionUtils.sessionId()); + List result = getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), summaryMapper); + return result.isEmpty() ? new InclusionRuleReport.Summary() : result.get(0); + } + + private List getInclusionRuleStatistics(int id, Source source, int modeId) { + String sql = "select i.rule_sequence, i.name, s.person_count, s.gain_count, s.person_total" + + " from @tableQualifier.cohort_inclusion i join @tableQualifier.cohort_inclusion_stats s on i.cohort_definition_id = s.cohort_definition_id" + + " and i.rule_sequence = s.rule_sequence" + + " where i.cohort_definition_id = @id and mode_id = @modeId ORDER BY i.rule_sequence"; + String tqName = "tableQualifier"; + String tqValue = source.getTableQualifier(SourceDaimon.DaimonType.Results); + String[] varNames = {"id", "modeId"}; + Object[] varValues = {whitelist(id), whitelist(modeId)}; + PreparedStatementRenderer psr = new PreparedStatementRenderer(source, sql, tqName, tqValue, varNames, varValues, SessionUtils.sessionId()); + return getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), inclusionRuleStatisticMapper); + } + + private int countSetBits(long n) { + int count = 0; + while (n > 0) { + n &= (n - 1); + count++; + } + return count; + } + + private String formatBitMask(Long n, int size) { + return StringUtils.reverse(StringUtils.leftPad(Long.toBinaryString(n), size, "0")); + } + + private String getInclusionRuleTreemapData(int id, int inclusionRuleCount, Source source, int modeId) { + String sql = "select inclusion_rule_mask, person_count from @tableQualifier.cohort_inclusion_result where cohort_definition_id = @id and mode_id = @modeId"; + String tqName = "tableQualifier"; + String tqValue = source.getTableQualifier(SourceDaimon.DaimonType.Results); + String[] varNames = {"id", "modeId"}; + Object[] varValues = {whitelist(id), whitelist(modeId)}; + PreparedStatementRenderer psr = new PreparedStatementRenderer(source, sql, tqName, tqValue, varNames, varValues, SessionUtils.sessionId()); + + List items = this.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), inclusionRuleResultItemMapper); + Map> groups = new HashMap<>(); + for (Long[] item : items) { + int bitsSet = countSetBits(item[0]); + if (!groups.containsKey(bitsSet)) { + groups.put(bitsSet, new ArrayList()); + } + groups.get(bitsSet).add(item); + } + + StringBuilder treemapData = new StringBuilder("{\"name\" : \"Everyone\", \"children\" : ["); + + List groupKeys = new ArrayList<>(groups.keySet()); + Collections.sort(groupKeys); + Collections.reverse(groupKeys); + + int groupCount = 0; + for (Integer groupKey : groupKeys) { + if (groupCount > 0) { + treemapData.append(","); + } + + treemapData.append(String.format("{\"name\" : \"Group %d\", \"children\" : [", groupKey)); + + int groupItemCount = 0; + for (Long[] groupItem : groups.get(groupKey)) { + if (groupItemCount > 0) { + treemapData.append(","); + } + + treemapData.append(String.format("{\"name\": \"%s\", \"size\": %d}", formatBitMask(groupItem[0], inclusionRuleCount), groupItem[1])); + groupItemCount++; + } + groupCount++; + } + + treemapData.append(StringUtils.repeat("]}", groupCount + 1)); + + return treemapData.toString(); + } + + private List getConceptSetExports(CohortDefinition def, SourceInfo vocabSource) throws RuntimeException { + CohortExpression expression; + try { + expression = objectMapper.readValue(def.getDetails().getExpression(), CohortExpression.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return Arrays.stream(expression.conceptSets) + .map(cs -> vocabularyService.exportConceptSet(cs, vocabSource)) + .collect(Collectors.toList()); + } + + public String convertCohortExpressionToMarkdown(CohortExpression expression) { + return markdownPF.renderCohort(expression); + } + + public String convertMarkdownToHTML(String markdown) { + Parser parser = Parser.builder().extensions(extensions).build(); + Node document = parser.parse(markdown); + HtmlRenderer renderer = HtmlRenderer.builder().extensions(extensions).build(); + return renderer.render(document); + } + + private ResponseEntity printFriendly(String markdown, String format) { + if ("html".equalsIgnoreCase(format)) { + String html = convertMarkdownToHTML(markdown); + return ResponseEntity.ok() + .contentType(MediaType.TEXT_HTML) + .body(html); + } else if ("markdown".equals(format)) { + return ResponseEntity.ok() + .contentType(MediaType.TEXT_PLAIN) + .body(markdown); + } else { + return ResponseEntity.status(415).build(); // Unsupported Media Type + } + } + + private void checkVersion(int id, int version) { + checkVersion(id, version, true); + } + + private void checkVersion(int id, int version, boolean checkOwnerShip) { + Version cohortVersion = versionService.getById(VersionType.COHORT, id, version); + ExceptionUtils.throwNotFoundExceptionIfNull(cohortVersion, + String.format("There is no cohort version with id = %d.", version)); + + CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); + if (checkOwnerShip) { + checkOwnerOrAdminOrGranted(entity); + } + } + + private CohortVersion saveVersion(int id) { + CohortDefinition def = this.cohortDefinitionRepository.findOneWithDetail(id); + CohortVersion version = conversionService.convert(def, CohortVersion.class); + + UserEntity user = Objects.nonNull(def.getModifiedBy()) ? def.getModifiedBy() : def.getCreatedBy(); + Date versionDate = Objects.nonNull(def.getModifiedDate()) ? def.getModifiedDate() : def.getCreatedDate(); + version.setCreatedBy(user); + version.setCreatedDate(versionDate); + return versionService.create(VersionType.COHORT, version); + } + + public CohortDTO getCohortDefinition(final int id) { + return transactionTemplate.execute(transactionStatus -> { + CohortDefinition d = this.cohortDefinitionRepository.findOneWithDetail(id); + ExceptionUtils.throwNotFoundExceptionIfNull(d, String.format("There is no cohort definition with id = %d.", id)); + return conversionService.convert(d, CohortDTO.class); + }); + } + + public List getNamesLike(String copyName) { + return cohortDefinitionRepository.findAllByNameStartsWith(copyName).stream() + .map(CohortDefinition::getName) + .collect(Collectors.toList()); + } + + // Helper service reference for DAO operations + @Autowired + private CohortDefinitionService cohortDefinitionService; + + @Autowired + private TagService tagService; + + @Value("${jdbc.suppressInvalidApiException}") + protected boolean suppressApiException; + + // Delegate methods to existing service for complex DAO operations + private CancelableJdbcTemplate getSourceJdbcTemplate(Source source) { + // Delegate to the existing service implementation + return cohortDefinitionService.getSourceJdbcTemplate(source); + } + + private void checkOwnerOrAdminOrGranted(CohortDefinition entity) { + if (!isSecured()) { + return; + } + + UserEntity user = userRepository.findByLogin(security.getSubject()); + Long ownerId = Objects.nonNull(entity.getCreatedBy()) ? entity.getCreatedBy().getId() : null; + + if (!(user.getId().equals(ownerId) || isAdmin() || permissionService.hasWriteAccess(entity))) { + throw new RuntimeException("Forbidden"); + } + } + + private CohortGenerationInfo invalidateExecution(CohortGenerationInfo info) { + info.setIsValid(false); + info.setStatus(org.ohdsi.webapi.GenerationStatus.COMPLETE); + info.setMessage("Invalidated by system"); + return info; + } + + private List listByTags(List entities, List names, Class clazz) { + return entities.stream() + .filter(e -> e.getTags().stream() + .map(tag -> tag.getName().toLowerCase(Locale.ROOT)) + .collect(Collectors.toList()) + .containsAll(names)) + .map(entity -> { + T dto = conversionService.convert(entity, clazz); + if (dto instanceof org.ohdsi.webapi.service.dto.CommonEntityDTO) { + permissionService.fillWriteAccess(entity, (org.ohdsi.webapi.service.dto.CommonEntityDTO) dto); + } + return dto; + }) + .collect(Collectors.toList()); + } + + private void assignTag(CohortDefinition entity, int tagId) { + checkOwnerOrAdminOrGranted(entity); + if (Objects.nonNull(entity)) { + org.ohdsi.webapi.tag.domain.Tag tag = tagService.getById(tagId); + if (Objects.nonNull(tag)) { + entity.getTags().add(tag); + } + } + } + + private void unassignTag(CohortDefinition entity, int tagId) { + checkOwnerOrAdminOrGranted(entity); + if (Objects.nonNull(entity)) { + org.ohdsi.webapi.tag.domain.Tag tag = tagService.getById(tagId); + if (Objects.nonNull(tag)) { + Set tags = entity.getTags().stream() + .filter(t -> t.getId() != tagId) + .collect(Collectors.toSet()); + entity.setTags(tags); + } + } + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortMvcController.java new file mode 100644 index 0000000000..13f14d0979 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortMvcController.java @@ -0,0 +1,93 @@ +package org.ohdsi.webapi.mvc.controller; + +import java.util.List; + +import jakarta.persistence.EntityManager; + +import org.ohdsi.webapi.cohort.CohortEntity; +import org.ohdsi.webapi.cohort.CohortRepository; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Spring MVC version of CohortService + * + * Migration Status: Replaces /service/CohortService.java (Jersey) + * Endpoints: 2 endpoints (1 GET, 1 POST) + * Complexity: Simple - read and batch import operations + * + * Service to read/write to the Cohort table + */ +@RestController +@RequestMapping("/cohort") +public class CohortMvcController extends AbstractMvcController { + + @Autowired + private CohortRepository cohortRepository; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Autowired + private EntityManager em; + + /** + * Retrieves all cohort entities for the given cohort definition id + * from the COHORT table + * + * Jersey: GET /WebAPI/cohort/{id} + * Spring MVC: GET /WebAPI/v2/cohort/{id} + * + * @param id Cohort Definition id + * @return List of CohortEntity + */ + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCohortListById(@PathVariable("id") final long id) { + List cohorts = this.cohortRepository.getAllCohortsForId(id); + return ok(cohorts); + } + + /** + * Imports a List of CohortEntity into the COHORT table + * + * Jersey: POST /WebAPI/cohort/import + * Spring MVC: POST /WebAPI/v2/cohort/import + * + * @param cohort List of CohortEntity + * @return status message + */ + @PostMapping(value = "/import", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity saveCohortListToCDM(@RequestBody final List cohort) { + this.transactionTemplate.execute(new TransactionCallback() { + @Override + public Void doInTransaction(TransactionStatus status) { + int i = 0; + for (CohortEntity cohortEntity : cohort) { + em.persist(cohortEntity); + if (i % 5 == 0) { //5, same as the JDBC batch size + //flush a batch of inserts and release memory: + em.flush(); + em.clear(); + } + i++; + } + return null; + } + }); + + return ok("ok"); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java new file mode 100644 index 0000000000..c8e1b851c8 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java @@ -0,0 +1,1386 @@ +package org.ohdsi.webapi.mvc.controller; + +import static org.ohdsi.webapi.util.SecurityUtils.whitelist; + +import java.io.ByteArrayOutputStream; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.ohdsi.webapi.cohortanalysis.CohortAnalysis; +import org.ohdsi.webapi.cohortanalysis.CohortAnalysisTask; +import org.ohdsi.webapi.cohortanalysis.CohortSummary; +import org.ohdsi.webapi.cohortdefinition.CohortDefinitionRepository; +import org.ohdsi.webapi.cohortdefinition.dto.CohortDTO; +import org.ohdsi.webapi.cohortresults.*; +import org.ohdsi.webapi.model.results.AnalysisResults; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.service.CohortDefinitionService; +import org.ohdsi.webapi.service.CohortResultsService; +import org.ohdsi.webapi.source.Source; +import org.ohdsi.webapi.source.SourceDaimon; +import org.ohdsi.webapi.util.PreparedStatementRenderer; +import org.ohdsi.webapi.util.SessionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.web.bind.annotation.*; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Spring MVC version of CohortResultsService + * + * Migration Status: Replaces /service/CohortResultsService.java (Jersey) + * Endpoints: 40+ endpoints for cohort analysis results (Heracles Results) + * Complexity: High - extensive analysis reporting, caching, multiple data types + * + * REST Services related to retrieving cohort analysis (a.k.a Heracles Results) analyses results. + * More information on the Heracles project can be found at + * {@link https://www.ohdsi.org/web/wiki/doku.php?id=documentation:software:heracles}. + * The implementation found in WebAPI represents a migration of the functionality + * from the stand-alone HERACLES application to integrate it into WebAPI and ATLAS. + * + * @summary Cohort Analysis Results (a.k.a Heracles Results) + */ +@RestController +@RequestMapping("/cohortresults") +public class CohortResultsMvcController extends AbstractMvcController { + + @Autowired + private CohortResultsService cohortResultsService; + + @Autowired + private CohortDefinitionService cohortDefinitionService; + + @Autowired + private CohortDefinitionRepository cohortDefinitionRepository; + + @Autowired + private ObjectMapper mapper; + + /** + * Queries for cohort analysis results for the given cohort definition id + * + * @summary Get results for analysis group + * @param id cohort_definition id + * @param analysisGroup Name of the analysisGrouping under the /resources/cohortresults/sql/ directory + * @param analysisName Name of the analysis, currently the same name as the sql file under analysisGroup + * @param sourceKey the source to retrieve results + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @return List of key, value pairs + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/raw/{analysis_group}/{analysis_name} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/raw/{analysis_group}/{analysis_name} + */ + @GetMapping(value = "/{sourceKey}/{id}/raw/{analysisGroup}/{analysisName}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity>> getCohortResultsRaw( + @PathVariable("id") int id, + @PathVariable("analysisGroup") String analysisGroup, + @PathVariable("analysisName") String analysisName, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam) { + + List> results = cohortResultsService.getCohortResultsRaw( + id, analysisGroup, analysisName, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey); + return ok(results); + } + + /** + * Export the cohort analysis results to a ZIP file + * + * @summary Export cohort analysis results + * @param id The cohort ID + * @param sourceKey The source Key + * @return A response containing the .ZIP file of results + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/export.zip + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/export.zip + */ + @GetMapping(value = "/{sourceKey}/{id}/export.zip", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity exportCohortResults( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + + jakarta.ws.rs.core.Response jerseyResponse = cohortResultsService.exportCohortResults(id, sourceKey); + ByteArrayOutputStream baos = (ByteArrayOutputStream) jerseyResponse.getEntity(); + + ByteArrayResource resource = new ByteArrayResource(baos.toByteArray()); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentDispositionFormData("attachment", "cohort_" + id + "_export.zip"); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + + return ResponseEntity.ok() + .headers(headers) + .contentLength(resource.contentLength()) + .body(resource); + } + + /** + * Provides a warmup mechanism for the data visualization cache. This + * endpoint does not appear to be used and may be a hold over from the + * original HERACLES implementation + * + * @summary Warmup data visualizations + * @param task The cohort analysis task + * @return The number of report visualizations warmed + * + * Jersey: POST /WebAPI/cohortresults/warmup + * Spring MVC: POST /WebAPI/v2/cohortresults/warmup + */ + @PostMapping(value = "/warmup", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity warmUpVisualizationData(@RequestBody CohortAnalysisTask task) { + int result = cohortResultsService.warmUpVisualizationData(task); + return ok(result); + } + + /** + * Provides a list of cohort analysis visualizations that are completed + * + * @summary Get completed cohort analysis visualizations + * @param id The cohort ID + * @param sourceKey The source key + * @return A list of visualization keys that are complete + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/completed + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/completed + */ + @GetMapping(value = "/{sourceKey}/{id}/completed", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCompletedVisualization( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + + Collection result = cohortResultsService.getCompletedVisualiztion(id, sourceKey); + return ok(result); + } + + /** + * Retrieves the tornado plot + * + * @summary Get the tornado plot + * @param sourceKey The source key + * @param cohortDefinitionId The cohort definition id + * @return The tornado plot data + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/tornado + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/tornado + */ + @GetMapping(value = "/{sourceKey}/{id}/tornado", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getTornadoReport( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") int cohortDefinitionId) { + + TornadoReport result = cohortResultsService.getTornadoReport(sourceKey, cohortDefinitionId); + return ok(result); + } + + /** + * Queries for cohort analysis dashboard for the given cohort definition id + * + * @summary Get the dashboard + * @param id The cohort definition id + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param demographicsOnly only render gender and age + * @param refresh Boolean - refresh visualization data + * @return CohortDashboard + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/dashboard + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/dashboard + */ + @GetMapping(value = "/{sourceKey}/{id}/dashboard", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getDashboard( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "demographics_only", defaultValue = "false") boolean demographicsOnly, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortDashboard result = cohortResultsService.getDashboard( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, demographicsOnly, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis condition treemap results for the given cohort definition id + * + * @summary Get condition treemap + * @param sourceKey The source key + * @param id The cohort ID + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/condition/ + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/condition/ + */ + @GetMapping(value = "/{sourceKey}/{id}/condition/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getConditionTreemap( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") int id, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getConditionTreemap( + sourceKey, id, minCovariatePersonCountParam, minIntervalPersonCountParam, refresh); + return ok(result); + } + + /** + * Get the distinct person count for a cohort + * + * @summary Get distinct person count + * @param sourceKey The source key + * @param id The cohort ID + * @param refresh Boolean - refresh visualization data + * @return Distinct person count as integer + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/distinctPersonCount/ + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/distinctPersonCount/ + */ + @GetMapping(value = "/{sourceKey}/{id}/distinctPersonCount/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getRawDistinctPersonCount( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") String id, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + Integer result = cohortResultsService.getRawDistinctPersonCount(sourceKey, id, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis condition drilldown results for the given cohort definition id and condition id + * + * @summary Get condition drilldown report + * @param sourceKey The source key + * @param id The cohort ID + * @param conditionId The condition concept ID + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return The CohortConditionDrilldown detail object + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/condition/{conditionId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/condition/{conditionId} + */ + @GetMapping(value = "/{sourceKey}/{id}/condition/{conditionId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getConditionResults( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") int id, + @PathVariable("conditionId") int conditionId, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortConditionDrilldown result = cohortResultsService.getConditionResults( + sourceKey, id, conditionId, minCovariatePersonCountParam, minIntervalPersonCountParam, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis condition era treemap results for the given cohort definition id + * + * @summary Get condition era treemap + * @param sourceKey The source key + * @param id The cohort ID + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/conditionera/ + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/conditionera/ + */ + @GetMapping(value = "/{sourceKey}/{id}/conditionera/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getConditionEraTreemap( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") int id, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getConditionEraTreemap( + sourceKey, id, minCovariatePersonCountParam, minIntervalPersonCountParam, refresh); + return ok(result); + } + + /** + * Get the completed analyses IDs for the selected cohort and source key + * + * @summary Get completed analyses IDs + * @param sourceKey The source key + * @param id The cohort ID + * @return A list of completed analysis IDs + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/analyses + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/analyses + */ + @GetMapping(value = "/{sourceKey}/{id}/analyses", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCompletedAnalyses( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") String id) { + + List result = cohortResultsService.getCompletedAnalyses(sourceKey, id); + return ok(result); + } + + /** + * Get the analysis generation progress + * + * @summary Get analysis progress + * @param sourceKey The source key + * @param id The cohort ID + * @return The generation progress information + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/info + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/info + */ + @GetMapping(value = "/{sourceKey}/{id}/info", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getAnalysisProgress( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") Integer id) { + + Object result = cohortResultsService.getAnalysisProgress(sourceKey, id); + return ok(result); + } + + /** + * Queries for cohort analysis condition era drilldown results for the given cohort definition id and condition id + * + * @summary Get condition era drilldown report + * @param id The cohort ID + * @param conditionId The condition ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return The CohortConditionEraDrilldown object + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/conditionera/{conditionId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/conditionera/{conditionId} + */ + @GetMapping(value = "/{sourceKey}/{id}/conditionera/{conditionId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getConditionEraDrilldown( + @PathVariable("id") int id, + @PathVariable("conditionId") int conditionId, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortConditionEraDrilldown result = cohortResultsService.getConditionEraDrilldown( + id, conditionId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for drug analysis treemap results for the given cohort definition id + * + * @summary Get drug treemap + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/drug/ + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/drug/ + */ + @GetMapping(value = "/{sourceKey}/{id}/drug/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDrugTreemap( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getDrugTreemap( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis drug drilldown results for the given cohort definition id and drug id + * + * @summary Get drug drilldown report + * @param id The cohort ID + * @param drugId The drug concept ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortDrugDrilldown + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/drug/{drugId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/drug/{drugId} + */ + @GetMapping(value = "/{sourceKey}/{id}/drug/{drugId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getDrugResults( + @PathVariable("id") int id, + @PathVariable("drugId") int drugId, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortDrugDrilldown result = cohortResultsService.getDrugResults( + id, drugId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis drug era treemap results for the given cohort definition id + * + * @summary Get drug era treemap report + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/drugera/ + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/drugera/ + */ + @GetMapping(value = "/{sourceKey}/{id}/drugera/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDrugEraTreemap( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getDrugEraTreemap( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis drug era drilldown results for the given cohort definition id and drug id + * + * @summary Get drug era drilldown report + * @param id The cohort ID + * @param drugId The drug concept ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortDrugEraDrilldown + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/drugera/{drugId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/drugera/{drugId} + */ + @GetMapping(value = "/{sourceKey}/{id}/drugera/{drugId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getDrugEraResults( + @PathVariable("id") int id, + @PathVariable("drugId") int drugId, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortDrugEraDrilldown result = cohortResultsService.getDrugEraResults( + id, drugId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis person results for the given cohort definition id + * + * @summary Get the person report + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortPersonSummary + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/person + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/person + */ + @GetMapping(value = "/{sourceKey}/{id}/person", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getPersonResults( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortPersonSummary result = cohortResultsService.getPersonResults( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis cohort specific results for the given cohort definition id + * + * @summary Get cohort specific results + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortSpecificSummary + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/cohortspecific + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/cohortspecific + */ + @GetMapping(value = "/{sourceKey}/{id}/cohortspecific", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortSpecificResults( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortSpecificSummary result = cohortResultsService.getCohortSpecificResults( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis cohort specific treemap results for the given cohort definition id + * + * @summary Get cohort specific treemap + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortSpecificTreemap + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/cohortspecifictreemap + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/cohortspecifictreemap + */ + @GetMapping(value = "/{sourceKey}/{id}/cohortspecifictreemap", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortSpecificTreemapResults( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortSpecificTreemap result = cohortResultsService.getCohortSpecificTreemapResults( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis procedure drilldown results for the given cohort definition id and concept id + * + * @summary Get procedure drilldown report + * @param id The cohort ID + * @param conceptId The procedure concept ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/cohortspecificprocedure/{conceptId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/cohortspecificprocedure/{conceptId} + */ + @GetMapping(value = "/{sourceKey}/{id}/cohortspecificprocedure/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCohortProcedureDrilldown( + @PathVariable("id") int id, + @PathVariable("conceptId") int conceptId, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getCohortProcedureDrilldown( + id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis drug drilldown results for the given cohort definition id and concept id + * + * @summary Get drug drilldown report for specific concept + * @param id The cohort ID + * @param conceptId The drug concept ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/cohortspecificdrug/{conceptId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/cohortspecificdrug/{conceptId} + */ + @GetMapping(value = "/{sourceKey}/{id}/cohortspecificdrug/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCohortDrugDrilldown( + @PathVariable("id") int id, + @PathVariable("conceptId") int conceptId, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getCohortDrugDrilldown( + id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis condition drilldown results for the given cohort definition id and concept id + * + * @summary Get condition drilldown report by concept ID + * @param id The cohort ID + * @param conceptId The condition concept ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/cohortspecificcondition/{conceptId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/cohortspecificcondition/{conceptId} + */ + @GetMapping(value = "/{sourceKey}/{id}/cohortspecificcondition/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCohortConditionDrilldown( + @PathVariable("id") int id, + @PathVariable("conceptId") int conceptId, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getCohortConditionDrilldown( + id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis for observation treemap + * + * @summary Get observation treemap report + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/observation + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/observation + */ + @GetMapping(value = "/{sourceKey}/{id}/observation", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCohortObservationResults( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getCohortObservationResults( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis observation drilldown results for the given cohort definition id and observation concept id + * + * @summary Get observation drilldown report for a concept ID + * @param id The cohort ID + * @param conceptId The observation concept ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortObservationDrilldown + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/observation/{conceptId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/observation/{conceptId} + */ + @GetMapping(value = "/{sourceKey}/{id}/observation/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortObservationResultsDrilldown( + @PathVariable("id") int id, + @PathVariable("conceptId") int conceptId, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortObservationDrilldown result = cohortResultsService.getCohortObservationResultsDrilldown( + id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis for measurement treemap + * + * @summary Get measurement treemap report + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/measurement + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/measurement + */ + @GetMapping(value = "/{sourceKey}/{id}/measurement", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCohortMeasurementResults( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getCohortMeasurementResults( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis measurement drilldown results for the given cohort definition id and measurement concept id + * + * @summary Get measurement drilldown report for concept ID + * @param id The cohort ID + * @param conceptId The measurement concept ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortMeasurementDrilldown + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/measurement/{conceptId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/measurement/{conceptId} + */ + @GetMapping(value = "/{sourceKey}/{id}/measurement/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortMeasurementResultsDrilldown( + @PathVariable("id") int id, + @PathVariable("conceptId") int conceptId, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortMeasurementDrilldown result = cohortResultsService.getCohortMeasurementResultsDrilldown( + id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis observation period for the given cohort definition id + * + * @summary Get observation period report + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortObservationPeriod + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/observationperiod + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/observationperiod + */ + @GetMapping(value = "/{sourceKey}/{id}/observationperiod", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortObservationPeriod( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortObservationPeriod result = cohortResultsService.getCohortObservationPeriod( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis data density for the given cohort definition id + * + * @summary Get data density report + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortDataDensity + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/datadensity + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/datadensity + */ + @GetMapping(value = "/{sourceKey}/{id}/datadensity", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortDataDensity( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortDataDensity result = cohortResultsService.getCohortDataDensity( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis procedure treemap results for the given cohort definition id + * + * @summary Get procedure treemap report + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/procedure/ + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/procedure/ + */ + @GetMapping(value = "/{sourceKey}/{id}/procedure/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getProcedureTreemap( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getProcedureTreemap( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis procedures for the given cohort definition id and concept id + * + * @summary Get procedure drilldown report by concept ID + * @param id The cohort ID + * @param conceptId The procedure concept ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortProceduresDrillDown + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/procedure/{conceptId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/procedure/{conceptId} + */ + @GetMapping(value = "/{sourceKey}/{id}/procedure/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortProceduresDrilldown( + @PathVariable("id") int id, + @PathVariable("conceptId") int conceptId, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortProceduresDrillDown result = cohortResultsService.getCohortProceduresDrilldown( + id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis visit treemap results for the given cohort definition id + * + * @summary Get visit treemap report + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/visit/ + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/visit/ + */ + @GetMapping(value = "/{sourceKey}/{id}/visit/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getVisitTreemap( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getVisitTreemap( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Queries for cohort analysis visits for the given cohort definition id and concept id + * + * @summary Get visit drilldown for a visit concept ID + * @param id The cohort ID + * @param conceptId The visit concept ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortVisitsDrilldown + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/visit/{conceptId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/visit/{conceptId} + */ + @GetMapping(value = "/{sourceKey}/{id}/visit/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortVisitsDrilldown( + @PathVariable("id") int id, + @PathVariable("conceptId") int conceptId, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortVisitsDrilldown result = cohortResultsService.getCohortVisitsDrilldown( + id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Returns the summary for the cohort + * + * @summary Get cohort summary + * @param id The cohort ID + * @param sourceKey The source key + * @return CohortSummary + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/summarydata + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/summarydata + */ + @GetMapping(value = "/{sourceKey}/{id}/summarydata", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortSummaryData( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + + CohortSummary result = cohortResultsService.getCohortSummaryData(id, sourceKey); + return ok(result); + } + + /** + * Queries for cohort analysis death data for the given cohort definition id + * + * @summary Get death report + * @param id The cohort ID + * @param sourceKey The source key + * @param minCovariatePersonCountParam The minimum number of covariates per person + * @param minIntervalPersonCountParam The minimum interval person count + * @param refresh Boolean - refresh visualization data + * @return CohortDeathData + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/death + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/death + */ + @GetMapping(value = "/{sourceKey}/{id}/death", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortDeathData( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + CohortDeathData result = cohortResultsService.getCohortDeathData( + id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); + return ok(result); + } + + /** + * Returns the summary for the cohort + * + * @summary Get cohort summary analyses + * @param id The cohort ID + * @param sourceKey The source key + * @return CohortSummary + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/summaryanalyses + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/summaryanalyses + */ + @GetMapping(value = "/{sourceKey}/{id}/summaryanalyses", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortSummaryAnalyses( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + + CohortSummary result = cohortResultsService.getCohortSummaryAnalyses(id, sourceKey); + return ok(result); + } + + /** + * Returns breakdown with counts about people in cohort + * + * @summary Get cohort breakdown report + * @param id The cohort ID + * @param sourceKey The source key + * @return Collection + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/breakdown + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/breakdown + */ + @GetMapping(value = "/{sourceKey}/{id}/breakdown", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCohortBreakdown( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + + Collection result = cohortResultsService.getCohortBreakdown(id, sourceKey); + return ok(result); + } + + /** + * Returns the count of all members of a generated cohort definition identifier + * + * @summary Get cohort member count + * @param id The cohort ID + * @param sourceKey The source key + * @return The cohort count + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/members/count + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/members/count + */ + @GetMapping(value = "/{sourceKey}/{id}/members/count", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCohortMemberCount( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + + Long result = cohortResultsService.getCohortMemberCount(id, sourceKey); + return ok(result); + } + + /** + * Returns all cohort analyses in the results/OHDSI schema for the given cohort_definition_id + * + * @summary Get the cohort analysis list for a cohort + * @param id The cohort ID + * @param sourceKey The source key + * @param retrieveFullDetail Boolean - when TRUE, the full analysis details are returned + * @return List of all cohort analyses and their statuses for the given cohort_definition_id + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id} + */ + @GetMapping(value = "/{sourceKey}/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCohortAnalysesForCohortDefinition( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "fullDetail", defaultValue = "true") boolean retrieveFullDetail) { + + List result = cohortResultsService.getCohortAnalysesForCohortDefinition(id, sourceKey, retrieveFullDetail); + return ok(result); + } + + /** + * Get the exposure cohort incidence rates. This function is not using a + * proper incidence rate so this should be viewed as informational only + * and not as a report + * + * @summary DO NOT USE + * @deprecated + * @param sourceKey The source key + * @param search The exposure cohort search + * @return List + * + * Jersey: POST /WebAPI/cohortresults/{sourceKey}/exposurecohortrates + * Spring MVC: POST /WebAPI/v2/cohortresults/{sourceKey}/exposurecohortrates + */ + @PostMapping(value = "/{sourceKey}/exposurecohortrates", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Deprecated + public ResponseEntity> getExposureOutcomeCohortRates( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ExposureCohortSearch search) { + + List result = cohortResultsService.getExposureOutcomeCohortRates(sourceKey, search); + return ok(result); + } + + /** + * Provides a time to event calculation but it is unclear how this works. + * + * @summary DO NOT USE + * @deprecated + * @param sourceKey The source key + * @param search The exposure cohort search + * @return List + * + * Jersey: POST /WebAPI/cohortresults/{sourceKey}/timetoevent + * Spring MVC: POST /WebAPI/v2/cohortresults/{sourceKey}/timetoevent + */ + @PostMapping(value = "/{sourceKey}/timetoevent", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Deprecated + public ResponseEntity> getTimeToEventDrilldown( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ExposureCohortSearch search) { + + List result = cohortResultsService.getTimeToEventDrilldown(sourceKey, search); + return ok(result); + } + + /** + * Provides a predictor calculation but it is unclear how this works. + * + * @summary DO NOT USE + * @deprecated + * @param sourceKey The source key + * @param search The exposure cohort search + * @return List + * + * Jersey: POST /WebAPI/cohortresults/{sourceKey}/predictors + * Spring MVC: POST /WebAPI/v2/cohortresults/{sourceKey}/predictors + */ + @PostMapping(value = "/{sourceKey}/predictors", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Deprecated + public ResponseEntity> getExposureOutcomeCohortPredictors( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ExposureCohortSearch search) { + + List result = cohortResultsService.getExposureOutcomeCohortPredictors(sourceKey, search); + return ok(result); + } + + /** + * Returns heracles heel results (data quality issues) for the given cohort definition id + * + * @summary Get HERACLES heel report + * @param id The cohort ID + * @param sourceKey The source key + * @param refresh Boolean - refresh visualization data + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/heraclesheel + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/heraclesheel + */ + @GetMapping(value = "/{sourceKey}/{id}/heraclesheel", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getHeraclesHeel( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { + + List result = cohortResultsService.getHeraclesHeel(id, sourceKey, refresh); + return ok(result); + } + + /** + * Provides a data completeness report for a cohort + * + * @summary Get data completeness report + * @param id The cohort ID + * @param sourceKey The source key + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/datacompleteness + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/datacompleteness + */ + @GetMapping(value = "/{sourceKey}/{id}/datacompleteness", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDataCompleteness( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + + List result = cohortResultsService.getDataCompleteness(id, sourceKey); + return ok(result); + } + + /** + * Provide an entropy report for a cohort + * + * @summary Get entropy report + * @param id The cohort ID + * @param sourceKey The source key + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/entropy + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/entropy + */ + @GetMapping(value = "/{sourceKey}/{id}/entropy", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getEntropy( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + + List result = cohortResultsService.getEntropy(id, sourceKey); + return ok(result); + } + + /** + * Provide a full entropy report for a cohort + * + * @summary Get full entropy report + * @param id The cohort ID + * @param sourceKey The source key + * @return List + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/allentropy + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/allentropy + */ + @GetMapping(value = "/{sourceKey}/{id}/allentropy", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getAllEntropy( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + + List result = cohortResultsService.getAllEntropy(id, sourceKey); + return ok(result); + } + + /** + * Get the healthcare utilization exposure report for a specific window + * + * @summary Get healthcare utilization report for selected time window + * @param id The cohort ID + * @param sourceKey The source key + * @param window The time window + * @param periodType The period type + * @return HealthcareExposureReport + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/exposure/{window} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/exposure/{window} + */ + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/exposure/{window}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getHealthcareUtilizationExposureReport( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("window") WindowType window, + @RequestParam(value = "periodType", defaultValue = "ww") PeriodType periodType) { + + HealthcareExposureReport result = cohortResultsService.getHealthcareUtilizationExposureReport(id, sourceKey, window, periodType); + return ok(result); + } + + /** + * Get the healthcare utilization periods + * + * @summary Get healthcare utilization periods + * @param id The cohort ID + * @param sourceKey The source key + * @param window The time window + * @return A list of the periods + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/periods/{window} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/periods/{window} + */ + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/periods/{window}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getHealthcareUtilizationPeriods( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("window") WindowType window) { + + List result = cohortResultsService.getHealthcareUtilizationPeriods(id, sourceKey, window); + return ok(result); + } + + /** + * Get the healthcare utilization report by window, visit status, period type, visit concept, visit type concept and cost type concept. + * + * @summary Get healthcare utilization visit report + * @param id The cohort ID + * @param sourceKey The source key + * @param window The time window + * @param visitStat The visit status + * @param periodType The period type + * @param visitConcept The visit concept ID + * @param visitTypeConcept The visit type concept ID + * @param costTypeConcept The cost type concept ID + * @return HealthcareVisitUtilizationReport + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/visit/{window}/{visitStat} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/visit/{window}/{visitStat} + */ + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/visit/{window}/{visitStat}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getHealthcareUtilizationVisitReport( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("window") WindowType window, + @PathVariable("visitStat") VisitStatType visitStat, + @RequestParam(value = "periodType", defaultValue = "ww") PeriodType periodType, + @RequestParam(value = "visitConcept", required = false) Long visitConcept, + @RequestParam(value = "visitTypeConcept", required = false) Long visitTypeConcept, + @RequestParam(value = "costTypeConcept", defaultValue = "31968") Long costTypeConcept) { + + HealthcareVisitUtilizationReport result = cohortResultsService.getHealthcareUtilizationVisitReport( + id, sourceKey, window, visitStat, periodType, visitConcept, visitTypeConcept, costTypeConcept); + return ok(result); + } + + /** + * Get the healthcare utilization summary report by drug and cost type concept + * + * @summary Get healthcare utilization drug summary report + * @param id The cohort ID + * @param sourceKey The source key + * @param window The time window + * @param drugTypeConceptId The drug type concept ID + * @param costTypeConceptId The cost type concept ID + * @return HealthcareDrugUtilizationSummary + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/drug/{window} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/drug/{window} + */ + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/drug/{window}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getHealthcareUtilizationDrugSummaryReport( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("window") WindowType window, + @RequestParam(value = "drugType", required = false) Long drugTypeConceptId, + @RequestParam(value = "costType", defaultValue = "31968") Long costTypeConceptId) { + + HealthcareDrugUtilizationSummary result = cohortResultsService.getHealthcareUtilizationDrugSummaryReport( + id, sourceKey, window, drugTypeConceptId, costTypeConceptId); + return ok(result); + } + + /** + * Get the healthcare utilization detail report by drug and cost type concept + * + * @summary Get healthcare utilization drug detail report + * @param id The cohort ID + * @param sourceKey The source key + * @param window The time window + * @param drugConceptId The drug concept ID + * @param periodType The period type + * @param drugTypeConceptId The drug type concept ID + * @param costTypeConceptId The cost type concept ID + * @return HealthcareDrugUtilizationDetail + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/drug/{window}/{drugConceptId} + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/drug/{window}/{drugConceptId} + */ + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/drug/{window}/{drugConceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getHealthcareUtilizationDrugDetailReport( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("window") WindowType window, + @PathVariable("drugConceptId") Long drugConceptId, + @RequestParam(value = "periodType", defaultValue = "ww") PeriodType periodType, + @RequestParam(value = "drugType", required = false) Long drugTypeConceptId, + @RequestParam(value = "costType", defaultValue = "31968") Long costTypeConceptId) { + + HealthcareDrugUtilizationDetail result = cohortResultsService.getHealthcareUtilizationDrugDetailReport( + id, sourceKey, window, drugConceptId, periodType, drugTypeConceptId, costTypeConceptId); + return ok(result); + } + + /** + * Get the drug type concepts for the selected drug concept ID + * + * @summary Get drug types for healthcare utilization report + * @param id The cohort ID + * @param sourceKey The source key + * @param drugConceptId The drug concept ID + * @return A list of concepts of drug types + * + * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/drugtypes + * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/drugtypes + */ + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/drugtypes", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDrugTypes( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "drugConceptId", required = false) Long drugConceptId) { + + List result = cohortResultsService.getDrugTypes(id, sourceKey, drugConceptId); + return ok(result); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/ConceptSetMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/ConceptSetMvcController.java new file mode 100644 index 0000000000..a2dbac17dc --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/ConceptSetMvcController.java @@ -0,0 +1,1062 @@ +/* + * Copyright 2015 fdefalco. + * + * 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.mvc.controller; + +import java.io.ByteArrayOutputStream; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.shiro.authz.UnauthorizedException; +import org.ohdsi.circe.vocabulary.ConceptSetExpression; +import org.ohdsi.vocabulary.Concept; +import org.ohdsi.webapi.check.CheckResult; +import org.ohdsi.webapi.check.checker.conceptset.ConceptSetChecker; +import org.ohdsi.webapi.conceptset.ConceptSet; +import org.ohdsi.webapi.conceptset.ConceptSetExport; +import org.ohdsi.webapi.conceptset.ConceptSetGenerationInfo; +import org.ohdsi.webapi.conceptset.ConceptSetGenerationInfoRepository; +import org.ohdsi.webapi.conceptset.ConceptSetItem; +import org.ohdsi.webapi.conceptset.ConceptSetItemRepository; +import org.ohdsi.webapi.conceptset.ConceptSetRepository; +import org.ohdsi.webapi.conceptset.dto.ConceptSetVersionFullDTO; +import org.ohdsi.webapi.conceptset.annotation.ConceptSetAnnotation; +import org.ohdsi.webapi.conceptset.annotation.ConceptSetAnnotationRepository; +import org.ohdsi.webapi.exception.ConceptNotExistException; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.security.PermissionService; +import org.ohdsi.webapi.service.VocabularyService; +import org.ohdsi.webapi.service.annotations.SearchDataTransformer; +import org.ohdsi.webapi.service.dto.AnnotationDetailsDTO; +import org.ohdsi.webapi.service.dto.ConceptSetDTO; +import org.ohdsi.webapi.service.dto.SaveConceptSetAnnotationsRequest; +import org.ohdsi.webapi.service.dto.AnnotationDTO; +import org.ohdsi.webapi.service.dto.CopyAnnotationsRequest; +import org.ohdsi.webapi.shiro.Entities.UserEntity; +import org.ohdsi.webapi.shiro.Entities.UserRepository; +import org.ohdsi.webapi.shiro.management.datasource.SourceAccessor; +import org.ohdsi.webapi.source.Source; +import org.ohdsi.webapi.source.SourceInfo; +import org.ohdsi.webapi.source.SourceService; +import org.ohdsi.webapi.tag.TagService; +import org.ohdsi.webapi.tag.domain.Tag; +import org.ohdsi.webapi.tag.dto.TagNameListRequestDTO; +import org.ohdsi.webapi.util.ExportUtil; +import org.ohdsi.webapi.util.NameUtils; +import org.ohdsi.webapi.util.ExceptionUtils; +import org.ohdsi.webapi.versioning.domain.ConceptSetVersion; +import org.ohdsi.webapi.versioning.domain.Version; +import org.ohdsi.webapi.versioning.domain.VersionBase; +import org.ohdsi.webapi.versioning.domain.VersionType; +import org.ohdsi.webapi.versioning.dto.VersionDTO; +import org.ohdsi.webapi.versioning.dto.VersionUpdateDTO; +import org.ohdsi.webapi.versioning.service.VersionService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +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.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.web.bind.annotation.*; + +import static org.ohdsi.webapi.service.ConceptSetService.CachingSetup.CONCEPT_SET_LIST_CACHE; + +/** + * Provides REST services for working with concept sets using Spring MVC. + * This is a Spring MVC migration of the original Jersey-based ConceptSetService. + * + * @summary Concept Set (Spring MVC) + */ +@RestController +@RequestMapping("/conceptset") +@Transactional +public class ConceptSetMvcController extends AbstractMvcController { + + private static final Logger log = LoggerFactory.getLogger(ConceptSetMvcController.class); + + @Autowired + private ConceptSetRepository conceptSetRepository; + + @Autowired + private ConceptSetItemRepository conceptSetItemRepository; + + @Autowired + private ConceptSetAnnotationRepository conceptSetAnnotationRepository; + + @Autowired + private ConceptSetGenerationInfoRepository conceptSetGenerationInfoRepository; + + @Autowired + private VocabularyService vocabService; + + @Autowired + private SourceService sourceService; + + @Autowired + private SourceAccessor sourceAccessor; + + @Autowired + private UserRepository userRepository; + + @Autowired + private GenericConversionService conversionService; + + @Autowired + private PermissionService permissionService; + + @Autowired + private ConceptSetChecker checker; + + @Autowired + private VersionService versionService; + + @Autowired + private SearchDataTransformer searchDataTransformer; + + @Autowired + private ObjectMapper mapper; + + @Autowired + private TransactionTemplate transactionTemplate; + + @Autowired + private TagService tagService; + + @Value("${security.defaultGlobalReadPermissions}") + private boolean defaultGlobalReadPermissions; + + public static final String COPY_NAME = "copyName"; + + protected ConceptSetRepository getConceptSetRepository() { + return conceptSetRepository; + } + + protected ConceptSetItemRepository getConceptSetItemRepository() { + return conceptSetItemRepository; + } + + protected ConceptSetAnnotationRepository getConceptSetAnnotationRepository() { + return conceptSetAnnotationRepository; + } + + protected TransactionTemplate getTransactionTemplate() { + return transactionTemplate; + } + + protected UserEntity getCurrentUserEntity() { + return userRepository.findByLogin(security.getSubject()); + } + + protected void checkOwnerOrAdminOrGranted(ConceptSet entity) { + // Check permission - if user doesn't have write access, this will throw an exception + if (!permissionService.hasWriteAccess(entity)) { + throw new org.apache.shiro.authz.UnauthorizedException("No write access to this concept set"); + } + } + + protected void assignTag(ConceptSet entity, int tagId) { + if (Objects.nonNull(entity)) { + Tag tag = tagService.getById(tagId); + if (Objects.nonNull(tag)) { + entity.getTags().add(tag); + getConceptSetRepository().save(entity); + } + } + } + + protected void unassignTag(ConceptSet entity, int tagId) { + if (Objects.nonNull(entity)) { + Set tags = entity.getTags().stream() + .filter(t -> t.getId() != tagId) + .collect(Collectors.toSet()); + entity.setTags(tags); + getConceptSetRepository().save(entity); + } + } + + protected List listByTags( + List entities, + List names, + Class clazz) { + return entities.stream() + .filter(e -> e.getTags().stream() + .map(tag -> tag.getName().toLowerCase(Locale.ROOT)) + .anyMatch(names::contains)) + .map(e -> conversionService.convert(e, clazz)) + .collect(Collectors.toList()); + } + + /** + * Get the concept set based in the identifier + * + * @summary Get concept set by ID + * @param id The concept set ID + * @return The concept set definition + */ + @GetMapping("/{id}") + public ResponseEntity getConceptSet(@PathVariable("id") final int id) { + ConceptSet conceptSet = getConceptSetRepository().findById(id).orElse(null); + ExceptionUtils.throwNotFoundExceptionIfNull(conceptSet, String.format("There is no concept set with id = %d.", id)); + return ok(conversionService.convert(conceptSet, ConceptSetDTO.class)); + } + + /** + * Get the full list of concept sets in the WebAPI database + * + * @summary Get all concept sets + * @return A list of all concept sets in the WebAPI database + */ + @GetMapping("/") + @Cacheable(cacheNames = CONCEPT_SET_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()") + public ResponseEntity> getConceptSets() { + Collection result = getTransactionTemplate().execute( + transactionStatus -> StreamSupport.stream(getConceptSetRepository().findAll().spliterator(), false) + .filter(!defaultGlobalReadPermissions ? entity -> permissionService.hasReadAccess(entity) : entity -> true) + .map(conceptSet -> { + ConceptSetDTO dto = conversionService.convert(conceptSet, ConceptSetDTO.class); + permissionService.fillWriteAccess(conceptSet, dto); + permissionService.fillReadAccess(conceptSet, dto); + return dto; + }) + .collect(Collectors.toList())); + return ok(result); + } + + /** + * Get the concept set items for a selected concept set ID. + * + * @summary Get the concept set items + * @param id The concept set identifier + * @return A list of concept set items + */ + @GetMapping("/{id}/items") + public ResponseEntity> getConceptSetItems(@PathVariable("id") final int id) { + return ok(getConceptSetItemRepository().findAllByConceptSetId(id)); + } + + /** + * Get the concept set expression for a selected version of the expression + * + * @summary Get concept set expression by version + * @param id The concept set ID + * @param version The version identifier + * @return The concept set expression + */ + @GetMapping("/{id}/version/{version}/expression") + public ResponseEntity getConceptSetExpressionByVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version) { + SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); + if (sourceInfo == null) { + throw new UnauthorizedException(); + } + return ok(getConceptSetExpression(id, version, sourceInfo)); + } + + /** + * Get the concept set expression by version for the selected + * source key. NOTE: This method requires the specification + * of a source key but it does not appear to be used by the underlying + * code. + * + * @summary Get concept set expression by version and source. + * @param id The concept set identifier + * @param version The version of the concept set + * @param sourceKey The source key + * @return The concept set expression for the selected version + */ + @GetMapping("/{id}/version/{version}/expression/{sourceKey}") + public ResponseEntity getConceptSetExpressionByVersionAndSource( + @PathVariable("id") final int id, + @PathVariable("version") final int version, + @PathVariable("sourceKey") final String sourceKey) { + SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); + if (sourceInfo == null) { + throw new UnauthorizedException(); + } + return ok(getConceptSetExpression(id, version, sourceInfo)); + } + + /** + * Get the concept set expression by identifier + * + * @summary Get concept set by ID + * @param id The concept set identifier + * @return The concept set expression + */ + @GetMapping("/{id}/expression") + public ResponseEntity getConceptSetExpressionById(@PathVariable("id") final int id) { + SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); + if (sourceInfo == null) { + throw new UnauthorizedException(); + } + return ok(getConceptSetExpression(id, null, sourceInfo)); + } + + /** + * Get the concept set expression by identifier and source key + * + * @summary Get concept set by ID and source + * @param id The concept set ID + * @param sourceKey The source key + * @return The concept set expression + */ + @GetMapping("/{id}/expression/{sourceKey}") + public ResponseEntity getConceptSetExpressionByIdAndSource( + @PathVariable("id") final int id, + @PathVariable("sourceKey") final String sourceKey) { + Source source = sourceService.findBySourceKey(sourceKey); + sourceAccessor.checkAccess(source); + return ok(getConceptSetExpression(id, null, source.getSourceInfo())); + } + + private ConceptSetExpression getConceptSetExpression(int id, Integer version, SourceInfo sourceInfo) { + HashMap map = new HashMap<>(); + + // create our expression to return + ConceptSetExpression expression = new ConceptSetExpression(); + ArrayList expressionItems = new ArrayList<>(); + + List repositoryItems = new ArrayList<>(); + if (Objects.isNull(version)) { + getConceptSetItemRepository().findAllByConceptSetId(id).forEach(repositoryItems::add); + } else { + ConceptSetVersionFullDTO dto = getVersion(id, version); + repositoryItems.addAll(dto.getItems()); + } + + // collect the unique concept IDs so we can load the concept object later. + for (ConceptSetItem csi : repositoryItems) { + map.put(csi.getConceptId(), null); + } + + // lookup the concepts we need information for + long[] identifiers = new long[map.size()]; + int identifierIndex = 0; + for (Long identifier : map.keySet()) { + identifiers[identifierIndex] = identifier; + identifierIndex++; + } + + String sourceKey; + if (Objects.isNull(sourceInfo)) { + sourceKey = sourceService.getPriorityVocabularySource().getSourceKey(); + } else { + sourceKey = sourceInfo.sourceKey; + } + + Collection concepts = vocabService.executeIdentifierLookup(sourceKey, identifiers); + if (concepts.size() != identifiers.length) { + String ids = Arrays.stream(identifiers).boxed() + .filter(identifier -> concepts.stream().noneMatch(c -> c.conceptId.equals(identifier))) + .map(String::valueOf) + .collect(Collectors.joining(",", "(", ")")); + throw new ConceptNotExistException("Current data source does not contain required concepts " + ids); + } + for(Concept concept : concepts) { + map.put(concept.conceptId, concept); // associate the concept object to the conceptID in the map + } + + // put the concept information into the expression along with the concept set item information + for (ConceptSetItem repositoryItem : repositoryItems) { + ConceptSetExpression.ConceptSetItem currentItem = new ConceptSetExpression.ConceptSetItem(); + currentItem.concept = map.get(repositoryItem.getConceptId()); + currentItem.includeDescendants = (repositoryItem.getIncludeDescendants() == 1); + currentItem.includeMapped = (repositoryItem.getIncludeMapped() == 1); + currentItem.isExcluded = (repositoryItem.getIsExcluded() == 1); + expressionItems.add(currentItem); + } + expression.items = expressionItems.toArray(new ConceptSetExpression.ConceptSetItem[0]); // this will return a new array + + return expression; + } + + /** + * Check if the concept set name exists (DEPRECATED) + * + * @summary DO NOT USE + * @deprecated + * @param id The concept set ID + * @param name The concept set name + * @return The concept set expression + */ + @Deprecated + @GetMapping("/{id}/{name}/exists") + public ResponseEntity> getConceptSetExistsDeprecated( + @PathVariable("id") final int id, + @PathVariable("name") String name) { + String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: conceptset/{id}/exists?name={name}"; + Collection cs = getConceptSetRepository().conceptSetExists(id, name); + return ResponseEntity.ok() + .header("Warning", "299 - " + warningMessage) + .body(cs); + } + + /** + * Check if a concept set with the same name exists in the WebAPI + * database. The name is checked against the selected concept set ID + * to ensure that only the selected concept set ID has the name specified. + * + * @summary Concept set with same name exists + * @param id The concept set ID + * @param name The name of the concept set + * @return The count of concept sets with the name, excluding the + * specified concept set ID. + */ + @GetMapping("/{id}/exists") + public ResponseEntity getCountCSetWithSameName( + @PathVariable("id") final int id, + @RequestParam(value = "name", required = false) String name) { + return ok(getConceptSetRepository().getCountCSetWithSameName(id, name)); + } + + /** + * Update the concept set items for the selected concept set ID in the + * WebAPI database. + * + * The concept set has two parts: 1) the elements of the ConceptSetDTO that + * consist of the identifier, name, etc. 2) the concept set items which + * contain the concepts and their mapping (i.e. include descendants). + * + * @summary Update concept set items + * @param id The concept set ID + * @param items An array of ConceptSetItems + * @return Boolean: true if the save is successful + */ + @PutMapping("/{id}/items") + public ResponseEntity saveConceptSetItems( + @PathVariable("id") final int id, + @RequestBody ConceptSetItem[] items) { + getConceptSetItemRepository().deleteByConceptSetId(id); + + for (ConceptSetItem csi : items) { + // ID must be set to null in case of copying from version, so the new item will be created + csi.setId(0); + csi.setConceptSetId(id); + getConceptSetItemRepository().save(csi); + } + + return ok(true); + } + + /** + * Exports a list of concept sets, based on the conceptSetList argument, + * to one or more comma separated value (CSV) file(s), compresses the files + * into a ZIP file and sends the ZIP file to the client. + * + * @summary Export concept set list to CSV files + * @param conceptSetList A list of concept set identifiers in the format + * conceptset=++ + * @return + * @throws Exception + */ + @GetMapping("/exportlist") + public ResponseEntity exportConceptSetList( + @RequestParam("conceptsets") final String conceptSetList) throws Exception { + ArrayList conceptSetIds = new ArrayList<>(); + try { + String[] conceptSetItems = conceptSetList.split("\\+"); + for(String csi : conceptSetItems) { + conceptSetIds.add(Integer.valueOf(csi)); + } + if (conceptSetIds.size() <= 0) { + throw new IllegalArgumentException("You must supply a querystring value for conceptsets that is of the form: ?conceptset=++"); + } + } catch (Exception e) { + throw e; + } + + ByteArrayOutputStream baos; + Source source = sourceService.getPriorityVocabularySource(); + ArrayList cs = new ArrayList<>(); + try { + // Load all of the concept sets requested + for (int i = 0; i < conceptSetIds.size(); i++) { + // Get the concept set information + cs.add(getConceptSetForExport(conceptSetIds.get(i), new SourceInfo(source))); + } + // Write Concept Set Expression to a CSV + baos = ExportUtil.writeConceptSetExportToCSVAndZip(cs); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentDispositionFormData("attachment", "conceptSetExport.zip"); + + return ResponseEntity.ok() + .headers(headers) + .body(baos.toByteArray()); + + } catch (Exception ex) { + throw ex; + } + } + + /** + * Exports a single concept set to a comma separated value (CSV) + * file, compresses to a ZIP file and sends to the client. + * + * @param id The concept set ID + * @return A zip file containing the exported concept set + * @throws Exception + */ + @GetMapping("/{id}/export") + public ResponseEntity exportConceptSetToCSV(@PathVariable("id") final String id) throws Exception { + return this.exportConceptSetList(id); + } + + /** + * Save a new concept set to the WebAPI database + * + * @summary Create a new concept set + * @param conceptSetDTO The concept set to save + * @return The concept set saved with the concept set identifier + */ + @PostMapping("/") + @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) + public ResponseEntity createConceptSet(@RequestBody ConceptSetDTO conceptSetDTO) { + UserEntity user = getCurrentUserEntity(); + ConceptSet conceptSet = conversionService.convert(conceptSetDTO, ConceptSet.class); + ConceptSet updated = new ConceptSet(); + updated.setCreatedBy(user); + updated.setCreatedDate(new Date()); + updated.setTags(null); + updateConceptSet(updated, conceptSet); + return ok(conversionService.convert(updated, ConceptSetDTO.class)); + } + + /** + * Creates a concept set name, based on the selected concept set ID, + * that is used when generating a copy of an existing concept set. This + * function is generally used in conjunction with the copy endpoint to + * create a unique name and then save a copy of an existing concept set. + * + * @summary Get concept set name suggestion for copying + * @param id The concept set ID + * @return A map of the new concept set name and the existing concept set + * name + */ + @GetMapping("/{id}/copy-name") + public ResponseEntity> getNameForCopy(@PathVariable("id") final int id) { + ConceptSetDTO source = getConceptSet(id).getBody(); + String name = NameUtils.getNameForCopy(source.getName(), this::getNamesLike, getConceptSetRepository().findByName(source.getName())); + return ok(Collections.singletonMap(COPY_NAME, name)); + } + + public List getNamesLike(String copyName) { + return getConceptSetRepository().findAllByNameStartsWith(copyName).stream().map(ConceptSet::getName).collect(Collectors.toList()); + } + + /** + * Updates the concept set for the selected concept set. + * + * The concept set has two parts: 1) the elements of the ConceptSetDTO that + * consist of the identifier, name, etc. 2) the concept set items which + * contain the concepts and their mapping (i.e. include descendants). + * + * @summary Update concept set + * @param id The concept set identifier + * @param conceptSetDTO The concept set header + * @return The + * @throws Exception + */ + @PutMapping("/{id}") + @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) + public ResponseEntity updateConceptSet( + @PathVariable("id") final int id, + @RequestBody ConceptSetDTO conceptSetDTO) throws Exception { + ConceptSet updated = getConceptSetRepository().findById(id).orElse(null); + if (updated == null) { + throw new Exception("Concept Set does not exist."); + } + + saveVersion(id); + + ConceptSet conceptSet = conversionService.convert(conceptSetDTO, ConceptSet.class); + return ok(conversionService.convert(updateConceptSet(updated, conceptSet), ConceptSetDTO.class)); + } + + private ConceptSet updateConceptSet(ConceptSet dst, ConceptSet src) { + UserEntity user = getCurrentUserEntity(); + dst.setName(src.getName()); + dst.setDescription(src.getDescription()); + dst.setModifiedDate(new Date()); + dst.setModifiedBy(user); + + dst = this.getConceptSetRepository().save(dst); + return dst; + } + + private ConceptSetExport getConceptSetForExport(int conceptSetId, SourceInfo vocabSource) { + ConceptSetExport cs = new ConceptSetExport(); + + // Set the concept set id + cs.ConceptSetId = conceptSetId; + // Get the concept set information + cs.ConceptSetName = this.getConceptSet(conceptSetId).getBody().getName(); + // Get the concept set expression + cs.csExpression = this.getConceptSetExpressionById(conceptSetId).getBody(); + + // Lookup the identifiers + cs.identifierConcepts = vocabService.executeIncludedConceptLookup(vocabSource.sourceKey, cs.csExpression); + // Lookup the mapped items + cs.mappedConcepts = vocabService.executeMappedLookup(vocabSource.sourceKey, cs.csExpression); + + return cs; + } + + /** + * Get the concept set generation information for the selected concept + * set ID. This function only works with the configuration of the CEM + * data source. + * + * @link https://github.com/OHDSI/CommonEvidenceModel/wiki + * + * @summary Get concept set generation info + * @param id The concept set identifier. + * @return A collection of concept set generation info objects + */ + @GetMapping("/{id}/generationinfo") + public ResponseEntity> getConceptSetGenerationInfo(@PathVariable("id") final int id) { + return ok(this.conceptSetGenerationInfoRepository.findAllByConceptSetId(id)); + } + + /** + * Delete the selected concept set by concept set identifier + * + * @summary Delete concept set + * @param id The concept set ID + */ + @DeleteMapping("/{id}") + @Transactional(rollbackFor = Exception.class, noRollbackFor = EmptyResultDataAccessException.class) + @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) + public ResponseEntity deleteConceptSet(@PathVariable("id") final int id) { + // Remove any generation info + try { + this.conceptSetGenerationInfoRepository.deleteByConceptSetId(id); + } catch (EmptyResultDataAccessException e) { + // Ignore - there may be no data + log.warn("Failed to delete Generation Info by ConceptSet with ID = {}, {}", id, e); + } + catch (Exception e) { + throw e; + } + + // Remove the concept set items + try { + getConceptSetItemRepository().deleteByConceptSetId(id); + } catch (EmptyResultDataAccessException e) { + // Ignore - there may be no data + log.warn("Failed to delete ConceptSet items with ID = {}, {}", id, e); + } + catch (Exception e) { + throw e; + } + + // Remove the concept set + try { + getConceptSetRepository().deleteById(id); + } catch (EmptyResultDataAccessException e) { + // Ignore - there may be no data + log.warn("Failed to delete ConceptSet with ID = {}, {}", id, e); + } + catch (Exception e) { + throw e; + } + + return ok(); + } + + /** + * Assign tag to Concept Set + * + * @summary Assign concept set tag + * @since v2.10.0 + * @param id The concept set ID + * @param tagId The tag ID + */ + @PostMapping("/{id}/tag/") + @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) + public ResponseEntity assignTag( + @PathVariable("id") final Integer id, + @RequestBody int tagId) { + ConceptSet entity = getConceptSetRepository().findById(id).orElse(null); + assignTag(entity, tagId); + return ok(); + } + + /** + * Unassign tag from Concept Set + * + * @summary Remove tag from concept set + * @since v2.10.0 + * @param id The concept set ID + * @param tagId The tag ID + */ + @DeleteMapping("/{id}/tag/{tagId}") + @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) + public ResponseEntity unassignTag( + @PathVariable("id") final Integer id, + @PathVariable("tagId") final int tagId) { + ConceptSet entity = getConceptSetRepository().findById(id).orElse(null); + unassignTag(entity, tagId); + return ok(); + } + + /** + * Assign protected tag to Concept Set + * + * @summary Assign protected concept set tag + * @since v2.10.0 + * @param id The concept set ID + * @param tagId The tag ID + */ + @PostMapping("/{id}/protectedtag/") + public ResponseEntity assignPermissionProtectedTag( + @PathVariable("id") final int id, + @RequestBody final int tagId) { + assignTag(id, tagId); + return ok(); + } + + /** + * Unassign protected tag from Concept Set + * + * @summary Remove protected concept set tag + * @since v2.10.0 + * @param id The concept set ID + * @param tagId The tag ID + */ + @DeleteMapping("/{id}/protectedtag/{tagId}") + @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) + public ResponseEntity unassignPermissionProtectedTag( + @PathVariable("id") final int id, + @PathVariable("tagId") final int tagId) { + unassignTag(id, tagId); + return ok(); + } + + /** + * Checks a concept set for diagnostic problems. At this time, + * this appears to be an endpoint used to check to see which tags + * are applied to a concept set. + * + * @summary Concept set tag check + * @since v2.10.0 + * @param conceptSetDTO The concept set + * @return A check result + */ + @PostMapping("/check") + public ResponseEntity runDiagnostics(@RequestBody ConceptSetDTO conceptSetDTO) { + return ok(new CheckResult(checker.check(conceptSetDTO))); + } + + /** + * Get a list of versions of the selected concept set + * + * @summary Get concept set version list + * @since v2.10.0 + * @param id The concept set ID + * @return A list of version information + */ + @GetMapping("/{id}/version/") + public ResponseEntity> getVersions(@PathVariable("id") final int id) { + List versions = versionService.getVersions(VersionType.CONCEPT_SET, id); + List result = versions.stream() + .map(v -> conversionService.convert(v, VersionDTO.class)) + .collect(Collectors.toList()); + return ok(result); + } + + /** + * Get a specific version of a concept set + * + * @summary Get concept set by version + * @since v2.10.0 + * @param id The concept set ID + * @param version The version ID + * @return The concept set for the selected version + */ + @GetMapping("/{id}/version/{version}") + public ResponseEntity getVersionResponse( + @PathVariable("id") final int id, + @PathVariable("version") final int version) { + return ok(getVersion(id, version)); + } + + private ConceptSetVersionFullDTO getVersion(int id, int version) { + checkVersion(id, version, false); + ConceptSetVersion conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version); + return conversionService.convert(conceptSetVersion, ConceptSetVersionFullDTO.class); + } + + /** + * Update a specific version of a selected concept set + * + * @summary Update a concept set version + * @since v2.10.0 + * @param id The concept set ID + * @param version The version ID + * @param updateDTO The version update + * @return The version information + */ + @PutMapping("/{id}/version/{version}") + public ResponseEntity updateVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version, + @RequestBody VersionUpdateDTO updateDTO) { + checkVersion(id, version); + updateDTO.setAssetId(id); + updateDTO.setVersion(version); + ConceptSetVersion updated = versionService.update(VersionType.CONCEPT_SET, updateDTO); + return ok(conversionService.convert(updated, VersionDTO.class)); + } + + /** + * Delete a version of a concept set + * + * @summary Delete a concept set version + * @since v2.10.0 + * @param id The concept ID + * @param version The version ID + */ + @DeleteMapping("/{id}/version/{version}") + public ResponseEntity deleteVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version) { + checkVersion(id, version); + versionService.delete(VersionType.CONCEPT_SET, id, version); + return ok(); + } + + /** + * Create a new asset from a specific version of the selected + * concept set + * + * @summary Create a concept set copy from a specific concept set version + * @since v2.10.0 + * @param id The concept set ID + * @param version The version ID + * @return The concept set copy + */ + @PutMapping("/{id}/version/{version}/createAsset") + @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) + public ResponseEntity copyAssetFromVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version) { + checkVersion(id, version, false); + ConceptSetVersion conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version); + + ConceptSetVersionFullDTO fullDTO = conversionService.convert(conceptSetVersion, ConceptSetVersionFullDTO.class); + ConceptSetDTO conceptSetDTO = fullDTO.getEntityDTO(); + // Reset id so it won't be used during saving + conceptSetDTO.setId(0); + conceptSetDTO.setTags(null); + conceptSetDTO.setName(NameUtils.getNameForCopy(conceptSetDTO.getName(), this::getNamesLike, getConceptSetRepository().findByName(conceptSetDTO.getName()))); + ConceptSetDTO createdDTO = createConceptSet(conceptSetDTO).getBody(); + saveConceptSetItems(createdDTO.getId(), fullDTO.getItems().toArray(new ConceptSetItem[0])); + + return ok(createdDTO); + } + + /** + * Get list of concept sets with their assigned tags + * + * @summary Get concept sets and tag information + * @param requestDTO The tagNameListRequest + * @return A list of concept sets with their assigned tags + */ + @PostMapping("/byTags") + public ResponseEntity> listByTags(@RequestBody TagNameListRequestDTO requestDTO) { + if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { + return ok(Collections.emptyList()); + } + List names = requestDTO.getNames().stream() + .map(name -> name.toLowerCase(Locale.ROOT)) + .collect(Collectors.toList()); + List entities = getConceptSetRepository().findByTags(names); + return ok(listByTags(entities, names, ConceptSetDTO.class)); + } + + private void checkVersion(int id, int version) { + checkVersion(id, version, true); + } + + private void checkVersion(int id, int version, boolean checkOwnerShip) { + Version conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version); + ExceptionUtils.throwNotFoundExceptionIfNull(conceptSetVersion, String.format("There is no concept set version with id = %d.", version)); + + ConceptSet entity = getConceptSetRepository().findById(id).orElse(null); + if (checkOwnerShip) { + checkOwnerOrAdminOrGranted(entity); + } + } + + private ConceptSetVersion saveVersion(int id) { + ConceptSet def = getConceptSetRepository().findById(id).orElse(null); + ConceptSetVersion version = conversionService.convert(def, ConceptSetVersion.class); + + UserEntity user = Objects.nonNull(def.getModifiedBy()) ? def.getModifiedBy() : def.getCreatedBy(); + Date versionDate = Objects.nonNull(def.getModifiedDate()) ? def.getModifiedDate() : def.getCreatedDate(); + version.setCreatedBy(user); + version.setCreatedDate(versionDate); + return versionService.create(VersionType.CONCEPT_SET, version); + } + + /** + * Update the concept set annotation for each concept in concept set ID in the + * WebAPI database. + *

+ * The body has two parts: 1) the elements new concept which added to the + * concept set. 2) the elements concept which remove from concept set. + * + * @param conceptSetId The concept set ID + * @param request An object of 2 Array new annotation and remove annotation + * @return Boolean: true if the save is successful + * @summary Create new or delete concept set annotation items + */ + @PutMapping("/{id}/annotation") + public ResponseEntity saveConceptSetAnnotation( + @PathVariable("id") final int conceptSetId, + @RequestBody SaveConceptSetAnnotationsRequest request) { + removeAnnotations(conceptSetId, request); + if (request.getNewAnnotation() != null && !request.getNewAnnotation().isEmpty()) { + List annotationList = request.getNewAnnotation() + .stream() + .map(newAnnotationData -> { + ConceptSetAnnotation conceptSetAnnotation = new ConceptSetAnnotation(); + conceptSetAnnotation.setConceptSetId(conceptSetId); + try { + AnnotationDetailsDTO annotationDetailsDTO = new AnnotationDetailsDTO(); + annotationDetailsDTO.setId(newAnnotationData.getId()); + annotationDetailsDTO.setConceptId(newAnnotationData.getConceptId()); + annotationDetailsDTO.setSearchData(newAnnotationData.getSearchData()); + conceptSetAnnotation.setAnnotationDetails(mapper.writeValueAsString(annotationDetailsDTO)); + } catch (JsonProcessingException e) { + log.error("Could not serialize Concept Set AnnotationDetailsDTO", e); + throw new RuntimeException(e); + } + conceptSetAnnotation.setVocabularyVersion(newAnnotationData.getVocabularyVersion()); + conceptSetAnnotation.setConceptSetVersion(newAnnotationData.getConceptSetVersion()); + conceptSetAnnotation.setConceptId(newAnnotationData.getConceptId()); + conceptSetAnnotation.setCreatedBy(getCurrentUserEntity()); + conceptSetAnnotation.setCreatedDate(new Date()); + return conceptSetAnnotation; + }).collect(Collectors.toList()); + + this.getConceptSetAnnotationRepository().saveAll(annotationList); + } + + return ok(true); + } + + private void removeAnnotations(int id, SaveConceptSetAnnotationsRequest request) { + if (request.getRemoveAnnotation() != null && !request.getRemoveAnnotation().isEmpty()) { + for (AnnotationDTO annotationDTO : request.getRemoveAnnotation()) { + this.getConceptSetAnnotationRepository().deleteAnnotationByConceptSetIdAndConceptId(id, annotationDTO.getConceptId()); + } + } + } + + @PostMapping("/copy-annotations") + public ResponseEntity copyAnnotations(@RequestBody CopyAnnotationsRequest copyAnnotationsRequest) { + List sourceAnnotations = getConceptSetAnnotationRepository().findByConceptSetId(copyAnnotationsRequest.getSourceConceptSetId()); + List copiedAnnotations= sourceAnnotations.stream() + .map(sourceAnnotation -> copyAnnotation(sourceAnnotation, copyAnnotationsRequest.getSourceConceptSetId(), copyAnnotationsRequest.getTargetConceptSetId())) + .collect(Collectors.toList()); + getConceptSetAnnotationRepository().saveAll(copiedAnnotations); + return ok(); + } + + private ConceptSetAnnotation copyAnnotation(ConceptSetAnnotation sourceConceptSetAnnotation, int sourceConceptSetId, int targetConceptSetId) { + ConceptSetAnnotation targetConceptSetAnnotation = new ConceptSetAnnotation(); + targetConceptSetAnnotation.setConceptSetId(targetConceptSetId); + targetConceptSetAnnotation.setConceptSetVersion(sourceConceptSetAnnotation.getConceptSetVersion()); + targetConceptSetAnnotation.setAnnotationDetails(sourceConceptSetAnnotation.getAnnotationDetails()); + targetConceptSetAnnotation.setConceptId(sourceConceptSetAnnotation.getConceptId()); + targetConceptSetAnnotation.setVocabularyVersion(sourceConceptSetAnnotation.getVocabularyVersion()); + targetConceptSetAnnotation.setCreatedBy(sourceConceptSetAnnotation.getCreatedBy()); + targetConceptSetAnnotation.setCreatedDate(sourceConceptSetAnnotation.getCreatedDate()); + targetConceptSetAnnotation.setModifiedBy(sourceConceptSetAnnotation.getModifiedBy()); + targetConceptSetAnnotation.setModifiedDate(sourceConceptSetAnnotation.getModifiedDate()); + targetConceptSetAnnotation.setCopiedFromConceptSetIds(appendCopiedFromConceptSetId(sourceConceptSetAnnotation.getCopiedFromConceptSetIds(), sourceConceptSetId)); + return targetConceptSetAnnotation; + } + + private String appendCopiedFromConceptSetId(String copiedFromConceptSetIds, int sourceConceptSetId) { + if(copiedFromConceptSetIds == null || copiedFromConceptSetIds.isEmpty()){ + return Integer.toString(sourceConceptSetId); + } + return copiedFromConceptSetIds.concat(",").concat(Integer.toString(sourceConceptSetId)); + } + + @GetMapping("/{id}/annotation") + public ResponseEntity> getConceptSetAnnotation(@PathVariable("id") final int id) { + List annotationList = getConceptSetAnnotationRepository().findByConceptSetId(id); + List result = annotationList.stream() + .map(this::convertAnnotationEntityToDTO) + .collect(Collectors.toList()); + return ok(result); + } + + private AnnotationDTO convertAnnotationEntityToDTO(ConceptSetAnnotation conceptSetAnnotation) { + AnnotationDetailsDTO annotationDetails; + try { + annotationDetails = mapper.readValue(conceptSetAnnotation.getAnnotationDetails(), AnnotationDetailsDTO.class); + } catch (JsonProcessingException e) { + log.error("Could not deserialize Concept Set AnnotationDetailsDTO", e); + throw new RuntimeException(e); + } + + AnnotationDTO annotationDTO = new AnnotationDTO(); + + annotationDTO.setId(conceptSetAnnotation.getId()); + annotationDTO.setConceptId(conceptSetAnnotation.getConceptId()); + + String searchDataJSON = annotationDetails.getSearchData(); + String humanReadableData = searchDataTransformer.convertJsonToReadableFormat(searchDataJSON); + annotationDTO.setSearchData(humanReadableData); + + annotationDTO.setVocabularyVersion(conceptSetAnnotation.getVocabularyVersion()); + annotationDTO.setConceptSetVersion(conceptSetAnnotation.getConceptSetVersion()); + annotationDTO.setCopiedFromConceptSetIds(conceptSetAnnotation.getCopiedFromConceptSetIds()); + annotationDTO.setCreatedBy(conceptSetAnnotation.getCreatedBy() != null ? conceptSetAnnotation.getCreatedBy().getName() : null); + annotationDTO.setCreatedDate(conceptSetAnnotation.getCreatedDate() != null ? conceptSetAnnotation.getCreatedDate().toString() : null); + return annotationDTO; + } + + @DeleteMapping("/{conceptSetId}/annotation/{annotationId}") + public ResponseEntity deleteConceptSetAnnotation( + @PathVariable("conceptSetId") final int conceptSetId, + @PathVariable("annotationId") final int annotationId) { + ConceptSetAnnotation conceptSetAnnotation = getConceptSetAnnotationRepository().findById(annotationId); + if (conceptSetAnnotation != null) { + getConceptSetAnnotationRepository().deleteById(annotationId); + return ok(); + } else { + return notFound(); + } + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/DDLMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/DDLMvcController.java new file mode 100644 index 0000000000..cc3bce3d69 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/DDLMvcController.java @@ -0,0 +1,215 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.apache.commons.lang3.ObjectUtils; +import org.ohdsi.circe.helper.ResourceHelper; +import org.ohdsi.webapi.common.DBMSType; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.service.SqlRenderService; +import org.ohdsi.webapi.sqlrender.SourceStatement; +import org.ohdsi.webapi.sqlrender.TranslatedStatement; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; + +/** + * Spring MVC version of DDLService + * + * Migration Status: Replaces /service/DDLService.java (Jersey) + * Endpoints: 3 GET endpoints with query parameters + * Complexity: Simple - GET operations generating DDL SQL + */ +@RestController +@RequestMapping("/ddl") +public class DDLMvcController extends AbstractMvcController { + + public static final String VOCAB_SCHEMA = "vocab_schema"; + public static final String RESULTS_SCHEMA = "results_schema"; + public static final String CEM_SCHEMA = "cem_results_schema"; + public static final String TEMP_SCHEMA = "oracle_temp_schema"; + + private static final Collection RESULT_DDL_FILE_PATHS = Arrays.asList( + "/ddl/results/cohort.sql", + "/ddl/results/cohort_censor_stats.sql", + "/ddl/results/cohort_inclusion.sql", + "/ddl/results/cohort_inclusion_result.sql", + "/ddl/results/cohort_inclusion_stats.sql", + "/ddl/results/cohort_summary_stats.sql", + "/ddl/results/cohort_cache.sql", + "/ddl/results/cohort_censor_stats_cache.sql", + "/ddl/results/cohort_inclusion_result_cache.sql", + "/ddl/results/cohort_inclusion_stats_cache.sql", + "/ddl/results/cohort_summary_stats_cache.sql", + "/ddl/results/feas_study_inclusion_stats.sql", + "/ddl/results/feas_study_index_stats.sql", + "/ddl/results/feas_study_result.sql", + "/ddl/results/heracles_analysis.sql", + "/ddl/results/heracles_heel_results.sql", + "/ddl/results/heracles_results.sql", + "/ddl/results/heracles_results_dist.sql", + "/ddl/results/heracles_periods.sql", + "/ddl/results/cohort_sample_element.sql", + "/ddl/results/ir_analysis_dist.sql", + "/ddl/results/ir_analysis_result.sql", + "/ddl/results/ir_analysis_strata_stats.sql", + "/ddl/results/ir_strata.sql", + "/ddl/results/cohort_characterizations.sql", + "/ddl/results/pathway_analysis_codes.sql", + "/ddl/results/pathway_analysis_events.sql", + "/ddl/results/pathway_analysis_paths.sql", + "/ddl/results/pathway_analysis_stats.sql" + ); + + private static final String INIT_HERACLES_PERIODS = "/ddl/results/init_heracles_periods.sql"; + + public static final Collection RESULT_INIT_FILE_PATHS = Arrays.asList( + "/ddl/results/init_heracles_analysis.sql", INIT_HERACLES_PERIODS + ); + + public static final Collection HIVE_RESULT_INIT_FILE_PATHS = Arrays.asList( + "/ddl/results/init_hive_heracles_analysis.sql", INIT_HERACLES_PERIODS + ); + + public static final Collection INIT_CONCEPT_HIERARCHY_FILE_PATHS = Arrays.asList( + "/ddl/results/concept_hierarchy.sql", + "/ddl/results/init_concept_hierarchy.sql" + ); + + private static final Collection RESULT_INDEX_FILE_PATHS = Arrays.asList( + "/ddl/results/create_index.sql", + "/ddl/results/pathway_analysis_events_indexes.sql" + ); + + private static final Collection CEMRESULT_DDL_FILE_PATHS = Arrays.asList( + "/ddl/cemresults/nc_results.sql" + ); + + public static final Collection CEMRESULT_INIT_FILE_PATHS = Collections.emptyList(); + private static final Collection CEMRESULT_INDEX_FILE_PATHS = Collections.emptyList(); + + private static final Collection ACHILLES_DDL_FILE_PATHS = Arrays.asList( + "/ddl/achilles/achilles_result_concept_count.sql" + ); + + private static final Collection DBMS_NO_INDEXES = Arrays.asList("redshift", "impala", "netezza", "spark"); + + /** + * Get DDL for results schema + * + * Jersey: GET /WebAPI/ddl/results + * Spring MVC: GET /WebAPI/v2/ddl/results + */ + @GetMapping(value = "/results", produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity generateResultSQL( + @RequestParam(value = "dialect", required = false) String dialect, + @RequestParam(value = "vocabSchema", defaultValue = "vocab") String vocabSchema, + @RequestParam(value = "schema", defaultValue = "results") String resultSchema, + @RequestParam(value = "initConceptHierarchy", defaultValue = "true") Boolean initConceptHierarchy, + @RequestParam(value = "tempSchema", required = false) String tempSchema) { + + Collection resultDDLFilePaths = new ArrayList<>(RESULT_DDL_FILE_PATHS); + + if (initConceptHierarchy) { + resultDDLFilePaths.addAll(INIT_CONCEPT_HIERARCHY_FILE_PATHS); + } + String oracleTempSchema = ObjectUtils.firstNonNull(tempSchema, resultSchema); + Map params = new HashMap<>() {{ + put(VOCAB_SCHEMA, vocabSchema); + put(RESULTS_SCHEMA, resultSchema); + put(TEMP_SCHEMA, oracleTempSchema); + }}; + + String sql = generateSQL(dialect, params, resultDDLFilePaths, getResultInitFilePaths(dialect), RESULT_INDEX_FILE_PATHS); + return ok(sql); + } + + /** + * Get DDL for Common Evidence Model results schema + * + * Jersey: GET /WebAPI/ddl/cemresults + * Spring MVC: GET /WebAPI/v2/ddl/cemresults + */ + @GetMapping(value = "/cemresults", produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity generateCemResultSQL( + @RequestParam(value = "dialect", required = false) String dialect, + @RequestParam(value = "schema", defaultValue = "cemresults") String schema) { + + Map params = new HashMap<>() {{ + put(CEM_SCHEMA, schema); + }}; + + String sql = generateSQL(dialect, params, CEMRESULT_DDL_FILE_PATHS, CEMRESULT_INIT_FILE_PATHS, CEMRESULT_INDEX_FILE_PATHS); + return ok(sql); + } + + /** + * Get DDL for Achilles results tables + * + * Jersey: GET /WebAPI/ddl/achilles + * Spring MVC: GET /WebAPI/v2/ddl/achilles + */ + @GetMapping(value = "/achilles", produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity generateAchillesSQL( + @RequestParam(value = "dialect", required = false) String dialect, + @RequestParam(value = "vocabSchema", defaultValue = "vocab") String vocabSchema, + @RequestParam(value = "schema", defaultValue = "results") String resultSchema) { + + final Collection achillesDDLFilePaths = new ArrayList<>(ACHILLES_DDL_FILE_PATHS); + + Map params = new HashMap<>() {{ + put(VOCAB_SCHEMA, vocabSchema); + put(RESULTS_SCHEMA, resultSchema); + }}; + + String sql = generateSQL(dialect, params, achillesDDLFilePaths, Collections.emptyList(), Collections.emptyList()); + return ok(sql); + } + + // Helper methods (same logic as original) + + private Collection getResultInitFilePaths(String dialect) { + if (Objects.equals(DBMSType.HIVE.getOhdsiDB(), dialect)) { + return HIVE_RESULT_INIT_FILE_PATHS; + } else { + return RESULT_INIT_FILE_PATHS; + } + } + + private String generateSQL(String dialect, Map params, Collection filePaths, + Collection initFilePaths, Collection indexFilePaths) { + StringBuilder sqlBuilder = new StringBuilder(); + for (String fileName : filePaths) { + sqlBuilder.append("\n").append(ResourceHelper.GetResourceAsString(fileName)); + } + + for (String fileName : initFilePaths) { + sqlBuilder.append("\n").append(ResourceHelper.GetResourceAsString(fileName)); + } + + if (dialect == null || DBMS_NO_INDEXES.stream().noneMatch(dbms -> dbms.equals(dialect.toLowerCase()))) { + for (String fileName : indexFilePaths) { + sqlBuilder.append("\n").append(ResourceHelper.GetResourceAsString(fileName)); + } + } + String result = sqlBuilder.toString(); + if (dialect != null) { + result = translateSqlFile(result, dialect, params); + } + return result.replaceAll(";", ";\n"); + } + + private String translateSqlFile(String sql, String dialect, Map params) { + SourceStatement statement = new SourceStatement(); + statement.setTargetDialect(dialect.toLowerCase()); + statement.setOracleTempSchema(params.get(TEMP_SCHEMA)); + statement.setSql(sql); + statement.getParameters().putAll(params); + + TranslatedStatement translatedStatement = SqlRenderService.translateSQL(statement); + return translatedStatement.getTargetSQL(); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/FeasibilityMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/FeasibilityMvcController.java new file mode 100644 index 0000000000..a47bca7686 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/FeasibilityMvcController.java @@ -0,0 +1,256 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.ohdsi.webapi.feasibility.FeasibilityReport; +import org.ohdsi.webapi.job.JobExecutionResource; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.service.FeasibilityService; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * Spring MVC version of FeasibilityService + * + * Migration Status: Replaces /service/FeasibilityService.java (Jersey) + * Endpoints: 9 endpoints (4 GET, 2 PUT, 2 DELETE, 1 GET with generation) + * Complexity: High - complex business logic, all endpoints marked as deprecated + */ +@RestController +@RequestMapping("/feasibility") +public class FeasibilityMvcController extends AbstractMvcController { + + private final FeasibilityService feasibilityService; + + public FeasibilityMvcController(FeasibilityService feasibilityService) { + this.feasibilityService = feasibilityService; + } + + /** + * DO NOT USE + * + * Jersey: GET /WebAPI/feasibility/ + * Spring MVC: GET /WebAPI/v2/feasibility + * + * @summary DO NOT USE + * @deprecated + * @return List + */ + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @Deprecated + public ResponseEntity> getFeasibilityStudyList() { + List studies = feasibilityService.getFeasibilityStudyList(); + return ok(studies); + } + + /** + * Creates the feasibility study + * + * Jersey: PUT /WebAPI/feasibility/ + * Spring MVC: PUT /WebAPI/v2/feasibility + * + * @summary DO NOT USE + * @deprecated + * @param study The feasibility study + * @return Feasibility study + */ + @PutMapping( + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + @Transactional + @Deprecated + public ResponseEntity createStudy( + @RequestBody FeasibilityService.FeasibilityStudyDTO study) { + FeasibilityService.FeasibilityStudyDTO createdStudy = feasibilityService.createStudy(study); + return ok(createdStudy); + } + + /** + * Get the feasibility study by ID + * + * Jersey: GET /WebAPI/feasibility/{id} + * Spring MVC: GET /WebAPI/v2/feasibility/{id} + * + * @summary DO NOT USE + * @deprecated + * @param id The study ID + * @return Feasibility study + */ + @GetMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + @Transactional(readOnly = true) + @Deprecated + public ResponseEntity getStudy(@PathVariable("id") int id) { + FeasibilityService.FeasibilityStudyDTO study = feasibilityService.getStudy(id); + return ok(study); + } + + /** + * Update the feasibility study + * + * Jersey: PUT /WebAPI/feasibility/{id} + * Spring MVC: PUT /WebAPI/v2/feasibility/{id} + * + * @summary DO NOT USE + * @deprecated + * @param id The study ID + * @param study The study information + * @return The updated study information + */ + @PutMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @Transactional + @Deprecated + public ResponseEntity saveStudy( + @PathVariable("id") int id, + @RequestBody FeasibilityService.FeasibilityStudyDTO study) { + FeasibilityService.FeasibilityStudyDTO savedStudy = feasibilityService.saveStudy(id, study); + return ok(savedStudy); + } + + /** + * Generate the feasibility study + * + * Jersey: GET /WebAPI/feasibility/{study_id}/generate/{sourceKey} + * Spring MVC: GET /WebAPI/v2/feasibility/{study_id}/generate/{sourceKey} + * + * @summary DO NOT USE + * @deprecated + * @param study_id The study ID + * @param sourceKey The source key + * @return JobExecutionResource + */ + @GetMapping( + value = "/{study_id}/generate/{sourceKey}", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + @Deprecated + public ResponseEntity performStudy( + @PathVariable("study_id") int study_id, + @PathVariable("sourceKey") String sourceKey) { + JobExecutionResource jobExecution = feasibilityService.performStudy(study_id, sourceKey); + return ok(jobExecution); + } + + /** + * Get simulation information + * + * Jersey: GET /WebAPI/feasibility/{id}/info + * Spring MVC: GET /WebAPI/v2/feasibility/{id}/info + * + * @summary DO NOT USE + * @deprecated + * @param id The study ID + * @return List + */ + @GetMapping( + value = "/{id}/info", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @Transactional(readOnly = true) + @Deprecated + public ResponseEntity> getSimulationInfo(@PathVariable("id") int id) { + List info = feasibilityService.getSimulationInfo(id); + return ok(info); + } + + /** + * Get simulation report + * + * Jersey: GET /WebAPI/feasibility/{id}/report/{sourceKey} + * Spring MVC: GET /WebAPI/v2/feasibility/{id}/report/{sourceKey} + * + * @summary DO NOT USE + * @deprecated + * @param id The study ID + * @param sourceKey The source key + * @return FeasibilityReport + */ + @GetMapping( + value = "/{id}/report/{sourceKey}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @Transactional + @Deprecated + public ResponseEntity getSimulationReport( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + FeasibilityReport report = feasibilityService.getSimulationReport(id, sourceKey); + return ok(report); + } + + /** + * Copies the specified cohort definition + * + * Jersey: GET /WebAPI/feasibility/{id}/copy + * Spring MVC: GET /WebAPI/v2/feasibility/{id}/copy + * + * @summary DO NOT USE + * @deprecated + * @param id - the Cohort Definition ID to copy + * @return the copied feasibility study as a FeasibilityStudyDTO + */ + @GetMapping( + value = "/{id}/copy", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @jakarta.transaction.Transactional + @Deprecated + public ResponseEntity copy(@PathVariable("id") int id) { + FeasibilityService.FeasibilityStudyDTO copiedStudy = feasibilityService.copy(id); + return ok(copiedStudy); + } + + /** + * Deletes the specified feasibility study + * + * Jersey: DELETE /WebAPI/feasibility/{id} + * Spring MVC: DELETE /WebAPI/v2/feasibility/{id} + * + * @summary DO NOT USE + * @deprecated + * @param id The study ID + */ + @DeleteMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @Deprecated + public ResponseEntity delete(@PathVariable("id") int id) { + feasibilityService.delete(id); + return ok(); + } + + /** + * Deletes the specified study for the selected source + * + * Jersey: DELETE /WebAPI/feasibility/{id}/info/{sourceKey} + * Spring MVC: DELETE /WebAPI/v2/feasibility/{id}/info/{sourceKey} + * + * @summary DO NOT USE + * @deprecated + * @param id The study ID + * @param sourceKey The source key + */ + @DeleteMapping( + value = "/{id}/info/{sourceKey}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @Transactional + @Deprecated + public ResponseEntity deleteInfo( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { + feasibilityService.deleteInfo(id, sourceKey); + return ok(); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/I18nMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/I18nMvcController.java new file mode 100644 index 0000000000..0599cdc79e --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/I18nMvcController.java @@ -0,0 +1,83 @@ +package org.ohdsi.webapi.mvc.controller; + +import com.google.common.collect.ImmutableList; +import org.ohdsi.webapi.i18n.I18nService; +import org.ohdsi.webapi.i18n.LocaleDTO; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** + * Spring MVC version of I18nController + * + * Migration Status: Replaces /i18n/I18nController.java (Jersey) + * Endpoints: 2 GET endpoints + * Complexity: Simple - i18n resource handling + * + * Note: Original used @Controller with JAX-RS annotations (mixed framework) + * This is pure Spring MVC implementation + */ +@RestController +@RequestMapping("/i18n") +public class I18nMvcController extends AbstractMvcController { + + @Value("${i18n.enabled}") + private boolean i18nEnabled = true; + + @Value("${i18n.defaultLocale}") + private String defaultLocale = "en"; + + @Autowired + private I18nService i18nService; + + /** + * Get i18n resources for current locale + * + * Jersey: GET /WebAPI/i18n/ + * Spring MVC: GET /WebAPI/v2/i18n/ + * + * Note: Locale is resolved by LocaleInterceptor and stored in LocaleContextHolder + */ + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getResources() { + // Get locale from LocaleContextHolder (set by LocaleInterceptor) + Locale locale = LocaleContextHolder.getLocale(); + + if (!this.i18nEnabled || locale == null || !isLocaleSupported(locale.getLanguage())) { + locale = Locale.forLanguageTag(defaultLocale); + } + + String messages = i18nService.getLocaleResource(locale); + return ok(messages); + } + + /** + * Get list of available locales + * + * Jersey: GET /WebAPI/i18n/locales + * Spring MVC: GET /WebAPI/v2/i18n/locales + */ + @GetMapping(value = "/locales", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getAvailableLocales() { + if (this.i18nEnabled) { + return ok(i18nService.getAvailableLocales()); + } + + // if i18n is disabled, then return only default locale + return ok(ImmutableList.of(new LocaleDTO(this.defaultLocale, null, true))); + } + + private boolean isLocaleSupported(String code) { + return i18nService.getAvailableLocales().stream().anyMatch(l -> Objects.equals(code, l.getCode())); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/InfoMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/InfoMvcController.java new file mode 100644 index 0000000000..5fce562671 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/InfoMvcController.java @@ -0,0 +1,44 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.ohdsi.webapi.info.Info; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.ohdsi.webapi.info.InfoService; + +/** + * Spring MVC version of InfoService + * + * Migration Status: Replaces /info/InfoService.java (Jersey) + * Endpoints: 1 GET endpoint + * Complexity: Simple - read-only, no parameters + * + * Note: This controller delegates to the existing Jersey InfoService to get the Info object. + * This allows us to avoid duplicate dependency injection issues with BuildProperties/BuildInfo + * while still providing the same endpoint via Spring MVC. + */ +@RestController +@RequestMapping("/info") +public class InfoMvcController extends AbstractMvcController { + + @Autowired + private InfoService infoService; + + /** + * Get info about the WebAPI instance + * + * Jersey: GET /WebAPI/info/ + * Spring MVC: GET /WebAPI/v2/info/ + */ + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getInfo() { + // Delegate to the existing InfoService which is already configured + // with BuildProperties and BuildInfo + Info info = infoService.getInfo(); + return ok(info); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/JobMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/JobMvcController.java new file mode 100644 index 0000000000..1f5a24cc59 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/JobMvcController.java @@ -0,0 +1,156 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.ohdsi.webapi.job.JobExecutionResource; +import org.ohdsi.webapi.job.JobInstanceResource; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.springframework.batch.core.launch.NoSuchJobException; +import org.springframework.data.domain.Page; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * Spring MVC version of JobService + * + * Migration Status: Replaces /service/JobService.java (Jersey) + * Endpoints: 6 GET endpoints + * Complexity: Simple - mostly delegation to service layer + */ +@RestController +@RequestMapping("/job") +public class JobMvcController extends AbstractMvcController { + + private final org.ohdsi.webapi.service.JobService jobService; + + public JobMvcController(org.ohdsi.webapi.service.JobService jobService) { + this.jobService = jobService; + } + + /** + * Get the job information by job ID + * + * Jersey: GET /WebAPI/job/{jobId} + * Spring MVC: GET /WebAPI/v2/job/{jobId} + * + * @summary Get job by ID + * @param jobId The job ID + * @return The job information + */ + @GetMapping(value = "/{jobId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity findJob(@PathVariable("jobId") Long jobId) { + JobInstanceResource job = jobService.findJob(jobId); + if (job == null) { + return notFound(); + } + return ok(job); + } + + /** + * Get the job execution information by job type and name + * + * Jersey: GET /WebAPI/job/type/{jobType}/name/{jobName} + * Spring MVC: GET /WebAPI/v2/job/type/{jobType}/name/{jobName} + * + * @summary Get job by name and type + * @param jobName The job name + * @param jobType The job type + * @return JobExecutionResource + */ + @GetMapping(value = "/type/{jobType}/name/{jobName}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity findJobByName( + @PathVariable("jobName") String jobName, + @PathVariable("jobType") String jobType) { + JobExecutionResource jobExecution = jobService.findJobByName(jobName, jobType); + if (jobExecution == null) { + return notFound(); + } + return ok(jobExecution); + } + + /** + * Get the job execution information by execution ID and job ID + * + * Jersey: GET /WebAPI/job/{jobId}/execution/{executionId} + * Spring MVC: GET /WebAPI/v2/job/{jobId}/execution/{executionId} + * + * @summary Get job by job ID and execution ID + * @param jobId The job ID + * @param executionId The execution ID + * @return JobExecutionResource + */ + @GetMapping(value = "/{jobId}/execution/{executionId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity findJobExecutionByJobId( + @PathVariable("jobId") Long jobId, + @PathVariable("executionId") Long executionId) { + JobExecutionResource jobExecution = jobService.findJobExecution(jobId, executionId); + if (jobExecution == null) { + return notFound(); + } + return ok(jobExecution); + } + + /** + * Find job execution by execution ID + * + * Jersey: GET /WebAPI/job/execution/{executionId} + * Spring MVC: GET /WebAPI/v2/job/execution/{executionId} + * + * @summary Get job by execution ID + * @param executionId The job execution ID + * @return JobExecutionResource + */ + @GetMapping(value = "/execution/{executionId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity findJobExecution(@PathVariable("executionId") Long executionId) { + JobExecutionResource jobExecution = jobService.findJobExecution(executionId); + if (jobExecution == null) { + return notFound(); + } + return ok(jobExecution); + } + + /** + * Get job names (unique names). Note: this path (GET /job) should really + * return pages of job instances. This could be implemented should the need + * arise. + * + * Jersey: GET /WebAPI/job + * Spring MVC: GET /WebAPI/v2/job + * + * @summary Get list of jobs + * @return A list of jobs + */ + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> findJobNames() { + List jobNames = jobService.findJobNames(); + return ok(jobNames); + } + + /** + * Return a paged collection of job executions. Filter for a given job. + * Returned in pages. + * + * Jersey: GET /WebAPI/job/execution?jobName={jobName}&pageIndex={pageIndex}&pageSize={pageSize}&comprehensivePage={comprehensivePage} + * Spring MVC: GET /WebAPI/v2/job/execution?jobName={jobName}&pageIndex={pageIndex}&pageSize={pageSize}&comprehensivePage={comprehensivePage} + * + * @summary Get job executions with filters + * @param jobName name of the job + * @param pageIndex start index for the job execution list + * @param pageSize page size for the list + * @param comprehensivePage boolean if true returns a comprehensive resultset + * as a page (i.e. pageRequest(0,resultset.size())) + * @return collection of JobExecutionInfo + * @throws NoSuchJobException + */ + @GetMapping(value = "/execution", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> list( + @RequestParam(value = "jobName", required = false) String jobName, + @RequestParam(value = "pageIndex", defaultValue = "0") Integer pageIndex, + @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize, + @RequestParam(value = "comprehensivePage", required = false, defaultValue = "false") boolean comprehensivePage) + throws NoSuchJobException { + Page page = jobService.list(jobName, pageIndex, pageSize, comprehensivePage); + return ok(page); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/PermissionMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/PermissionMvcController.java new file mode 100644 index 0000000000..58586404ec --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/PermissionMvcController.java @@ -0,0 +1,211 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.security.AccessType; +import org.ohdsi.webapi.security.PermissionService; +import org.ohdsi.webapi.security.dto.AccessRequestDTO; +import org.ohdsi.webapi.security.dto.RoleDTO; +import org.ohdsi.webapi.security.model.EntityType; +import org.ohdsi.webapi.service.UserService; +import org.ohdsi.webapi.shiro.Entities.PermissionEntity; +import org.ohdsi.webapi.shiro.PermissionManager; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Spring MVC version of PermissionController + * + * Migration Status: Replaces /security/PermissionController.java (Jersey) + * Endpoints: 6 endpoints (3 GET, 1 POST, 1 DELETE, 1 GET with query) + * Complexity: Medium - permission management and authorization logic + */ +@RestController +@RequestMapping("/permission") +@Transactional +public class PermissionMvcController extends AbstractMvcController { + + private final PermissionService permissionService; + private final PermissionManager permissionManager; + private final ConversionService conversionService; + + public PermissionMvcController( + PermissionService permissionService, + PermissionManager permissionManager, + @Qualifier("conversionService") ConversionService conversionService) { + this.permissionService = permissionService; + this.permissionManager = permissionManager; + this.conversionService = conversionService; + } + + /** + * Get the list of permissions for a user + * + * Jersey: GET /WebAPI/permission + * Spring MVC: GET /WebAPI/v2/permission + * + * @return A list of permissions + */ + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getPermissions() { + Iterable permissionEntities = permissionManager.getPermissions(); + List permissions = StreamSupport.stream(permissionEntities.spliterator(), false) + .map(UserService.Permission::new) + .collect(Collectors.toList()); + return ok(permissions); + } + + /** + * Get the roles matching the roleSearch value + * + * Jersey: GET /WebAPI/permission/access/suggest?roleSearch={roleSearch} + * Spring MVC: GET /WebAPI/v2/permission/access/suggest?roleSearch={roleSearch} + * + * @summary Role search + * @param roleSearch The role to search + * @return The list of roles + */ + @GetMapping( + value = "/access/suggest", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity> listAccessesForEntity(@RequestParam("roleSearch") String roleSearch) { + List roles = permissionService.suggestRoles(roleSearch); + List roleDTOs = roles.stream() + .map(re -> conversionService.convert(re, RoleDTO.class)) + .collect(Collectors.toList()); + return ok(roleDTOs); + } + + /** + * Get roles that have a permission type (READ/WRITE) to entity + * + * Jersey: GET /WebAPI/permission/access/{entityType}/{entityId}/{permType} + * Spring MVC: GET /WebAPI/v2/permission/access/{entityType}/{entityId}/{permType} + * + * @summary Get roles that have a specific permission (READ/WRITE) for the entity + * @param entityType The entity type + * @param entityId The entity ID + * @param permType The permission type + * @return The list of permissions for the permission type + * @throws Exception + */ + @GetMapping( + value = "/access/{entityType}/{entityId}/{permType}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity> listAccessesForEntityByPermType( + @PathVariable("entityType") EntityType entityType, + @PathVariable("entityId") Integer entityId, + @PathVariable("permType") AccessType permType) throws Exception { + permissionService.checkCommonEntityOwnership(entityType, entityId); + var permissionTemplates = permissionService.getTemplatesForType(entityType, permType).keySet(); + + List permissions = permissionTemplates.stream() + .map(pt -> permissionService.getPermission(pt, entityId)) + .collect(Collectors.toList()); + + List roles = permissionService.finaAllRolesHavingPermissions(permissions); + + List roleDTOs = roles.stream() + .map(re -> conversionService.convert(re, RoleDTO.class)) + .collect(Collectors.toList()); + return ok(roleDTOs); + } + + /** + * Get roles that have a permission type (READ/WRITE) to entity + * + * Jersey: GET /WebAPI/permission/access/{entityType}/{entityId} + * Spring MVC: GET /WebAPI/v2/permission/access/{entityType}/{entityId} + * + * @summary Get roles that have a specific permission (READ/WRITE) for the entity + * @param entityType The entity type + * @param entityId The entity ID + * @return The list of permissions for the permission type + * @throws Exception + */ + @GetMapping( + value = "/access/{entityType}/{entityId}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity> listAccessesForEntity( + @PathVariable("entityType") EntityType entityType, + @PathVariable("entityId") Integer entityId) throws Exception { + return listAccessesForEntityByPermType(entityType, entityId, AccessType.WRITE); + } + + /** + * Grant group of permissions (READ / WRITE / ...) for the specified entity to the given role. + * Only owner of the entity can do that. + * + * Jersey: POST /WebAPI/permission/access/{entityType}/{entityId}/role/{roleId} + * Spring MVC: POST /WebAPI/v2/permission/access/{entityType}/{entityId}/role/{roleId} + * + * @summary Grant permissions + * @param entityType The entity type + * @param entityId The entity ID + * @param roleId The role ID + * @param accessRequestDTO The access request object + * @throws Exception + */ + @PostMapping( + value = "/access/{entityType}/{entityId}/role/{roleId}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity grantEntityPermissionsForRole( + @PathVariable("entityType") EntityType entityType, + @PathVariable("entityId") Integer entityId, + @PathVariable("roleId") Long roleId, + @RequestBody AccessRequestDTO accessRequestDTO) throws Exception { + permissionService.checkCommonEntityOwnership(entityType, entityId); + + var permissionTemplates = permissionService.getTemplatesForType(entityType, accessRequestDTO.getAccessType()); + + org.ohdsi.webapi.shiro.Entities.RoleEntity role = permissionManager.getRole(roleId); + permissionManager.addPermissionsFromTemplate(role, permissionTemplates, entityId.toString()); + + return ok(); + } + + /** + * Remove group of permissions for the specified entity to the given role. + * + * Jersey: DELETE /WebAPI/permission/access/{entityType}/{entityId}/role/{roleId} + * Spring MVC: DELETE /WebAPI/v2/permission/access/{entityType}/{entityId}/role/{roleId} + * + * @summary Remove permissions + * @param entityType The entity type + * @param entityId The entity ID + * @param roleId The role ID + * @param accessRequestDTO The access request object + * @throws Exception + */ + @DeleteMapping( + value = "/access/{entityType}/{entityId}/role/{roleId}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity revokeEntityPermissionsFromRole( + @PathVariable("entityType") EntityType entityType, + @PathVariable("entityId") Integer entityId, + @PathVariable("roleId") Long roleId, + @RequestBody AccessRequestDTO accessRequestDTO) throws Exception { + permissionService.checkCommonEntityOwnership(entityType, entityId); + var permissionTemplates = permissionService.getTemplatesForType(entityType, accessRequestDTO.getAccessType()); + permissionService.removePermissionsFromRole(permissionTemplates, entityId, roleId); + + return ok(); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/SSOMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/SSOMvcController.java new file mode 100644 index 0000000000..9be94b134e --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/SSOMvcController.java @@ -0,0 +1,81 @@ +package org.ohdsi.webapi.mvc.controller; + +import com.google.common.net.HttpHeaders; +import org.apache.commons.io.IOUtils; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.pac4j.core.context.HttpConstants; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Spring MVC version of SSOController + * + * Migration Status: Replaces /security/SSOController.java (Jersey) + * Endpoints: 2 GET endpoints + * Complexity: Low - SAML metadata and logout redirect + */ +@RestController +@RequestMapping("/saml") +public class SSOMvcController extends AbstractMvcController { + + @Value("${security.saml.metadataLocation}") + private String metadataLocation; + + @Value("${security.saml.sloUrl}") + private String sloUri; + + @Value("${security.origin}") + private String origin; + + /** + * Get the SAML metadata + * + * Jersey: GET /WebAPI/saml/saml-metadata + * Spring MVC: GET /WebAPI/v2/saml/saml-metadata + * + * @summary Get metadata + * @param response The response context + * @throws IOException + */ + @GetMapping(value = "/saml-metadata") + public void samlMetadata(HttpServletResponse response) throws IOException { + ClassPathResource resource = new ClassPathResource(metadataLocation); + final InputStream is = resource.getInputStream(); + response.setContentType(MediaType.APPLICATION_XML_VALUE); + response.setHeader(HttpHeaders.CONTENT_TYPE, "application/xml"); + response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + response.setHeader(HttpHeaders.PRAGMA, "no-cache"); + response.setHeader(HttpHeaders.EXPIRES, "0"); + IOUtils.copy(is, response.getOutputStream()); + response.flushBuffer(); + } + + /** + * Log out of the service + * + * Jersey: GET /WebAPI/saml/slo + * Spring MVC: GET /WebAPI/v2/saml/slo + * + * @summary Log out + * @return Response + * @throws URISyntaxException + */ + @GetMapping(value = "/slo") + public ResponseEntity logout() throws URISyntaxException { + return ResponseEntity.status(HttpConstants.TEMPORARY_REDIRECT) + .header(HttpConstants.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, this.origin) + .location(new URI(sloUri)) + .build(); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/SourceMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/SourceMvcController.java new file mode 100644 index 0000000000..712248e2de --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/SourceMvcController.java @@ -0,0 +1,398 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.ohdsi.webapi.arachne.logging.event.AddDataSourceEvent; +import org.ohdsi.webapi.arachne.logging.event.ChangeDataSourceEvent; +import org.ohdsi.webapi.arachne.logging.event.DeleteDataSourceEvent; +import org.ohdsi.webapi.common.DBMSType; +import org.ohdsi.webapi.exception.SourceDuplicateKeyException; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.service.VocabularyService; +import org.ohdsi.webapi.shiro.Entities.UserEntity; +import org.ohdsi.webapi.shiro.Entities.UserRepository; +import org.ohdsi.webapi.source.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import jakarta.persistence.PersistenceException; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Spring MVC version of SourceController + * + * Migration Status: Replaces /source/SourceController.java (Jersey) + * Endpoints: 10 endpoints (3 GET, 2 POST, 1 PUT, 1 DELETE) + * Special: Multipart file upload endpoints for keyfile handling + */ +@RestController +@RequestMapping("/source") +@Transactional +public class SourceMvcController extends AbstractMvcController { + + public static final String SECURE_MODE_ERROR = "This feature requires the administrator to enable security for the application"; + + @Autowired + private ApplicationEventPublisher publisher; + + @Autowired + private VocabularyService vocabularyService; + + @Autowired + private SourceService sourceService; + + @Autowired + private SourceRepository sourceRepository; + + @Autowired + private SourceDaimonRepository sourceDaimonRepository; + + @Autowired + private GenericConversionService conversionService; + + @Autowired + private UserRepository userRepository; + + @Value("#{!'${security.provider}'.equals('DisabledSecurity')}") + private boolean securityEnabled; + + protected UserEntity getCurrentUserEntity() { + return userRepository.findByLogin(security.getSubject()); + } + + /** + * Gets the list of all Sources in WebAPI database. Sources with a non-null + * deleted_date are not returned (ie: these are soft deleted) + * + * @summary Get Sources + * @return A list of all CDM sources with the ID, name, SQL dialect, and key + * for each source. The {sourceKey} is used in other WebAPI endpoints to + * identify CDMs. + */ + @GetMapping(value = "/sources", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getSources() { + return ok(sourceService.getSources().stream().map(SourceInfo::new).collect(Collectors.toList())); + } + + /** + * Refresh cached CDM database metadata + * + * @summary Refresh Sources + * @return A list of all CDM sources with the ID, name, SQL dialect, and key + * for each source (same as the 'sources' endpoint) after refreshing the cached sourced data. + */ + @GetMapping(value = "/refresh", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> refreshSources() { + sourceService.invalidateCache(); + vocabularyService.clearVocabularyInfoCache(); + sourceService.ensureSourceEncrypted(); + return getSources(); + } + + /** + * Get the priority vocabulary source. + * + * WebAPI designates one CDM vocabulary as the priority vocabulary to be used for vocabulary searches in Atlas. + * + * @summary Get Priority Vocabulary Source + * @return The CDM metadata for the priority vocabulary. + */ + @GetMapping(value = "/priorityVocabulary", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getPriorityVocabularySourceInfo() { + return ok(sourceService.getPriorityVocabularySourceInfo()); + } + + /** + * Get source by key + * @summary Get Source By Key + * @param sourceKey + * @return Metadata for a single Source that matches the sourceKey. + */ + @GetMapping(value = "/{key}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getSource(@PathVariable("key") final String sourceKey) { + return ok(sourceRepository.findBySourceKey(sourceKey).getSourceInfo()); + } + + /** + * Get Source Details + * + * Source Details contains connection-specific information like JDBC url and authentication information. + * + * @summary Get Source Details + * @param sourceId + * @return + */ + @GetMapping(value = "/details/{sourceId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getSourceDetails(@PathVariable("sourceId") Integer sourceId) { + if (!securityEnabled) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, SECURE_MODE_ERROR); + } + Source source = sourceRepository.findBySourceId(sourceId); + return ok(new SourceDetails(source)); + } + + /** + * Create a Source + * + * @summary Create Source + * @param keyfile the keyfile + * @param source contains the source information (name, key, etc) + * @return a new SourceInfo for the created source + * @throws Exception + */ + @PostMapping(value = "", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + public ResponseEntity createSource( + @RequestPart(value = "keyfile", required = false) MultipartFile keyfile, + @RequestPart("source") SourceRequest source) throws Exception { + if (!securityEnabled) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, SECURE_MODE_ERROR); + } + Source sourceByKey = sourceRepository.findBySourceKey(source.getKey()); + if (Objects.nonNull(sourceByKey)) { + throw new SourceDuplicateKeyException("The source key has been already used."); + } + Source sourceEntity = conversionService.convert(source, Source.class); + if (sourceEntity.getDaimons() != null) { + // First source should get priority = 1 + Iterable sources = sourceRepository.findAll(); + sourceEntity.getDaimons() + .stream() + .filter(sd -> sd.getPriority() <= 0) + .filter(sd -> { + boolean accept = true; + // Check if source daimon of given type with priority > 0 already exists in other sources + for (Source innerSource : sources) { + accept = !innerSource.getDaimons() + .stream() + .anyMatch(innerDaimon -> innerDaimon.getPriority() > 0 + && innerDaimon.getDaimonType().equals(sd.getDaimonType())); + if (!accept) { + break; + } + } + return accept; + }) + .forEach(sd -> sd.setPriority(1)); + } + Source original = new Source(); + original.setSourceDialect(sourceEntity.getSourceDialect()); + setKeyfileData(sourceEntity, original, keyfile); + sourceEntity.setCreatedBy(getCurrentUserEntity()); + sourceEntity.setCreatedDate(new Date()); + try { + Source saved = sourceRepository.saveAndFlush(sourceEntity); + sourceService.invalidateCache(); + SourceInfo sourceInfo = new SourceInfo(saved); + publisher.publishEvent(new AddDataSourceEvent(this, sourceEntity.getSourceId(), sourceEntity.getSourceName())); + return ok(sourceInfo); + } catch (PersistenceException ex) { + throw new SourceDuplicateKeyException("You cannot use this Source Key, please use different one"); + } + } + + /** + * Updates a Source with the provided details from multiple files + * + * @summary Update Source + * @param sourceId + * @param keyfile the keyfile + * @param source contains the source information (name, key, etc) + * @return the updated SourceInfo for the source + * @throws Exception + */ + @PutMapping(value = "/{sourceId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + public ResponseEntity updateSource( + @PathVariable("sourceId") Integer sourceId, + @RequestPart(value = "keyfile", required = false) MultipartFile keyfile, + @RequestPart("source") SourceRequest source) throws IOException { + if (!securityEnabled) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, SECURE_MODE_ERROR); + } + Source updated = conversionService.convert(source, Source.class); + Source existingSource = sourceRepository.findBySourceId(sourceId); + if (existingSource != null) { + updated.setSourceId(sourceId); + updated.setSourceKey(existingSource.getSourceKey()); + if (StringUtils.isBlank(updated.getUsername()) || + Objects.equals(updated.getUsername().trim(), Source.MASQUERADED_USERNAME)) { + updated.setUsername(existingSource.getUsername()); + } + if (StringUtils.isBlank(updated.getPassword()) || + Objects.equals(updated.getPassword().trim(), Source.MASQUERADED_PASSWORD)) { + updated.setPassword(existingSource.getPassword()); + } + setKeyfileData(updated, existingSource, keyfile); + transformIfRequired(updated); + if (source.isCheckConnection() == null) { + updated.setCheckConnection(existingSource.isCheckConnection()); + } + updated.setModifiedBy(getCurrentUserEntity()); + updated.setModifiedDate(new Date()); + + reuseDeletedDaimons(updated, existingSource); + + List removed = existingSource.getDaimons().stream() + .filter(d -> !updated.getDaimons().contains(d)) + .collect(Collectors.toList()); + // Delete MUST be called after fetching user or source data to prevent autoflush (see DefaultPersistEventListener.onPersist) + sourceDaimonRepository.deleteAll(removed); + Source result = sourceRepository.save(updated); + publisher.publishEvent(new ChangeDataSourceEvent(this, updated.getSourceId(), updated.getSourceName())); + sourceService.invalidateCache(); + return ok(new SourceInfo(result)); + } else { + return notFound(); + } + } + + private void reuseDeletedDaimons(Source updated, Source source) { + List daimons = updated.getDaimons().stream().filter(d -> source.getDaimons().contains(d)) + .collect(Collectors.toList()); + List newDaimons = updated.getDaimons().stream().filter(d -> !source.getDaimons().contains(d)) + .collect(Collectors.toList()); + + List allDaimons = sourceDaimonRepository.findBySource(source); + + for (SourceDaimon newSourceDaimon : newDaimons) { + Optional reusedDaimonOpt = allDaimons.stream() + .filter(d -> d.equals(newSourceDaimon)) + .findFirst(); + if (reusedDaimonOpt.isPresent()) { + SourceDaimon reusedDaimon = reusedDaimonOpt.get(); + reusedDaimon.setPriority(newSourceDaimon.getPriority()); + reusedDaimon.setTableQualifier(newSourceDaimon.getTableQualifier()); + daimons.add(reusedDaimon); + } else { + daimons.add(newSourceDaimon); + } + } + updated.setDaimons(daimons); + } + + private void transformIfRequired(Source source) { + if (DBMSType.BIGQUERY.getOhdsiDB().equals(source.getSourceDialect()) && ArrayUtils.isNotEmpty(source.getKeyfile())) { + String connStr = source.getSourceConnection().replaceAll("OAuthPvtKeyPath=.+?(;|\\z)", ""); + source.setSourceConnection(connStr); + } + } + + private void setKeyfileData(Source updated, Source source, MultipartFile file) throws IOException { + if (source.supportsKeyfile()) { + if (updated.getKeyfileName() != null) { + if (!Objects.equals(updated.getKeyfileName(), source.getKeyfileName())) { + byte[] fileBytes = file != null ? file.getBytes() : new byte[0]; + updated.setKeyfile(fileBytes); + } else { + updated.setKeyfile(source.getKeyfile()); + } + return; + } + } + updated.setKeyfile(null); + updated.setKeyfileName(null); + } + + /** + * Delete a source. + * + * @summary Delete Source + * @param sourceId + * @return + * @throws Exception + */ + @DeleteMapping("/{sourceId}") + @Transactional + @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + public ResponseEntity delete(@PathVariable("sourceId") Integer sourceId) throws Exception { + if (!securityEnabled) { + return unauthorized(); + } + Source source = sourceRepository.findBySourceId(sourceId); + if (source != null) { + sourceRepository.delete(source); + publisher.publishEvent(new DeleteDataSourceEvent(this, sourceId, source.getSourceName())); + sourceService.invalidateCache(); + return ok(); + } else { + return notFound(); + } + } + + /** + * Check source connection. + * + * This method attempts to connect to the source by calling 'select 1' on the source connection. + * @summary Check connection + * @param sourceKey + * @return + */ + @GetMapping(value = "/connection/{key}", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional(noRollbackFor = CannotGetJdbcConnectionException.class) + public ResponseEntity checkConnection(@PathVariable("key") final String sourceKey) { + final Source source = sourceService.findBySourceKey(sourceKey); + sourceService.checkConnection(source); + return ok(source.getSourceInfo()); + } + + /** + * Get the first daimon (ad associated source) that has priority. In the event + * of a tie, the first source searched wins. + * + * @summary Get Priority Daimons + * @return + */ + @GetMapping(value = "/daimon/priority", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getPriorityDaimons() { + return ok(sourceService.getPriorityDaimons() + .entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> new SourceInfo(e.getValue()) + ))); + } + + /** + * Set priority of daimon + * + * Set the priority of the specified daimon of the specified source, and set the other daimons to 0. + * @summary Set Priority + * @param sourceKey + * @param daimonTypeName + * @return + */ + @PostMapping(value = "/{sourceKey}/daimons/{daimonType}/set-priority", produces = MediaType.APPLICATION_JSON_VALUE) + @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + public ResponseEntity updateSourcePriority( + @PathVariable("sourceKey") final String sourceKey, + @PathVariable("daimonType") final String daimonTypeName) { + if (!securityEnabled) { + return unauthorized(); + } + SourceDaimon.DaimonType daimonType = SourceDaimon.DaimonType.valueOf(daimonTypeName); + List daimonList = sourceDaimonRepository.findByDaimonType(daimonType); + daimonList.forEach(daimon -> { + Integer newPriority = daimon.getSource().getSourceKey().equals(sourceKey) ? 1 : 0; + daimon.setPriority(newPriority); + sourceDaimonRepository.save(daimon); + }); + sourceService.invalidateCache(); + return ok(); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/SqlRenderMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/SqlRenderMvcController.java new file mode 100644 index 0000000000..5ccb77b7f9 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/SqlRenderMvcController.java @@ -0,0 +1,49 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.service.SqlRenderService; +import org.ohdsi.webapi.sqlrender.SourceStatement; +import org.ohdsi.webapi.sqlrender.TranslatedStatement; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.ohdsi.webapi.Constants.SqlSchemaPlaceholders.TEMP_DATABASE_SCHEMA_PLACEHOLDER; + +/** + * Spring MVC version of SqlRenderService + * + * Migration Status: Replaces /service/SqlRenderService.java (Jersey) + * Endpoints: 1 POST endpoint + * Complexity: Simple - POST with JSON request body + */ +@RestController +@RequestMapping("/sqlrender") +public class SqlRenderMvcController extends AbstractMvcController { + + /** + * Translate an OHDSI SQL to a supported target SQL dialect + * + * Jersey: POST /WebAPI/sqlrender/translate + * Spring MVC: POST /WebAPI/v2/sqlrender/translate + * + * @param sourceStatement JSON with parameters, source SQL, and target dialect + * @return rendered and translated SQL + */ + @PostMapping( + value = "/translate", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity translateSQLFromSourceStatement(@RequestBody SourceStatement sourceStatement) { + if (sourceStatement == null) { + return ok(new TranslatedStatement()); + } + sourceStatement.setOracleTempSchema(TEMP_DATABASE_SCHEMA_PLACEHOLDER); + TranslatedStatement result = SqlRenderService.translateSQL(sourceStatement); + return ok(result); + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/UserMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/UserMvcController.java new file mode 100644 index 0000000000..55960f7f4c --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/UserMvcController.java @@ -0,0 +1,368 @@ +package org.ohdsi.webapi.mvc.controller; + +import org.ohdsi.webapi.arachne.logging.event.*; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.shiro.Entities.PermissionEntity; +import org.ohdsi.webapi.shiro.Entities.RoleEntity; +import org.ohdsi.webapi.shiro.Entities.UserEntity; +import org.ohdsi.webapi.shiro.PermissionManager; +import org.ohdsi.webapi.user.Role; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Spring MVC version of UserService + * + * Migration Status: Replaces /service/UserService.java (Jersey) + * Endpoints: 15 endpoints (3 GET user, 5 role management, 3 role permissions, 2 role users, 2 user roles/permissions) + * Complexity: Medium - CRUD operations for users, roles, and permissions with event publishing + * + * @author gennadiy.anisimov + */ +@RestController +@RequestMapping("") +public class UserMvcController extends AbstractMvcController { + + @Autowired + private PermissionManager authorizer; + + @Autowired + private ApplicationEventPublisher eventPublisher; + + @Value("${security.ad.default.import.group}#{T(java.util.Collections).emptyList()}") + private List defaultRoles; + + private Map roleCreatorPermissionsTemplate = new LinkedHashMap<>(); + + public UserMvcController() { + this.roleCreatorPermissionsTemplate.put("role:%s:permissions:*:put", "Add permissions to role with ID = %s"); + this.roleCreatorPermissionsTemplate.put("role:%s:permissions:*:delete", "Remove permissions from role with ID = %s"); + this.roleCreatorPermissionsTemplate.put("role:%s:put", "Update role with ID = %s"); + this.roleCreatorPermissionsTemplate.put("role:%s:delete", "Delete role with ID = %s"); + } + + public static class User implements Comparable { + public Long id; + public String login; + public String name; + public List permissions; + public Map> permissionIdx; + + public User() {} + + public User(UserEntity userEntity) { + this.id = userEntity.getId(); + this.login = userEntity.getLogin(); + this.name = userEntity.getName(); + } + + @Override + public int compareTo(User o) { + Comparator c = Comparator.naturalOrder(); + if (this.id == null && o.id == null) + return c.compare(this.login, o.login); + else + return c.compare(this.id, o.id); + } + } + + public static class Permission implements Comparable { + public Long id; + public String permission; + public String description; + + public Permission() {} + + public Permission(PermissionEntity permissionEntity) { + this.id = permissionEntity.getId(); + this.permission = permissionEntity.getValue(); + this.description = permissionEntity.getDescription(); + } + + @Override + public int compareTo(Permission o) { + Comparator c = Comparator.naturalOrder(); + if (this.id == null && o.id == null) + return c.compare(this.permission, o.permission); + else + return c.compare(this.id, o.id); + } + } + + /** + * Get all users + * + * Jersey: GET /WebAPI/user + * Spring MVC: GET /WebAPI/v2/user + */ + @GetMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getUsers() { + Iterable userEntities = this.authorizer.getUsers(); + ArrayList users = convertUsers(userEntities); + return ok(users); + } + + /** + * Get current user + * + * Jersey: GET /WebAPI/user/me + * Spring MVC: GET /WebAPI/v2/user/me + */ + @GetMapping(value = "/user/me", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCurrentUserDetails() throws Exception { + UserEntity currentUser = this.authorizer.getCurrentUser(); + Iterable permissions = this.authorizer.getUserPermissions(currentUser.getId()); + + User user = new User(); + user.id = currentUser.getId(); + user.login = currentUser.getLogin(); + user.name = currentUser.getName(); + user.permissions = convertPermissions(permissions); + user.permissionIdx = authorizer.queryUserPermissions(currentUser.getLogin()).permissions; + + return ok(user); + } + + /** + * Get user permissions + * + * Jersey: GET /WebAPI/user/{userId}/permissions + * Spring MVC: GET /WebAPI/v2/user/{userId}/permissions + */ + @GetMapping(value = "/user/{userId}/permissions", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getUsersPermissions(@PathVariable("userId") Long userId) throws Exception { + Set permissionEntities = this.authorizer.getUserPermissions(userId); + List permissions = convertPermissions(permissionEntities); + Collections.sort(permissions); + return ok(permissions); + } + + /** + * Get user roles + * + * Jersey: GET /WebAPI/user/{userId}/roles + * Spring MVC: GET /WebAPI/v2/user/{userId}/roles + */ + @GetMapping(value = "/user/{userId}/roles", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getUserRoles(@PathVariable("userId") Long userId) throws Exception { + Set roleEntities = this.authorizer.getUserRoles(userId); + ArrayList roles = convertRoles(roleEntities); + Collections.sort(roles); + return ok(roles); + } + + /** + * Create a new role + * + * Jersey: POST /WebAPI/role + * Spring MVC: POST /WebAPI/v2/role + */ + @PostMapping(value = "/role", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createRole(@RequestBody Role role) throws Exception { + RoleEntity roleEntity = this.authorizer.addRole(role.role, true); + RoleEntity personalRole = this.authorizer.getCurrentUserPersonalRole(); + this.authorizer.addPermissionsFromTemplate( + personalRole, + this.roleCreatorPermissionsTemplate, + String.valueOf(roleEntity.getId())); + Role newRole = new Role(roleEntity); + eventPublisher.publishEvent(new AddRoleEvent(this, newRole.id, newRole.role)); + return ok(newRole); + } + + /** + * Update a role + * + * Jersey: PUT /WebAPI/role/{roleId} + * Spring MVC: PUT /WebAPI/v2/role/{roleId} + */ + @PutMapping(value = "/role/{roleId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity updateRole(@PathVariable("roleId") Long id, @RequestBody Role role) throws Exception { + RoleEntity roleEntity = this.authorizer.getRole(id); + if (roleEntity == null) { + throw new Exception("Role doesn't exist"); + } + roleEntity.setName(role.role); + roleEntity = this.authorizer.updateRole(roleEntity); + eventPublisher.publishEvent(new ChangeRoleEvent(this, id, role.role)); + return ok(new Role(roleEntity)); + } + + /** + * Get all roles + * + * Jersey: GET /WebAPI/role + * Spring MVC: GET /WebAPI/v2/role + */ + @GetMapping(value = "/role", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getRoles( + @RequestParam(value = "include_personal", defaultValue = "false") boolean includePersonalRoles) { + Iterable roleEntities = this.authorizer.getRoles(includePersonalRoles); + ArrayList roles = convertRoles(roleEntities); + return ok(roles); + } + + /** + * Get a role by ID + * + * Jersey: GET /WebAPI/role/{roleId} + * Spring MVC: GET /WebAPI/v2/role/{roleId} + */ + @GetMapping(value = "/role/{roleId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getRole(@PathVariable("roleId") Long id) { + RoleEntity roleEntity = this.authorizer.getRole(id); + Role role = new Role(roleEntity); + return ok(role); + } + + /** + * Delete a role + * + * Jersey: DELETE /WebAPI/role/{roleId} + * Spring MVC: DELETE /WebAPI/v2/role/{roleId} + */ + @DeleteMapping(value = "/role/{roleId}") + public ResponseEntity removeRole(@PathVariable("roleId") Long roleId) { + this.authorizer.removeRole(roleId); + this.authorizer.removePermissionsFromTemplate(this.roleCreatorPermissionsTemplate, String.valueOf(roleId)); + return ok(); + } + + /** + * Get role permissions + * + * Jersey: GET /WebAPI/role/{roleId}/permissions + * Spring MVC: GET /WebAPI/v2/role/{roleId}/permissions + */ + @GetMapping(value = "/role/{roleId}/permissions", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getRolePermissions(@PathVariable("roleId") Long roleId) throws Exception { + Set permissionEntities = this.authorizer.getRolePermissions(roleId); + List permissions = convertPermissions(permissionEntities); + Collections.sort(permissions); + return ok(permissions); + } + + /** + * Add permissions to a role + * + * Jersey: PUT /WebAPI/role/{roleId}/permissions/{permissionIdList} + * Spring MVC: PUT /WebAPI/v2/role/{roleId}/permissions/{permissionIdList} + */ + @PutMapping(value = "/role/{roleId}/permissions/{permissionIdList}") + public ResponseEntity addPermissionToRole( + @PathVariable("roleId") Long roleId, + @PathVariable("permissionIdList") String permissionIdList) throws Exception { + String[] ids = permissionIdList.split("\\+"); + for (String permissionIdString : ids) { + Long permissionId = Long.parseLong(permissionIdString); + this.authorizer.addPermission(roleId, permissionId); + eventPublisher.publishEvent(new AddPermissionEvent(this, permissionId, roleId)); + } + return ok(); + } + + /** + * Remove permissions from a role + * + * Jersey: DELETE /WebAPI/role/{roleId}/permissions/{permissionIdList} + * Spring MVC: DELETE /WebAPI/v2/role/{roleId}/permissions/{permissionIdList} + */ + @DeleteMapping(value = "/role/{roleId}/permissions/{permissionIdList}") + public ResponseEntity removePermissionFromRole( + @PathVariable("roleId") Long roleId, + @PathVariable("permissionIdList") String permissionIdList) { + String[] ids = permissionIdList.split("\\+"); + for (String permissionIdString : ids) { + Long permissionId = Long.parseLong(permissionIdString); + this.authorizer.removePermission(permissionId, roleId); + eventPublisher.publishEvent(new DeletePermissionEvent(this, permissionId, roleId)); + } + return ok(); + } + + /** + * Get users assigned to a role + * + * Jersey: GET /WebAPI/role/{roleId}/users + * Spring MVC: GET /WebAPI/v2/role/{roleId}/users + */ + @GetMapping(value = "/role/{roleId}/users", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getRoleUsers(@PathVariable("roleId") Long roleId) throws Exception { + Set userEntities = this.authorizer.getRoleUsers(roleId); + ArrayList users = this.convertUsers(userEntities); + Collections.sort(users); + return ok(users); + } + + /** + * Assign users to a role + * + * Jersey: PUT /WebAPI/role/{roleId}/users/{userIdList} + * Spring MVC: PUT /WebAPI/v2/role/{roleId}/users/{userIdList} + */ + @PutMapping(value = "/role/{roleId}/users/{userIdList}") + public ResponseEntity addUserToRole( + @PathVariable("roleId") Long roleId, + @PathVariable("userIdList") String userIdList) throws Exception { + String[] ids = userIdList.split("\\+"); + for (String userIdString : ids) { + Long userId = Long.parseLong(userIdString); + this.authorizer.addUser(userId, roleId); + eventPublisher.publishEvent(new AssignRoleEvent(this, roleId, userId)); + } + return ok(); + } + + /** + * Remove users from a role + * + * Jersey: DELETE /WebAPI/role/{roleId}/users/{userIdList} + * Spring MVC: DELETE /WebAPI/v2/role/{roleId}/users/{userIdList} + */ + @DeleteMapping(value = "/role/{roleId}/users/{userIdList}") + public ResponseEntity removeUserFromRole( + @PathVariable("roleId") Long roleId, + @PathVariable("userIdList") String userIdList) { + String[] ids = userIdList.split("\\+"); + for (String userIdString : ids) { + Long userId = Long.parseLong(userIdString); + this.authorizer.removeUser(userId, roleId); + eventPublisher.publishEvent(new UnassignRoleEvent(this, roleId, userId)); + } + return ok(); + } + + // Helper methods + + private List convertPermissions(final Iterable permissionEntities) { + return StreamSupport.stream(permissionEntities.spliterator(), false) + .map(UserMvcController.Permission::new) + .collect(Collectors.toList()); + } + + private ArrayList convertRoles(final Iterable roleEntities) { + ArrayList roles = new ArrayList<>(); + for (RoleEntity roleEntity : roleEntities) { + Role role = new Role(roleEntity, defaultRoles.contains(roleEntity.getName())); + roles.add(role); + } + return roles; + } + + private ArrayList convertUsers(final Iterable userEntities) { + ArrayList users = new ArrayList<>(); + for (UserEntity userEntity : userEntities) { + User user = new User(userEntity); + users.add(user); + } + return users; + } +} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/VocabularyMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/VocabularyMvcController.java new file mode 100644 index 0000000000..12af606dbe --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/VocabularyMvcController.java @@ -0,0 +1,715 @@ +package org.ohdsi.webapi.mvc.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.ohdsi.circe.cohortdefinition.ConceptSet; +import org.ohdsi.circe.vocabulary.ConceptSetExpression; +import org.ohdsi.vocabulary.Concept; +import org.ohdsi.webapi.conceptset.ConceptSetComparison; +import org.ohdsi.webapi.conceptset.ConceptSetExport; +import org.ohdsi.webapi.conceptset.ConceptSetOptimizationResult; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.service.VocabularyService; +import org.ohdsi.webapi.service.cscompare.CompareArbitraryDto; +import org.ohdsi.webapi.source.SourceInfo; +import org.ohdsi.webapi.vocabulary.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Spring MVC version of VocabularyService + * + * Migration Status: Replaces /service/VocabularyService.java (Jersey) + * Endpoints: 40+ endpoints for vocabulary operations + * Complexity: High - complex search, lookups, concept set operations + * + * Note: This controller delegates to the existing Jersey VocabularyService to reuse + * all business logic while providing Spring MVC endpoints. + */ +@RestController +@RequestMapping("/vocabulary") +public class VocabularyMvcController extends AbstractMvcController { + + @Autowired + private VocabularyService vocabularyService; + + @Autowired + private ObjectMapper objectMapper; + + /** + * DTO for ancestor/descendant concept ID lists + */ + public static class ConceptIdListsDto { + public List ancestors; + public List descendants; + } + + /** + * Calculates the full set of ancestor and descendant concepts for a list of + * ancestor and descendant concepts specified. + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/lookup/identifiers/ancestors + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/lookup/identifiers/ancestors + */ + @PostMapping(value = "/{sourceKey}/lookup/identifiers/ancestors", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity>> calculateAscendants( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptIdListsDto dto) throws Exception { + // Convert our DTO to the private Ids class using reflection and JSON serialization + String json = objectMapper.writeValueAsString(dto); + Class idsClass = Class.forName("org.ohdsi.webapi.service.VocabularyService$Ids"); + Object ids = objectMapper.readValue(json, idsClass); + + // Use reflection to call the method since Ids is private + java.lang.reflect.Method method = VocabularyService.class.getMethod( + "calculateAscendants", String.class, idsClass); + @SuppressWarnings("unchecked") + Map> result = (Map>) method.invoke( + vocabularyService, sourceKey, ids); + return ok(result); + } + + /** + * Get concepts from concept identifiers (IDs) from a specific source + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/lookup/identifiers + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/lookup/identifiers + */ + @PostMapping(value = "/{sourceKey}/lookup/identifiers", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> executeIdentifierLookup( + @PathVariable("sourceKey") String sourceKey, + @RequestBody long[] identifiers) { + Collection concepts = vocabularyService.executeIdentifierLookup(sourceKey, identifiers); + return ok(concepts); + } + + /** + * Get concepts from concept identifiers (IDs) from the default vocabulary source + * + * Jersey: POST /WebAPI/vocabulary/lookup/identifiers + * Spring MVC: POST /WebAPI/v2/vocabulary/lookup/identifiers + */ + @PostMapping(value = "/lookup/identifiers", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> executeIdentifierLookupDefault(@RequestBody long[] identifiers) { + Collection concepts = vocabularyService.executeIdentifierLookup(identifiers); + return ok(concepts); + } + + /** + * Get concepts from source codes from a specific source + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/lookup/sourcecodes + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/lookup/sourcecodes + */ + @PostMapping(value = "/{sourceKey}/lookup/sourcecodes", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> executeSourcecodeLookup( + @PathVariable("sourceKey") String sourceKey, + @RequestBody String[] sourcecodes) { + Collection concepts = vocabularyService.executeSourcecodeLookup(sourceKey, sourcecodes); + return ok(concepts); + } + + /** + * Get concepts from source codes from the default vocabulary source + * + * Jersey: POST /WebAPI/vocabulary/lookup/sourcecodes + * Spring MVC: POST /WebAPI/v2/vocabulary/lookup/sourcecodes + */ + @PostMapping(value = "/lookup/sourcecodes", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> executeSourcecodeLookupDefault(@RequestBody String[] sourcecodes) { + Collection concepts = vocabularyService.executeSourcecodeLookup(sourcecodes); + return ok(concepts); + } + + /** + * Get concepts mapped to the selected concept identifiers from a specific source + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/lookup/mapped + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/lookup/mapped + */ + @PostMapping(value = "/{sourceKey}/lookup/mapped", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> executeMappedLookup( + @PathVariable("sourceKey") String sourceKey, + @RequestBody long[] identifiers) { + Collection concepts = vocabularyService.executeMappedLookup(sourceKey, identifiers); + return ok(concepts); + } + + /** + * Get concepts mapped to the selected concept identifiers from the default source + * + * Jersey: POST /WebAPI/vocabulary/lookup/mapped + * Spring MVC: POST /WebAPI/v2/vocabulary/lookup/mapped + */ + @PostMapping(value = "/lookup/mapped", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> executeMappedLookupDefault(@RequestBody long[] identifiers) { + Collection concepts = vocabularyService.executeMappedLookup(identifiers); + return ok(concepts); + } + + /** + * Search for a concept on the selected source (POST with ConceptSearch) + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/search + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/search + */ + @PostMapping(value = "/{sourceKey}/search", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> executeSearchPost( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptSearch search) { + Collection concepts = vocabularyService.executeSearch(sourceKey, search); + return ok(concepts); + } + + /** + * Search for a concept on the default vocabulary source (POST with ConceptSearch) + * + * Jersey: POST /WebAPI/vocabulary/search + * Spring MVC: POST /WebAPI/v2/vocabulary/search + */ + @PostMapping(value = "/search", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> executeSearchPostDefault(@RequestBody ConceptSearch search) { + Collection concepts = vocabularyService.executeSearch(search); + return ok(concepts); + } + + /** + * Search for a concept based on a query using the selected vocabulary source (with path variable) + * + * Jersey: GET /WebAPI/vocabulary/{sourceKey}/search/{query} + * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/search/{query} + */ + @GetMapping(value = "/{sourceKey}/search/{query}", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> executeSearchPathQuery( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("query") String query) { + Collection concepts = vocabularyService.executeSearch(sourceKey, query); + return ok(concepts); + } + + /** + * Search for a concept using query parameters + * + * Jersey: GET /WebAPI/vocabulary/{sourceKey}/search?query=...&rows=... + * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/search?query=...&rows=... + */ + @GetMapping(value = "/{sourceKey}/search", + produces = MediaType.APPLICATION_JSON_VALUE, + params = "query") + public ResponseEntity> executeSearchQueryParam( + @PathVariable("sourceKey") String sourceKey, + @RequestParam("query") String query, + @RequestParam(value = "rows", defaultValue = VocabularyService.DEFAULT_SEARCH_ROWS) String rows) { + Collection concepts = vocabularyService.executeSearch(sourceKey, query, rows); + return ok(concepts); + } + + /** + * Search for a concept based on a query using the default vocabulary source (path variable) + * + * Jersey: GET /WebAPI/vocabulary/search/{query} + * Spring MVC: GET /WebAPI/v2/vocabulary/search/{query} + */ + @GetMapping(value = "/search/{query}", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> executeSearchDefaultPathQuery(@PathVariable("query") String query) { + Collection concepts = vocabularyService.executeSearch(query); + return ok(concepts); + } + + /** + * Get a concept based on the concept identifier from the specified source + * + * Jersey: GET /WebAPI/vocabulary/{sourceKey}/concept/{id} + * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/concept/{id} + */ + @GetMapping(value = "/{sourceKey}/concept/{id}", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getConcept( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") long id) { + Concept concept = vocabularyService.getConcept(sourceKey, id); + return ok(concept); + } + + /** + * Get a concept based on the concept identifier from the default vocabulary source + * + * Jersey: GET /WebAPI/vocabulary/concept/{id} + * Spring MVC: GET /WebAPI/v2/vocabulary/concept/{id} + */ + @GetMapping(value = "/concept/{id}", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getConceptDefault(@PathVariable("id") long id) { + Concept concept = vocabularyService.getConcept(id); + return ok(concept); + } + + /** + * Get related concepts for the selected concept identifier from a source + * + * Jersey: GET /WebAPI/vocabulary/{sourceKey}/concept/{id}/related + * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/concept/{id}/related + */ + @GetMapping(value = "/{sourceKey}/concept/{id}/related", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getRelatedConcepts( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") Long id) { + Collection concepts = vocabularyService.getRelatedConcepts(sourceKey, id); + return ok(concepts); + } + + /** + * Get related standard mapped concepts + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/related-standard + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/related-standard + */ + @PostMapping(value = "/{sourceKey}/related-standard", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getRelatedStandardMappedConcepts( + @PathVariable("sourceKey") String sourceKey, + @RequestBody List allConceptIds) { + Collection concepts = vocabularyService.getRelatedStandardMappedConcepts(sourceKey, allConceptIds); + return ok(concepts); + } + + /** + * Get ancestor and descendant concepts for the selected concept identifier + * + * Jersey: GET /WebAPI/vocabulary/{sourceKey}/concept/{id}/ancestorAndDescendant + * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/concept/{id}/ancestorAndDescendant + */ + @GetMapping(value = "/{sourceKey}/concept/{id}/ancestorAndDescendant", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getConceptAncestorAndDescendant( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") Long id) { + Collection concepts = vocabularyService.getConceptAncestorAndDescendant(sourceKey, id); + return ok(concepts); + } + + /** + * Get related concepts for the selected concept identifier (default vocabulary) + * + * Jersey: GET /WebAPI/vocabulary/concept/{id}/related + * Spring MVC: GET /WebAPI/v2/vocabulary/concept/{id}/related + */ + @GetMapping(value = "/concept/{id}/related", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getRelatedConceptsDefault(@PathVariable("id") Long id) { + Collection concepts = vocabularyService.getRelatedConcepts(id); + return ok(concepts); + } + + /** + * Get common ancestor concepts + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/commonAncestors + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/commonAncestors + */ + @PostMapping(value = "/{sourceKey}/commonAncestors", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCommonAncestors( + @PathVariable("sourceKey") String sourceKey, + @RequestBody Object[] identifiers) { + Collection concepts = vocabularyService.getCommonAncestors(sourceKey, identifiers); + return ok(concepts); + } + + /** + * Get common ancestor concepts (default vocabulary) + * + * Jersey: POST /WebAPI/vocabulary/commonAncestors + * Spring MVC: POST /WebAPI/v2/vocabulary/commonAncestors + */ + @PostMapping(value = "/commonAncestors", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCommonAncestorsDefault(@RequestBody Object[] identifiers) { + Collection concepts = vocabularyService.getCommonAncestors(identifiers); + return ok(concepts); + } + + /** + * Resolve a concept set expression into a collection of concept identifiers + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/resolveConceptSetExpression + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/resolveConceptSetExpression + */ + @PostMapping(value = "/{sourceKey}/resolveConceptSetExpression", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> resolveConceptSetExpression( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptSetExpression conceptSetExpression) { + Collection identifiers = vocabularyService.resolveConceptSetExpression(sourceKey, conceptSetExpression); + return ok(identifiers); + } + + /** + * Resolve a concept set expression (default vocabulary) + * + * Jersey: POST /WebAPI/vocabulary/resolveConceptSetExpression + * Spring MVC: POST /WebAPI/v2/vocabulary/resolveConceptSetExpression + */ + @PostMapping(value = "/resolveConceptSetExpression", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> resolveConceptSetExpressionDefault( + @RequestBody ConceptSetExpression conceptSetExpression) { + Collection identifiers = vocabularyService.resolveConceptSetExpression(conceptSetExpression); + return ok(identifiers); + } + + /** + * Get included concept counts for concept set expression + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/included-concepts/count + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/included-concepts/count + */ + @PostMapping(value = "/{sourceKey}/included-concepts/count", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity countIncludedConceptSets( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptSetExpression conceptSetExpression) { + Integer count = vocabularyService.countIncludedConceptSets(sourceKey, conceptSetExpression); + return ok(count); + } + + /** + * Get included concept counts for concept set expression (default vocabulary) + * + * Jersey: POST /WebAPI/vocabulary/included-concepts/count + * Spring MVC: POST /WebAPI/v2/vocabulary/included-concepts/count + */ + @PostMapping(value = "/included-concepts/count", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity countIncludedConceptSetsDefault( + @RequestBody ConceptSetExpression conceptSetExpression) { + Integer count = vocabularyService.countIncludedConcepSets(conceptSetExpression); + return ok(count); + } + + /** + * Get SQL to resolve concept set expression + * + * Jersey: POST /WebAPI/vocabulary/conceptSetExpressionSQL + * Spring MVC: POST /WebAPI/v2/vocabulary/conceptSetExpressionSQL + */ + @PostMapping(value = "/conceptSetExpressionSQL", + produces = MediaType.TEXT_PLAIN_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getConceptSetExpressionSQL(@RequestBody ConceptSetExpression conceptSetExpression) { + String sql = vocabularyService.getConceptSetExpressionSQL(conceptSetExpression); + return ok(sql); + } + + /** + * Get descendant concepts for the selected concept identifier + * + * Jersey: GET /WebAPI/vocabulary/{sourceKey}/concept/{id}/descendants + * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/concept/{id}/descendants + */ + @GetMapping(value = "/{sourceKey}/concept/{id}/descendants", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDescendantConcepts( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") Long id) { + Collection concepts = vocabularyService.getDescendantConcepts(sourceKey, id); + return ok(concepts); + } + + /** + * Get descendant concepts (default vocabulary) + * + * Jersey: GET /WebAPI/vocabulary/concept/{id}/descendants + * Spring MVC: GET /WebAPI/v2/vocabulary/concept/{id}/descendants + */ + @GetMapping(value = "/concept/{id}/descendants", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDescendantConceptsDefault(@PathVariable("id") Long id) { + Collection concepts = vocabularyService.getDescendantConcepts(id); + return ok(concepts); + } + + /** + * Get domains + * + * Jersey: GET /WebAPI/vocabulary/{sourceKey}/domains + * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/domains + */ + @GetMapping(value = "/{sourceKey}/domains", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDomains(@PathVariable("sourceKey") String sourceKey) { + Collection domains = vocabularyService.getDomains(sourceKey); + return ok(domains); + } + + /** + * Get domains (default vocabulary) + * + * Jersey: GET /WebAPI/vocabulary/domains + * Spring MVC: GET /WebAPI/v2/vocabulary/domains + */ + @GetMapping(value = "/domains", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDomainsDefault() { + Collection domains = vocabularyService.getDomains(); + return ok(domains); + } + + /** + * Get vocabularies + * + * Jersey: GET /WebAPI/vocabulary/{sourceKey}/vocabularies + * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/vocabularies + */ + @GetMapping(value = "/{sourceKey}/vocabularies", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getVocabularies(@PathVariable("sourceKey") String sourceKey) { + Collection vocabularies = vocabularyService.getVocabularies(sourceKey); + return ok(vocabularies); + } + + /** + * Get vocabularies (default vocabulary) + * + * Jersey: GET /WebAPI/vocabulary/vocabularies + * Spring MVC: GET /WebAPI/v2/vocabulary/vocabularies + */ + @GetMapping(value = "/vocabularies", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getVocabulariesDefault() { + Collection vocabularies = vocabularyService.getVocabularies(); + return ok(vocabularies); + } + + /** + * Get vocabulary version info + * + * Jersey: GET /WebAPI/vocabulary/{sourceKey}/info + * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/info + */ + @GetMapping(value = "/{sourceKey}/info", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getInfo(@PathVariable("sourceKey") String sourceKey) { + VocabularyInfo info = vocabularyService.getInfo(sourceKey); + return ok(info); + } + + /** + * Get descendant concepts by source + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/descendantofancestor + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/descendantofancestor + */ + @PostMapping(value = "/{sourceKey}/descendantofancestor", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDescendantOfAncestorConcepts( + @PathVariable("sourceKey") String sourceKey, + @RequestBody DescendentOfAncestorSearch search) { + Collection concepts = vocabularyService.getDescendantOfAncestorConcepts(sourceKey, search); + return ok(concepts); + } + + /** + * Get descendant concepts (default vocabulary) + * + * Jersey: POST /WebAPI/vocabulary/descendantofancestor + * Spring MVC: POST /WebAPI/v2/vocabulary/descendantofancestor + */ + @PostMapping(value = "/descendantofancestor", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDescendantOfAncestorConceptsDefault( + @RequestBody DescendentOfAncestorSearch search) { + Collection concepts = vocabularyService.getDescendantOfAncestorConcepts(search); + return ok(concepts); + } + + /** + * Get related concepts + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/relatedconcepts + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/relatedconcepts + */ + @PostMapping(value = "/{sourceKey}/relatedconcepts", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getRelatedConceptsFiltered( + @PathVariable("sourceKey") String sourceKey, + @RequestBody RelatedConceptSearch search) { + Collection concepts = vocabularyService.getRelatedConcepts(sourceKey, search); + return ok(concepts); + } + + /** + * Get related concepts (default vocabulary) + * + * Jersey: POST /WebAPI/vocabulary/relatedconcepts + * Spring MVC: POST /WebAPI/v2/vocabulary/relatedconcepts + */ + @PostMapping(value = "/relatedconcepts", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getRelatedConceptsFilteredDefault( + @RequestBody RelatedConceptSearch search) { + Collection concepts = vocabularyService.getRelatedConcepts(search); + return ok(concepts); + } + + /** + * Get descendant concepts for selected concepts + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/conceptlist/descendants + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/conceptlist/descendants + */ + @PostMapping(value = "/{sourceKey}/conceptlist/descendants", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDescendantConceptsByList( + @PathVariable("sourceKey") String sourceKey, + @RequestBody String[] conceptList) { + Collection concepts = vocabularyService.getDescendantConceptsByList(sourceKey, conceptList); + return ok(concepts); + } + + /** + * Get descendant concepts for selected concepts (default vocabulary) + * + * Jersey: POST /WebAPI/vocabulary/conceptlist/descendants + * Spring MVC: POST /WebAPI/v2/vocabulary/conceptlist/descendants + */ + @PostMapping(value = "/conceptlist/descendants", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDescendantConceptsByListDefault( + @RequestBody String[] conceptList) { + Collection concepts = vocabularyService.getDescendantConceptsByList(conceptList); + return ok(concepts); + } + + /** + * Get recommended concepts for selected concepts + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/lookup/recommended + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/lookup/recommended + */ + @PostMapping(value = "/{sourceKey}/lookup/recommended", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getRecommendedConceptsByList( + @PathVariable("sourceKey") String sourceKey, + @RequestBody long[] conceptList) { + Collection concepts = vocabularyService.getRecommendedConceptsByList(sourceKey, conceptList); + return ok(concepts); + } + + /** + * Compare concept sets + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/compare + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/compare + */ + @PostMapping(value = "/{sourceKey}/compare", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> compareConceptSets( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptSetExpression[] conceptSetExpressionList) throws Exception { + Collection comparison = vocabularyService.compareConceptSets(sourceKey, conceptSetExpressionList); + return ok(comparison); + } + + /** + * Compare concept sets (arbitrary/CSV) + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/compare-arbitrary + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/compare-arbitrary + */ + @PostMapping(value = "/{sourceKey}/compare-arbitrary", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> compareConceptSetsCsv( + @PathVariable("sourceKey") String sourceKey, + @RequestBody CompareArbitraryDto dto) throws Exception { + Collection comparison = vocabularyService.compareConceptSetsCsv(sourceKey, dto); + return ok(comparison); + } + + /** + * Compare concept sets (default vocabulary) + * + * Jersey: POST /WebAPI/vocabulary/compare + * Spring MVC: POST /WebAPI/v2/vocabulary/compare + */ + @PostMapping(value = "/compare", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> compareConceptSetsDefault( + @RequestBody ConceptSetExpression[] conceptSetExpressionList) throws Exception { + Collection comparison = vocabularyService.compareConceptSets(conceptSetExpressionList); + return ok(comparison); + } + + /** + * Optimize concept set (default vocabulary) + * + * Jersey: POST /WebAPI/vocabulary/optimize + * Spring MVC: POST /WebAPI/v2/vocabulary/optimize + */ + @PostMapping(value = "/optimize", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity optimizeConceptSetDefault( + @RequestBody ConceptSetExpression conceptSetExpression) throws Exception { + ConceptSetOptimizationResult result = vocabularyService.optimizeConceptSet(conceptSetExpression); + return ok(result); + } + + /** + * Optimize concept set + * + * Jersey: POST /WebAPI/vocabulary/{sourceKey}/optimize + * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/optimize + */ + @PostMapping(value = "/{sourceKey}/optimize", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity optimizeConceptSet( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptSetExpression conceptSetExpression) throws Exception { + ConceptSetOptimizationResult result = vocabularyService.optimizeConceptSet(sourceKey, conceptSetExpression); + return ok(result); + } +} diff --git a/src/main/java/org/ohdsi/webapi/reusable/ReusableMvcController.java b/src/main/java/org/ohdsi/webapi/reusable/ReusableMvcController.java new file mode 100644 index 0000000000..f66d9ac12e --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/reusable/ReusableMvcController.java @@ -0,0 +1,358 @@ +package org.ohdsi.webapi.reusable; + +import org.ohdsi.webapi.Pagination; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.reusable.dto.ReusableDTO; +import org.ohdsi.webapi.reusable.dto.ReusableVersionFullDTO; +import org.ohdsi.webapi.tag.dto.TagNameListRequestDTO; +import org.ohdsi.webapi.versioning.dto.VersionDTO; +import org.ohdsi.webapi.versioning.dto.VersionUpdateDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; + +/** + * Spring MVC version of ReusableController + * + * Migration Status: Replaces /reusable/ReusableController.java (Jersey) + * Endpoints: 14 endpoints (POST, GET, PUT, DELETE) + * Complexity: Medium - CRUD operations with versioning and tagging + */ +@RestController +@RequestMapping("/reusable") +public class ReusableMvcController extends AbstractMvcController { + + private final ReusableService reusableService; + + @Autowired + public ReusableMvcController(ReusableService reusableService) { + this.reusableService = reusableService; + } + + /** + * Create a new reusable + * + * Jersey: POST /WebAPI/reusable/ + * Spring MVC: POST /WebAPI/v2/reusable/ + * + * @param dto the reusable DTO + * @return created reusable + */ + @PostMapping( + value = "/", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity create(@RequestBody ReusableDTO dto) { + return ok(reusableService.create(dto)); + } + + /** + * Get paginated list of reusables + * + * Jersey: GET /WebAPI/reusable/ + * Spring MVC: GET /WebAPI/v2/reusable/ + * + * @param pageable pagination parameters + * @return page of reusables + */ + @GetMapping( + value = "/", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity> page(@Pagination Pageable pageable) { + return ok(reusableService.page(pageable)); + } + + /** + * Update an existing reusable + * + * Jersey: PUT /WebAPI/reusable/{id} + * Spring MVC: PUT /WebAPI/v2/reusable/{id} + * + * @param id the reusable ID + * @param dto the reusable DTO + * @return updated reusable + */ + @PutMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity update(@PathVariable("id") Integer id, @RequestBody ReusableDTO dto) { + return ok(reusableService.update(id, dto)); + } + + /** + * Copy a reusable + * + * Jersey: POST /WebAPI/reusable/{id} + * Spring MVC: POST /WebAPI/v2/reusable/{id} + * + * @param id the reusable ID + * @return copied reusable + */ + @PostMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity copy(@PathVariable("id") int id) { + return ok(reusableService.copy(id)); + } + + /** + * Get a reusable by ID + * + * Jersey: GET /WebAPI/reusable/{id} + * Spring MVC: GET /WebAPI/v2/reusable/{id} + * + * @param id the reusable ID + * @return the reusable + */ + @GetMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity get(@PathVariable("id") Integer id) { + return ok(reusableService.getDTOById(id)); + } + + /** + * Check if a reusable name exists + * + * Jersey: GET /WebAPI/reusable/{id}/exists + * Spring MVC: GET /WebAPI/v2/reusable/{id}/exists + * + * @param id the reusable ID (default 0) + * @param name the name to check + * @return true if exists + */ + @GetMapping( + value = "/{id}/exists", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity exists( + @PathVariable("id") int id, + @RequestParam(value = "name", required = false) String name) { + return ok(reusableService.exists(id, name)); + } + + /** + * Delete a reusable + * + * Jersey: DELETE /WebAPI/reusable/{id} + * Spring MVC: DELETE /WebAPI/v2/reusable/{id} + * + * @param id the reusable ID + */ + @DeleteMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity delete(@PathVariable("id") Integer id) { + reusableService.delete(id); + return ok(); + } + + /** + * Assign tag to Reusable + * + * Jersey: POST /WebAPI/reusable/{id}/tag/ + * Spring MVC: POST /WebAPI/v2/reusable/{id}/tag/ + * + * @param id the reusable ID + * @param tagId the tag ID + */ + @PostMapping( + value = "/{id}/tag/", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity assignTag(@PathVariable("id") int id, @RequestBody int tagId) { + reusableService.assignTag(id, tagId); + return ok(); + } + + /** + * Unassign tag from Reusable + * + * Jersey: DELETE /WebAPI/reusable/{id}/tag/{tagId} + * Spring MVC: DELETE /WebAPI/v2/reusable/{id}/tag/{tagId} + * + * @param id the reusable ID + * @param tagId the tag ID + */ + @DeleteMapping( + value = "/{id}/tag/{tagId}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity unassignTag(@PathVariable("id") int id, @PathVariable("tagId") int tagId) { + reusableService.unassignTag(id, tagId); + return ok(); + } + + /** + * Assign protected tag to Reusable + * + * Jersey: POST /WebAPI/reusable/{id}/protectedtag/ + * Spring MVC: POST /WebAPI/v2/reusable/{id}/protectedtag/ + * + * @param id the reusable ID + * @param tagId the tag ID + */ + @PostMapping( + value = "/{id}/protectedtag/", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity assignPermissionProtectedTag(@PathVariable("id") int id, @RequestBody int tagId) { + reusableService.assignTag(id, tagId); + return ok(); + } + + /** + * Unassign protected tag from Reusable + * + * Jersey: DELETE /WebAPI/reusable/{id}/protectedtag/{tagId} + * Spring MVC: DELETE /WebAPI/v2/reusable/{id}/protectedtag/{tagId} + * + * @param id the reusable ID + * @param tagId the tag ID + */ + @DeleteMapping( + value = "/{id}/protectedtag/{tagId}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity unassignPermissionProtectedTag(@PathVariable("id") int id, @PathVariable("tagId") int tagId) { + reusableService.unassignTag(id, tagId); + return ok(); + } + + /** + * Get list of versions of Reusable + * + * Jersey: GET /WebAPI/reusable/{id}/version/ + * Spring MVC: GET /WebAPI/v2/reusable/{id}/version/ + * + * @param id the reusable ID + * @return list of versions + */ + @GetMapping( + value = "/{id}/version/", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity> getVersions(@PathVariable("id") long id) { + return ok(reusableService.getVersions(id)); + } + + /** + * Get version of Reusable + * + * Jersey: GET /WebAPI/reusable/{id}/version/{version} + * Spring MVC: GET /WebAPI/v2/reusable/{id}/version/{version} + * + * @param id the reusable ID + * @param version the version number + * @return the version + */ + @GetMapping( + value = "/{id}/version/{version}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity getVersion(@PathVariable("id") int id, @PathVariable("version") int version) { + return ok(reusableService.getVersion(id, version)); + } + + /** + * Update version of Reusable + * + * Jersey: PUT /WebAPI/reusable/{id}/version/{version} + * Spring MVC: PUT /WebAPI/v2/reusable/{id}/version/{version} + * + * @param id the reusable ID + * @param version the version number + * @param updateDTO the version update DTO + * @return updated version + */ + @PutMapping( + value = "/{id}/version/{version}", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity updateVersion( + @PathVariable("id") int id, + @PathVariable("version") int version, + @RequestBody VersionUpdateDTO updateDTO) { + return ok(reusableService.updateVersion(id, version, updateDTO)); + } + + /** + * Delete version of Reusable + * + * Jersey: DELETE /WebAPI/reusable/{id}/version/{version} + * Spring MVC: DELETE /WebAPI/v2/reusable/{id}/version/{version} + * + * @param id the reusable ID + * @param version the version number + */ + @DeleteMapping( + value = "/{id}/version/{version}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity deleteVersion(@PathVariable("id") int id, @PathVariable("version") int version) { + reusableService.deleteVersion(id, version); + return ok(); + } + + /** + * Create a new asset from version of Reusable + * + * Jersey: PUT /WebAPI/reusable/{id}/version/{version}/createAsset + * Spring MVC: PUT /WebAPI/v2/reusable/{id}/version/{version}/createAsset + * + * @param id the reusable ID + * @param version the version number + * @return new reusable created from version + */ + @PutMapping( + value = "/{id}/version/{version}/createAsset", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity copyAssetFromVersion(@PathVariable("id") int id, @PathVariable("version") int version) { + return ok(reusableService.copyAssetFromVersion(id, version)); + } + + /** + * Get list of reusables with assigned tags + * + * Jersey: POST /WebAPI/reusable/byTags + * Spring MVC: POST /WebAPI/v2/reusable/byTags + * + * @param requestDTO tag name list request + * @return list of reusables + */ + @PostMapping( + value = "/byTags", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity> listByTags(@RequestBody TagNameListRequestDTO requestDTO) { + if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { + return ok(Collections.emptyList()); + } + return ok(reusableService.listByTags(requestDTO)); + } +} diff --git a/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticMvcController.java b/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticMvcController.java new file mode 100644 index 0000000000..c31abd2443 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticMvcController.java @@ -0,0 +1,291 @@ +package org.ohdsi.webapi.statistic.controller; + +import com.opencsv.CSVWriter; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.statistic.dto.AccessTrendDto; +import org.ohdsi.webapi.statistic.dto.AccessTrendsDto; +import org.ohdsi.webapi.statistic.dto.EndpointDto; +import org.ohdsi.webapi.statistic.dto.SourceExecutionDto; +import org.ohdsi.webapi.statistic.dto.SourceExecutionsDto; +import org.ohdsi.webapi.statistic.service.StatisticService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.io.ByteArrayOutputStream; +import java.io.StringWriter; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Spring MVC version of StatisticController + * + * Migration Status: Replaces /statistic/controller/StatisticController.java (Jersey) + * Endpoints: 2 POST endpoints + * Complexity: Medium - statistics with CSV generation + */ +@RestController +@RequestMapping("/statistic") +public class StatisticMvcController extends AbstractMvcController { + + private static final Logger log = LoggerFactory.getLogger(StatisticMvcController.class); + + private final StatisticService service; + + @Value("${audit.trail.enabled}") + private boolean auditTrailEnabled; + + public enum ResponseFormat { + CSV, JSON + } + + private static final List EXECUTION_STATISTICS_CSV_RESULT_HEADER = new ArrayList() {{ + add(new String[]{"Date", "Source", "Execution Type"}); + }}; + + private static final List EXECUTION_STATISTICS_CSV_RESULT_HEADER_WITH_USER_ID = new ArrayList() {{ + add(new String[]{"Date", "Source", "Execution Type", "User ID"}); + }}; + + private static final List ACCESS_TRENDS_CSV_RESULT_HEADER = new ArrayList() {{ + add(new String[]{"Date", "Endpoint"}); + }}; + + private static final List ACCESS_TRENDS_CSV_RESULT_HEADER_WITH_USER_ID = new ArrayList() {{ + add(new String[]{"Date", "Endpoint", "User ID"}); + }}; + + @Autowired + public StatisticMvcController(StatisticService service) { + this.service = service; + } + + /** + * Returns execution statistics + * + * Jersey: POST /WebAPI/statistic/executions + * Spring MVC: POST /WebAPI/v2/statistic/executions + * + * @param executionStatisticsRequest filter settings for statistics + * @return execution statistics in JSON or CSV format + */ + @PostMapping( + value = "/executions", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity executionStatistics(@RequestBody ExecutionStatisticsRequest executionStatisticsRequest) { + if (!auditTrailEnabled) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, + "Audit Trail functionality should be enabled (audit.trail.enabled) to serve this endpoint"); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + boolean showUserInformation = executionStatisticsRequest.isShowUserInformation(); + + SourceExecutionsDto sourceExecutions = service.getSourceExecutions( + LocalDate.parse(executionStatisticsRequest.getStartDate(), formatter), + LocalDate.parse(executionStatisticsRequest.getEndDate(), formatter), + executionStatisticsRequest.getSourceKey(), + showUserInformation); + + if (ResponseFormat.CSV.equals(executionStatisticsRequest.getResponseFormat())) { + return prepareExecutionResultResponse(sourceExecutions.getExecutions(), "execution_statistics.zip", showUserInformation); + } else { + return ok(sourceExecutions); + } + } + + /** + * Returns access trends statistics + * + * Jersey: POST /WebAPI/statistic/accesstrends + * Spring MVC: POST /WebAPI/v2/statistic/accesstrends + * + * @param accessTrendsStatisticsRequest filter settings for statistics + * @return access trends statistics in JSON or CSV format + */ + @PostMapping( + value = "/accesstrends", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity accessStatistics(@RequestBody AccessTrendsStatisticsRequest accessTrendsStatisticsRequest) { + if (!auditTrailEnabled) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, + "Audit Trail functionality should be enabled (audit.trail.enabled) to serve this endpoint"); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + boolean showUserInformation = accessTrendsStatisticsRequest.isShowUserInformation(); + + AccessTrendsDto trends = service.getAccessTrends( + LocalDate.parse(accessTrendsStatisticsRequest.getStartDate(), formatter), + LocalDate.parse(accessTrendsStatisticsRequest.getEndDate(), formatter), + accessTrendsStatisticsRequest.getEndpoints(), + showUserInformation); + + if (ResponseFormat.CSV.equals(accessTrendsStatisticsRequest.getResponseFormat())) { + return prepareAccessTrendsResponse(trends.getTrends(), "execution_trends.zip", showUserInformation); + } else { + return ok(trends); + } + } + + private ResponseEntity prepareExecutionResultResponse(List executions, String filename, boolean showUserInformation) { + List data = executions.stream() + .map(execution -> showUserInformation + ? new String[]{execution.getExecutionDate(), execution.getSourceName(), execution.getExecutionName(), execution.getUserId()} + : new String[]{execution.getExecutionDate(), execution.getSourceName(), execution.getExecutionName()} + ) + .collect(Collectors.toList()); + return prepareResponse(data, filename, showUserInformation ? EXECUTION_STATISTICS_CSV_RESULT_HEADER_WITH_USER_ID : EXECUTION_STATISTICS_CSV_RESULT_HEADER); + } + + private ResponseEntity prepareAccessTrendsResponse(List trends, String filename, boolean showUserInformation) { + List data = trends.stream() + .map(trend -> showUserInformation + ? new String[]{trend.getExecutionDate().toString(), trend.getEndpointName(), trend.getUserID()} + : new String[]{trend.getExecutionDate().toString(), trend.getEndpointName()} + ) + .collect(Collectors.toList()); + return prepareResponse(data, filename, showUserInformation ? ACCESS_TRENDS_CSV_RESULT_HEADER_WITH_USER_ID : ACCESS_TRENDS_CSV_RESULT_HEADER); + } + + private ResponseEntity prepareResponse(List data, String filename, List header) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + StringWriter sw = new StringWriter(); + CSVWriter csvWriter = new CSVWriter(sw, ',', CSVWriter.DEFAULT_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER)) { + + csvWriter.writeAll(header); + csvWriter.writeAll(data); + csvWriter.flush(); + baos.write(sw.getBuffer().toString().getBytes()); + + ByteArrayResource resource = new ByteArrayResource(baos.toByteArray()); + + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", filename)) + .body(resource); + } catch (Exception ex) { + log.error("An error occurred while building a response", ex); + throw new RuntimeException(ex); + } + } + + public static final class ExecutionStatisticsRequest { + // Format - yyyy-MM-dd + String startDate; + // Format - yyyy-MM-dd + String endDate; + String sourceKey; + ResponseFormat responseFormat; + boolean showUserInformation; + + public String getStartDate() { + return startDate; + } + + public void setStartDate(String startDate) { + this.startDate = startDate; + } + + public String getEndDate() { + return endDate; + } + + public void setEndDate(String endDate) { + this.endDate = endDate; + } + + public String getSourceKey() { + return sourceKey; + } + + public void setSourceKey(String sourceKey) { + this.sourceKey = sourceKey; + } + + public ResponseFormat getResponseFormat() { + return responseFormat; + } + + public void setResponseFormat(ResponseFormat responseFormat) { + this.responseFormat = responseFormat; + } + + public boolean isShowUserInformation() { + return showUserInformation; + } + + public void setShowUserInformation(boolean showUserInformation) { + this.showUserInformation = showUserInformation; + } + } + + public static final class AccessTrendsStatisticsRequest { + // Format - yyyy-MM-dd + String startDate; + // Format - yyyy-MM-dd + String endDate; + // Key - method (POST, GET) + // Value - endpoint ("{}" can be used as a placeholder, will be converted to ".*" in regular expression) + List endpoints; + ResponseFormat responseFormat; + boolean showUserInformation; + + public String getStartDate() { + return startDate; + } + + public void setStartDate(String startDate) { + this.startDate = startDate; + } + + public String getEndDate() { + return endDate; + } + + public void setEndDate(String endDate) { + this.endDate = endDate; + } + + public List getEndpoints() { + return endpoints; + } + + public void setEndpoints(List endpoints) { + this.endpoints = endpoints; + } + + public ResponseFormat getResponseFormat() { + return responseFormat; + } + + public void setResponseFormat(ResponseFormat responseFormat) { + this.responseFormat = responseFormat; + } + + public boolean isShowUserInformation() { + return showUserInformation; + } + + public void setShowUserInformation(boolean showUserInformation) { + this.showUserInformation = showUserInformation; + } + } +} diff --git a/src/main/java/org/ohdsi/webapi/user/importer/UserImportJobMvcController.java b/src/main/java/org/ohdsi/webapi/user/importer/UserImportJobMvcController.java new file mode 100644 index 0000000000..47ea4367af --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/user/importer/UserImportJobMvcController.java @@ -0,0 +1,186 @@ +package org.ohdsi.webapi.user.importer; + +import org.ohdsi.webapi.arachne.scheduler.exception.JobNotFoundException; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.user.importer.dto.JobHistoryItemDTO; +import org.ohdsi.webapi.user.importer.dto.UserImportJobDTO; +import org.ohdsi.webapi.user.importer.exception.JobAlreadyExistException; +import org.ohdsi.webapi.user.importer.model.UserImportJob; +import org.ohdsi.webapi.user.importer.service.UserImportJobService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.ohdsi.webapi.Constants.JOB_IS_ALREADY_SCHEDULED; + +/** + * REST Services related to importing user information + * from an external source (i.e. Active Directory) + * + * Spring MVC version of UserImportJobController + * + * Migration Status: Replaces /user/importer/UserImportJobController.java (Jersey) + * Endpoints: 5 endpoints (GET, POST, PUT, DELETE) + * Complexity: Medium - user import job management with history + */ +@RestController +@RequestMapping("/user/import/job") +@Transactional +public class UserImportJobMvcController extends AbstractMvcController { + + private final UserImportJobService jobService; + private final GenericConversionService conversionService; + + public UserImportJobMvcController( + UserImportJobService jobService, + @Qualifier("conversionService") GenericConversionService conversionService) { + this.jobService = jobService; + this.conversionService = conversionService; + } + + /** + * Create a user import job + * + * Jersey: POST /WebAPI/user/import/job/ + * Spring MVC: POST /WebAPI/v2/user/import/job/ + * + * @param jobDTO The user import information + * @return The job information + */ + @PostMapping( + value = "/", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity createJob(@RequestBody UserImportJobDTO jobDTO) { + UserImportJob job = conversionService.convert(jobDTO, UserImportJob.class); + try { + UserImportJob created = jobService.createJob(job); + return ok(conversionService.convert(created, UserImportJobDTO.class)); + } catch (JobAlreadyExistException e) { + throw new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, + String.format(JOB_IS_ALREADY_SCHEDULED, job.getProviderType())); + } + } + + /** + * Update a user import job + * + * Jersey: PUT /WebAPI/user/import/job/{id} + * Spring MVC: PUT /WebAPI/v2/user/import/job/{id} + * + * @param jobId The job ID + * @param jobDTO The user import information + * @return The job information + */ + @PutMapping( + value = "/{id}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity updateJob( + @PathVariable("id") Long jobId, + @RequestBody UserImportJobDTO jobDTO) { + UserImportJob job = conversionService.convert(jobDTO, UserImportJob.class); + try { + job.setId(jobId); + UserImportJob updated = jobService.updateJob(job); + return ok(conversionService.convert(updated, UserImportJobDTO.class)); + } catch (JobNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + } + + /** + * Get the user import job list + * + * Jersey: GET /WebAPI/user/import/job/ + * Spring MVC: GET /WebAPI/v2/user/import/job/ + * + * @return The list of user import jobs + */ + @GetMapping( + value = "/", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @Transactional + public ResponseEntity> listJobs() { + return ok(jobService.getJobs().stream() + .map(job -> conversionService.convert(job, UserImportJobDTO.class)) + .peek(job -> jobService.getLatestHistoryItem(job.getId()) + .ifPresent(item -> job.setLastExecuted(item.getEndTime()))) + .collect(Collectors.toList())); + } + + /** + * Get user import job by ID + * + * Jersey: GET /WebAPI/user/import/job/{id} + * Spring MVC: GET /WebAPI/v2/user/import/job/{id} + * + * @param id The job ID + * @return The user import job + */ + @GetMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity getJob(@PathVariable("id") Long id) { + return jobService.getJob(id) + .map(job -> conversionService.convert(job, UserImportJobDTO.class)) + .map(this::ok) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + } + + /** + * Delete user import job by ID + * + * Jersey: DELETE /WebAPI/user/import/job/{id} + * Spring MVC: DELETE /WebAPI/v2/user/import/job/{id} + * + * @param id The job ID + */ + @DeleteMapping( + value = "/{id}" + ) + public ResponseEntity deleteJob(@PathVariable("id") Long id) { + UserImportJob job = jobService.getJob(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + jobService.delete(job); + return ok(); + } + + /** + * Get the user import job history + * + * Jersey: GET /WebAPI/user/import/job/{id}/history + * Spring MVC: GET /WebAPI/v2/user/import/job/{id}/history + * + * @param id The job ID + * @return The job history + */ + @GetMapping( + value = "/{id}/history", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity> getImportHistory(@PathVariable("id") Long id) { + return ok(jobService.getJobHistoryItems(id) + .map(item -> conversionService.convert(item, JobHistoryItemDTO.class)) + .collect(Collectors.toList())); + } +} diff --git a/src/main/java/org/ohdsi/webapi/user/importer/UserImportMvcController.java b/src/main/java/org/ohdsi/webapi/user/importer/UserImportMvcController.java new file mode 100644 index 0000000000..dddaf6270a --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/user/importer/UserImportMvcController.java @@ -0,0 +1,249 @@ +package org.ohdsi.webapi.user.importer; + +import org.ohdsi.analysis.Utils; +import org.ohdsi.webapi.arachne.scheduler.model.JobExecutingType; +import org.ohdsi.webapi.mvc.AbstractMvcController; +import org.ohdsi.webapi.user.importer.converter.RoleGroupMappingConverter; +import org.ohdsi.webapi.user.importer.dto.UserImportJobDTO; +import org.ohdsi.webapi.user.importer.exception.JobAlreadyExistException; +import org.ohdsi.webapi.user.importer.model.AtlasUserRoles; +import org.ohdsi.webapi.user.importer.model.AuthenticationProviders; +import org.ohdsi.webapi.user.importer.model.ConnectionInfo; +import org.ohdsi.webapi.user.importer.model.LdapGroup; +import org.ohdsi.webapi.user.importer.model.LdapProviderType; +import org.ohdsi.webapi.user.importer.model.RoleGroupEntity; +import org.ohdsi.webapi.user.importer.model.RoleGroupMapping; +import org.ohdsi.webapi.user.importer.model.UserImportJob; +import org.ohdsi.webapi.user.importer.service.UserImportJobService; +import org.ohdsi.webapi.user.importer.service.UserImportService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; + +import static org.ohdsi.webapi.Constants.JOB_IS_ALREADY_SCHEDULED; + +/** + * Spring MVC version of UserImportController + * + * Migration Status: Replaces /user/importer/UserImportController.java (Jersey) + * Endpoints: 6 endpoints (GET, POST) + * Complexity: Medium - user import from LDAP/AD with role mapping + */ +@RestController +@RequestMapping("/user") +public class UserImportMvcController extends AbstractMvcController { + + private static final Logger logger = LoggerFactory.getLogger(UserImportMvcController.class); + + private final UserImportService userImportService; + private final UserImportJobService userImportJobService; + private final GenericConversionService conversionService; + + @Value("${security.ad.url}") + private String adUrl; + + @Value("${security.ldap.url}") + private String ldapUrl; + + @Autowired + public UserImportMvcController(UserImportService userImportService, + UserImportJobService userImportJobService, + GenericConversionService conversionService) { + this.userImportService = userImportService; + this.userImportJobService = userImportJobService; + this.conversionService = conversionService; + } + + /** + * Get authentication providers + * + * Jersey: GET /WebAPI/user/providers + * Spring MVC: GET /WebAPI/v2/user/providers + * + * @return authentication providers configuration + */ + @GetMapping( + value = "/providers", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity getAuthenticationProviders() { + AuthenticationProviders providers = new AuthenticationProviders(); + providers.setAdUrl(adUrl); + providers.setLdapUrl(ldapUrl); + return ok(providers); + } + + /** + * Test connection to LDAP/AD provider + * + * Jersey: GET /WebAPI/user/import/{type}/test + * Spring MVC: GET /WebAPI/v2/user/import/{type}/test + * + * @param type provider type (ad or ldap) + * @return connection test result + */ + @GetMapping( + value = "/import/{type}/test", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity testConnection(@PathVariable("type") String type) { + LdapProviderType provider = LdapProviderType.fromValue(type); + ConnectionInfo result = new ConnectionInfo(); + userImportService.testConnection(provider); + result.setState(ConnectionInfo.ConnectionState.SUCCESS); + result.setMessage("Connection success"); + return ok(result); + } + + /** + * Find groups in LDAP/AD + * + * Jersey: GET /WebAPI/user/import/{type}/groups + * Spring MVC: GET /WebAPI/v2/user/import/{type}/groups + * + * @param type provider type (ad or ldap) + * @param searchStr search string + * @return list of LDAP groups + */ + @GetMapping( + value = "/import/{type}/groups", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity> findGroups( + @PathVariable("type") String type, + @RequestParam(value = "search", required = false) String searchStr) { + LdapProviderType provider = LdapProviderType.fromValue(type); + return ok(userImportService.findGroups(provider, searchStr)); + } + + /** + * Find users in directory + * + * Jersey: POST /WebAPI/user/import/{type} + * Spring MVC: POST /WebAPI/v2/user/import/{type} + * + * @param type provider type (ad or ldap) + * @param mapping role group mapping + * @return list of Atlas user roles + */ + @PostMapping( + value = "/import/{type}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity> findDirectoryUsers( + @PathVariable("type") String type, + @RequestBody RoleGroupMapping mapping) { + LdapProviderType provider = LdapProviderType.fromValue(type); + return ok(userImportService.findUsers(provider, mapping)); + } + + /** + * Import users from directory + * + * Jersey: POST /WebAPI/user/import + * Spring MVC: POST /WebAPI/v2/user/import + * + * @param users list of Atlas user roles to import + * @param provider provider type + * @param preserveRoles whether to preserve existing roles + * @return created user import job + */ + @PostMapping( + value = "/import", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity importUsers( + @RequestBody List users, + @RequestParam(value = "provider") String provider, + @RequestParam(value = "preserve", defaultValue = "TRUE") Boolean preserveRoles) { + LdapProviderType providerType = LdapProviderType.fromValue(provider); + + UserImportJobDTO jobDto = new UserImportJobDTO(); + jobDto.setProviderType(providerType); + jobDto.setPreserveRoles(preserveRoles); + jobDto.setEnabled(true); + jobDto.setStartDate(getJobStartDate()); + jobDto.setFrequency(JobExecutingType.ONCE); + jobDto.setRecurringTimes(0); + if (users != null) { + jobDto.setUserRoles(Utils.serialize(users)); + } + + try { + UserImportJob job = conversionService.convert(jobDto, UserImportJob.class); + UserImportJob created = userImportJobService.createJob(job); + return ok(conversionService.convert(created, UserImportJobDTO.class)); + } catch (JobAlreadyExistException e) { + throw new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, + String.format(JOB_IS_ALREADY_SCHEDULED, jobDto.getProviderType())); + } + } + + /** + * Save role group mapping + * + * Jersey: POST /WebAPI/user/import/{type}/mapping + * Spring MVC: POST /WebAPI/v2/user/import/{type}/mapping + * + * @param type provider type (ad or ldap) + * @param mapping role group mapping + */ + @PostMapping( + value = "/import/{type}/mapping", + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity saveMapping(@PathVariable("type") String type, @RequestBody RoleGroupMapping mapping) { + LdapProviderType providerType = LdapProviderType.fromValue(type); + List mappingEntities = RoleGroupMappingConverter.convertRoleGroupMapping(mapping); + userImportService.saveRoleGroupMapping(providerType, mappingEntities); + return ok(); + } + + /** + * Get role group mapping + * + * Jersey: GET /WebAPI/user/import/{type}/mapping + * Spring MVC: GET /WebAPI/v2/user/import/{type}/mapping + * + * @param type provider type (ad or ldap) + * @return role group mapping + */ + @GetMapping( + value = "/import/{type}/mapping", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity getMapping(@PathVariable("type") String type) { + LdapProviderType providerType = LdapProviderType.fromValue(type); + List mappingEntities = userImportService.getRoleGroupMapping(providerType); + return ok(RoleGroupMappingConverter.convertRoleGroupMapping(type, mappingEntities)); + } + + private Date getJobStartDate() { + Calendar calendar = GregorianCalendar.getInstance(); + // Job will be started in five seconds after now + calendar.add(Calendar.SECOND, 5); + calendar.set(Calendar.MILLISECOND, 0); + + return calendar.getTime(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cabbe0ecad..d4204cbdd2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -70,8 +70,10 @@ spring.jpa.properties.hibernate.order_inserts=${spring.jpa.properties.hibernate. spring.cache.jcache.config=classpath:appCache.xml spring.cache.type=${spring.cache.type} -#JAX-RS -jersey.resources.root.package=org.ohdsi.webapi +#JAX-RS (DISABLED - Jersey removed, using Spring MVC) +#jersey.resources.root.package=org.ohdsi.webapi +spring.jersey.application-path= +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration #Spring boot auto starts jobs upon application start spring.batch.job.enabled=false @@ -93,7 +95,7 @@ server.ssl.key-store = ${server.ssl.key-store} server.ssl.key-store-password = ${server.ssl.key-store-password} server.ssl.key-password = ${server.ssl.key-password} # the context path, defaults to '/' -server.context-path=/WebAPI +server.servlet.context-path=/WebAPI security.cas.loginUrl=${security.cas.loginUrl} security.cas.callbackUrl=${security.cas.callbackUrl} security.cas.serverUrl=${security.cas.serverUrl} diff --git a/src/test/java/org/ohdsi/webapi/test/ITStarter.java b/src/test/java/org/ohdsi/webapi/test/ITStarter.java index 94e17a5e8c..4413d8ff39 100644 --- a/src/test/java/org/ohdsi/webapi/test/ITStarter.java +++ b/src/test/java/org/ohdsi/webapi/test/ITStarter.java @@ -18,7 +18,7 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ - SecurityIT.class, + // SecurityIT.class, // DISABLED - Jersey-specific test JobServiceIT.class, CohortAnalysisServiceIT.class, VocabularyServiceIT.class, diff --git a/src/test/java/org/ohdsi/webapi/test/SecurityIT.java b/src/test/java/org/ohdsi/webapi/test/SecurityIT.java deleted file mode 100644 index fc1fec6b35..0000000000 --- a/src/test/java/org/ohdsi/webapi/test/SecurityIT.java +++ /dev/null @@ -1,219 +0,0 @@ -package org.ohdsi.webapi.test; - - -import com.github.springtestdbunit.DbUnitTestExecutionListener; -import com.github.springtestdbunit.annotation.DatabaseOperation; -import com.github.springtestdbunit.annotation.DatabaseTearDown; -import com.github.springtestdbunit.annotation.DbUnitConfiguration; -import com.google.common.collect.ImmutableMap; -import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; -import org.glassfish.jersey.server.model.Parameter; -import org.glassfish.jersey.server.model.Resource; -import org.glassfish.jersey.server.model.ResourceMethod; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.rules.ErrorCollector; -import org.junit.runner.RunWith; -import org.ohdsi.webapi.JerseyConfig; -import org.ohdsi.webapi.WebApi; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.*; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.TestExecutionListeners; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.web.util.UriComponentsBuilder; - -import jakarta.ws.rs.core.MediaType; -import java.io.IOException; -import java.net.URI; -import java.util.*; - -import static org.assertj.core.api.Java6Assertions.assertThat; -import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS; - -@RunWith(SpringRunner.class) -@SpringBootTest(classes = {WebApi.class, WebApiIT.DbUnitConfiguration.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("test") -@DbUnitConfiguration(databaseConnection = "dbUnitDatabaseConnection") -@TestExecutionListeners(value = {DbUnitTestExecutionListener.class}, mergeMode = MERGE_WITH_DEFAULTS) -@DatabaseTearDown(value = "/database/empty.xml", type = DatabaseOperation.DELETE_ALL) -@TestPropertySource(properties = {"security.provider=AtlasRegularSecurity"}) -public class SecurityIT { - - private final Map EXPECTED_RESPONSE_CODES = ImmutableMap.builder() - .put("/info/", HttpStatus.OK) - .put("/i18n/", HttpStatus.OK) - .put("/i18n/locales", HttpStatus.OK) - .put("/ddl/results", HttpStatus.OK) - .put("/ddl/cemresults", HttpStatus.OK) - .put("/ddl/achilles", HttpStatus.OK) - .put("/saml/saml-metadata", HttpStatus.OK) - .put("/saml/slo", HttpStatus.TEMPORARY_REDIRECT) - .build(); - - @Autowired - private TestRestTemplate restTemplate; - - @Autowired - private JerseyConfig jerseyConfig; - - @Value("${baseUri}") - private String baseUri; - - @Rule - public ErrorCollector collector = new ErrorCollector(); - - private final Logger LOG = LoggerFactory.getLogger(SecurityIT.class); - - @BeforeClass - public static void before() throws IOException { - TomcatURLStreamHandlerFactory.disable(); - ITStarter.before(); - } - - @AfterClass - public static void after() { - ITStarter.tearDownSubject(); - } - - @Test - @Ignore("Skipping due to Java 21 LDAP module access issues with AtlasRegularSecurity") - public void testServiceSecurity() { - - Map> serviceMap = getServiceMap(); - for (String servicePrefix : serviceMap.keySet()) { - List serviceInfos = serviceMap.get(servicePrefix); - for (ServiceInfo serviceInfo : serviceInfos) { - if (!serviceInfo.pathPrefix.startsWith("/")) { - serviceInfo.pathPrefix = "/" + serviceInfo.pathPrefix; - } - serviceInfo.pathPrefix = serviceInfo.pathPrefix.replaceAll("//", "/"); - String rawUrl = baseUri + serviceInfo.pathPrefix; - URI uri = null; - try { - Map parametersMap = prepareParameters(serviceInfo.parameters); - HttpEntity entity = new HttpEntity<>(new HttpHeaders()); - uri = UriComponentsBuilder.fromUriString(rawUrl) - .buildAndExpand(parametersMap).encode().toUri(); - LOG.info("testing service {}:{}", serviceInfo.httpMethod, uri); - ResponseEntity response = this.restTemplate.exchange(uri, serviceInfo.httpMethod, entity, - getResponseType(serviceInfo)); - LOG.info("tested service {}:{} with code {}", serviceInfo.httpMethod, uri, response.getStatusCode()); - HttpStatus expectedStatus = EXPECTED_RESPONSE_CODES.getOrDefault(serviceInfo.pathPrefix, HttpStatus.UNAUTHORIZED); - assertThat(response.getStatusCode()).isEqualTo(expectedStatus); - } catch (Throwable t) { - LOG.info("failed service {}:{}", serviceInfo.httpMethod, uri); - collector.addError(new ThrowableEx(t, rawUrl)); - } - } - } - } - - private Class getResponseType(ServiceInfo serviceInfo) { - - if (serviceInfo.mediaTypes.contains(MediaType.TEXT_PLAIN_TYPE)) { - return String.class; - } else if (serviceInfo.pathPrefix.equalsIgnoreCase("/saml/saml-metadata")) { - return Void.class; - } - return Object.class; - } - - private Map prepareParameters(List parameters) { - - Map parametersMap = new HashMap<>(); - if (parameters != null && !parameters.isEmpty()) { - for (Parameter parameter : parameters) { - String value = "0"; - // if parameter has classloader then it is of object type, else it is primitive type - if (parameter.getRawType().getClassLoader() != null) { - value = null; - } - parametersMap.put(parameter.getSourceName(), value); - } - } - return parametersMap; - } - - /* - * Retrieve information about rest services (path prefixes, http methods, parameters) - */ - private Map> getServiceMap() { - // - Set> classes = this.jerseyConfig.getClasses(); - Map> serviceMap = new HashMap<>(); - for (Class clazz : classes) { - Map> map = scan(clazz); - if (map != null) { - serviceMap.putAll(map); - } - } - - return serviceMap; - } - - private Map> scan(Class baseClass) { - - Resource.Builder builder = Resource.builder(baseClass); - if (null == builder) - return null; - Resource resource = builder.build(); - String uriPrefix = ""; - Map> info = new TreeMap<>(); - return process(uriPrefix, resource, info); - } - - private Map> process(String uriPrefix, Resource resource, Map> info) { - - String pathPrefix = uriPrefix; - List resources = new ArrayList<>(resource.getChildResources()); - if (resource.getPath() != null) { - pathPrefix = pathPrefix + resource.getPath(); - } - for (ResourceMethod method : resource.getAllMethods()) { - List serviceInfos = info.computeIfAbsent(pathPrefix, k -> new ArrayList<>()); - ServiceInfo serviceInfo = new ServiceInfo(); - serviceInfo.pathPrefix = pathPrefix; - serviceInfo.httpMethod = HttpMethod.valueOf(method.getHttpMethod()); - serviceInfo.parameters = method.getInvocable().getParameters(); - serviceInfo.mediaTypes = method.getProducedTypes(); - serviceInfos.add(serviceInfo); - } - for (Resource childResource : resources) { - process(pathPrefix, childResource, info); - } - return info; - } - - private static class ServiceInfo { - public String pathPrefix; - public HttpMethod httpMethod; - public List parameters; - public List mediaTypes; - } - - private static class ThrowableEx extends Throwable { - private final String serviceName; - - public ThrowableEx(Throwable throwable, String serviceName) { - - super(throwable); - this.serviceName = serviceName; - } - - @Override - public String getMessage() { - - return serviceName + ": " + super.getMessage(); - } - } -} diff --git a/src/test/java/org/ohdsi/webapi/test/migration/DualRuntimeTestSupport.java b/src/test/java/org/ohdsi/webapi/test/migration/DualRuntimeTestSupport.java new file mode 100644 index 0000000000..7c03f409b1 --- /dev/null +++ b/src/test/java/org/ohdsi/webapi/test/migration/DualRuntimeTestSupport.java @@ -0,0 +1,94 @@ +package org.ohdsi.webapi.test.migration; + +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; + +import static org.junit.Assert.*; + +/** + * Test support utilities for Spring MVC endpoints. + * Jersey has been removed - Spring MVC now serves all /WebAPI/* endpoints. + */ +public class DualRuntimeTestSupport { + + private final TestRestTemplate restTemplate; + private final String baseUri; + + public DualRuntimeTestSupport(TestRestTemplate restTemplate, String baseUri) { + this.restTemplate = restTemplate; + this.baseUri = baseUri; + } + + /** + * Test endpoint (deprecated - kept for backward compatibility) + * @deprecated Use verifyEndpoint instead + */ + @Deprecated + public void assertEndpointParity(String endpoint, Class responseType) { + verifyEndpoint(endpoint, 200, responseType); + } + + /** + * Test endpoint with request body (deprecated - kept for backward compatibility) + * @deprecated Use verifyEndpoint with custom assertions instead + */ + @Deprecated + public void assertEndpointParity(String endpoint, R requestBody, HttpMethod method, Class responseType) { + String url = baseUri + endpoint; + HttpEntity request = requestBody != null ? new HttpEntity<>(requestBody) : null; + ResponseEntity response = restTemplate.exchange(url, method, request, responseType); + assertNotNull("Response should not be null for endpoint: " + endpoint, response); + } + + /** + * Verify an endpoint exists and returns expected status + */ + public ResponseEntity verifyEndpoint(String endpoint, int expectedStatus, Class responseType) { + String url = baseUri + endpoint; + ResponseEntity response = restTemplate.getForEntity(url, responseType); + + assertEquals("Expected status code " + expectedStatus + " for endpoint: " + endpoint, + expectedStatus, response.getStatusCodeValue()); + + return response; + } + + /** + * Check if endpoint is available (returns non-404) + */ + public boolean isMvcEndpointAvailable(String endpoint) { + String url = baseUri + endpoint; + try { + ResponseEntity response = restTemplate.getForEntity(url, String.class); + return response.getStatusCodeValue() != 404; + } catch (Exception e) { + return false; + } + } + + /** + * Get the base URI for constructing test URLs + */ + public String getBaseUri() { + return baseUri; + } + + /** + * Get Jersey endpoint URL (deprecated - Jersey removed) + * @deprecated Use getMvcUrl instead + */ + @Deprecated + public String getJerseyUrl(String endpoint) { + return baseUri + endpoint; + } + + /** + * Get Spring MVC endpoint URL + * baseUri already includes context path (/WebAPI), so result is /WebAPI/endpoint + */ + public String getMvcUrl(String endpoint) { + return baseUri + endpoint; + } +} diff --git a/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase1IT.java b/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase1IT.java new file mode 100644 index 0000000000..00eac4c100 --- /dev/null +++ b/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase1IT.java @@ -0,0 +1,50 @@ +package org.ohdsi.webapi.test.migration; + +import org.junit.Test; +import org.ohdsi.webapi.test.WebApiIT; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.web.client.TestRestTemplate; + +/** + * Integration tests for Phase 1: Foundation & Parallel Runtime + * + * Verifies that: + * 1. Spring MVC is configured and running alongside Jersey + * 2. Both frameworks can handle requests independently + * 3. Configuration is correct for dual-runtime operation + */ +public class MigrationPhase1IT extends WebApiIT { + + @Value("${baseUri}") + private String baseUri; + + private final TestRestTemplate restTemplate = new TestRestTemplate(); + + @Test + public void testSpringMvcIsConfigured() { + // This test verifies that Spring MVC dispatcher servlet is active + // During migration, we use /v2/ prefix to avoid conflicts with Jersey + // A 404 for /WebAPI/v2/test is expected (no controllers yet), + // but it should be handled by Spring MVC, not Jersey + + log.info("Testing Spring MVC configuration..."); + log.info("Base URI: {}", baseUri); + + // This validates that Spring MVC is intercepting requests to /v2/ paths + // We're not testing a specific endpoint, just that the framework is active + } + + @Test + public void testJerseyStillWorks() { + // Verify that existing Jersey endpoints still function + String url = baseUri + "/WebAPI/info"; + + try { + var response = restTemplate.getForEntity(url, String.class); + log.info("Jersey /info endpoint returned status: {}", response.getStatusCode()); + // Should return 200 or appropriate status, not 404 + } catch (Exception e) { + log.info("Jersey endpoint test: {}", e.getMessage()); + } + } +} diff --git a/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase2IT.java b/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase2IT.java new file mode 100644 index 0000000000..9d439b877a --- /dev/null +++ b/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase2IT.java @@ -0,0 +1,94 @@ +package org.ohdsi.webapi.test.migration; + +import org.junit.Test; +import org.ohdsi.webapi.test.WebApiIT; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.junit.Assert.*; + +/** + * Integration tests for Phase 2: Provider Migration + * + * Verifies that: + * 1. GlobalExceptionHandler handles exceptions correctly (replaces GenericExceptionMapper) + * 2. JDBC connection exceptions are handled (replaces JdbcExceptionMapper) + * 3. LocaleInterceptor resolves locale correctly (replaces LocaleFilter) + * 4. OutputStreamMessageConverter works (replaces OutputStreamWriter) + */ +public class MigrationPhase2IT extends WebApiIT { + + @Value("${baseUri}") + private String baseUri; + + private final TestRestTemplate restTemplate = new TestRestTemplate(); + + @Test + public void testGlobalExceptionHandlerIsConfigured() { + log.info("Testing GlobalExceptionHandler configuration..."); + + // The @RestControllerAdvice annotation should be picked up by Spring + // This test verifies that Spring MVC exception handling is active + + // When we migrate a controller and it throws an exception, + // GlobalExceptionHandler should catch it and return proper error response + + // For now, just verify the handler class exists and is properly annotated + try { + Class.forName("org.ohdsi.webapi.mvc.GlobalExceptionHandler"); + log.info("GlobalExceptionHandler class found"); + } catch (ClassNotFoundException e) { + fail("GlobalExceptionHandler class not found"); + } + } + + @Test + public void testLocaleInterceptorIsConfigured() { + log.info("Testing LocaleInterceptor configuration..."); + + // The LocaleInterceptor should be registered in WebMvcConfig + // It should intercept requests and set locale based on headers/params + + // This will be fully testable once we have a migrated controller + // that uses locale-specific responses + + try { + Class.forName("org.ohdsi.webapi.i18n.mvc.LocaleInterceptor"); + log.info("LocaleInterceptor class found"); + } catch (ClassNotFoundException e) { + fail("LocaleInterceptor class not found"); + } + } + + @Test + public void testOutputStreamMessageConverterIsConfigured() { + log.info("Testing OutputStreamMessageConverter configuration..."); + + // The OutputStreamMessageConverter should be registered in WebMvcConfig + // It allows controllers to return ByteArrayOutputStream for downloads + + try { + Class.forName("org.ohdsi.webapi.mvc.OutputStreamMessageConverter"); + log.info("OutputStreamMessageConverter class found"); + } catch (ClassNotFoundException e) { + fail("OutputStreamMessageConverter class not found"); + } + } + + @Test + public void testExceptionHandlingWhenControllerNotFound() { + // Test that 404 errors are handled correctly by Spring MVC + // (Jersey has been removed) + + String url = baseUri + "/WebAPI/nonexistent-endpoint"; + ResponseEntity response = restTemplate.getForEntity(url, String.class); + + log.info("Response status for non-existent endpoint: {}", response.getStatusCode()); + + // Should get 404, not 500 + assertTrue("Should return 404 or similar client error", + response.getStatusCode().is4xxClientError()); + } +} diff --git a/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase3IT.java b/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase3IT.java new file mode 100644 index 0000000000..b2881074d6 --- /dev/null +++ b/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase3IT.java @@ -0,0 +1,165 @@ +package org.ohdsi.webapi.test.migration; + +import org.junit.Test; +import org.ohdsi.webapi.info.Info; +import org.ohdsi.webapi.test.WebApiIT; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.junit.Assert.*; + +/** + * Integration tests for Phase 3: Simple Controllers Migration + * + * Verifies that all 6 migrated simple controllers work correctly: + * 1. InfoMvcController + * 2. CacheMvcController + * 3. ActivityMvcController + * 4. SqlRenderMvcController + * 5. DDLMvcController + * 6. I18nMvcController + * + * Each test verifies: + * - Controller is accessible at /WebAPI/v2/* URL + * - Returns expected HTTP status + * - Response body is valid (where applicable) + */ +public class MigrationPhase3IT extends WebApiIT { + + private final TestRestTemplate restTemplate = new TestRestTemplate(); + private DualRuntimeTestSupport support; + + @Test + public void testInfoMvcController() { + log.info("Testing InfoMvcController..."); + + // Initialize support with baseUri from parent class + support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); + + String mvcUrl = support.getMvcUrl("/info/"); + ResponseEntity response = restTemplate.getForEntity(mvcUrl, Info.class); + + log.info("InfoMvcController status: {}", response.getStatusCode()); + assertEquals("Should return 200 OK", HttpStatus.OK, response.getStatusCode()); + + Info info = response.getBody(); + assertNotNull("Info should not be null", info); + assertNotNull("Version should not be null", info.getVersion()); + log.info("WebAPI version: {}", info.getVersion()); + } + + @Test + public void testCacheMvcController() { + log.info("Testing CacheMvcController..."); + + support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); + + // Test GET /cache/ + String mvcUrl = support.getMvcUrl("/cache/"); + ResponseEntity response = restTemplate.getForEntity(mvcUrl, String.class); + + log.info("CacheMvcController status: {}", response.getStatusCode()); + assertEquals("Should return 200 OK", HttpStatus.OK, response.getStatusCode()); + assertNotNull("Response body should not be null", response.getBody()); + } + + @Test + public void testActivityMvcController() { + log.info("Testing ActivityMvcController..."); + + support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); + + String mvcUrl = support.getMvcUrl("/activity/latest"); + ResponseEntity response = restTemplate.getForEntity(mvcUrl, Object[].class); + + log.info("ActivityMvcController status: {}", response.getStatusCode()); + assertEquals("Should return 200 OK", HttpStatus.OK, response.getStatusCode()); + assertNotNull("Activity array should not be null", response.getBody()); + } + + @Test + public void testSqlRenderMvcController() { + log.info("Testing SqlRenderMvcController..."); + + support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); + + // This requires a POST request with JSON body + // For now, just verify the endpoint exists + String mvcUrl = support.getMvcUrl("/sqlrender/translate"); + + // POST without body should still return a response (not 404) + ResponseEntity response = restTemplate.postForEntity(mvcUrl, null, String.class); + + log.info("SqlRenderMvcController status: {}", response.getStatusCode()); + // Should get 200 or 400, not 404 + assertTrue("Should not return 404", + response.getStatusCode() != HttpStatus.NOT_FOUND); + } + + @Test + public void testDDLMvcController() { + log.info("Testing DDLMvcController..."); + + support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); + + // Test GET /ddl/results + String mvcUrl = support.getMvcUrl("/ddl/results?dialect=postgresql&schema=results"); + ResponseEntity response = restTemplate.getForEntity(mvcUrl, String.class); + + log.info("DDLMvcController status: {}", response.getStatusCode()); + assertEquals("Should return 200 OK", HttpStatus.OK, response.getStatusCode()); + assertNotNull("DDL SQL should not be null", response.getBody()); + assertTrue("DDL should contain SQL", response.getBody().length() > 0); + } + + @Test + public void testI18nMvcController() { + log.info("Testing I18nMvcController..."); + + support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); + + // Test GET /i18n/ + String mvcUrl = support.getMvcUrl("/i18n/"); + ResponseEntity response = restTemplate.getForEntity(mvcUrl, String.class); + + log.info("I18nMvcController status: {}", response.getStatusCode()); + assertEquals("Should return 200 OK", HttpStatus.OK, response.getStatusCode()); + assertNotNull("i18n resources should not be null", response.getBody()); + + // Test GET /i18n/locales + mvcUrl = support.getMvcUrl("/i18n/locales"); + response = restTemplate.getForEntity(mvcUrl, String.class); + + assertEquals("Should return 200 OK for locales", HttpStatus.OK, response.getStatusCode()); + assertNotNull("Locales should not be null", response.getBody()); + } + + @Test + public void testAllMigratedControllersAccessible() { + log.info("Testing all Phase 3 controllers are accessible..."); + + support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); + + String[] endpoints = { + "/info/", + "/cache/", + "/activity/latest", + "/ddl/results", + "/i18n/", + "/i18n/locales" + }; + + int successCount = 0; + for (String endpoint : endpoints) { + if (support.isMvcEndpointAvailable(endpoint)) { + successCount++; + log.info("✓ {} is available", endpoint); + } else { + log.warn("✗ {} is NOT available", endpoint); + } + } + + assertEquals("All 6 endpoints should be accessible", endpoints.length, successCount); + } +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 036d21c02e..d1604e3553 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -1,4 +1,4 @@ -baseUri=http://localhost:${local.server.port}${server.context-path} +baseUri=http://localhost:${local.server.port}${server.servlet.context-path} security.db.datasource.url=http://localhost:${datasource.url}/arachne_portal_enterprise vocabularyservice.endpoint=${baseUri}/vocabulary cdmResultsService.endpoint=${baseUri}/cdmresults From fdc162cb1bc0f0cbb40971831acc9cc1a7057cbd Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 22 Nov 2025 18:05:51 +0800 Subject: [PATCH 02/18] cleanup --- pom.xml | 107 +--- .../org/ohdsi/webapi/JdbcExceptionMapper.java | 23 - .../webapi/PageableValueFactoryProvider.java | 109 ---- .../audittrail/AuditTrailServiceImpl.java | 6 +- .../org/ohdsi/webapi/cache/CacheService.java | 94 --- .../cohortanalysis/CohortAnalysisTasklet.java | 7 +- .../cohortsample/CohortSamplingService.java | 16 +- .../cohortsample/dto/SampleParametersDTO.java | 25 +- .../ExampleApplicationWithJobService.java | 222 ------- .../org/ohdsi/webapi/i18n/I18nController.java | 61 -- .../ohdsi/webapi/i18n/I18nServiceImpl.java | 5 +- .../org/ohdsi/webapi/i18n/LocaleFilter.java | 35 -- .../org/ohdsi/webapi/info/InfoService.java | 8 - .../org/ohdsi/webapi/job/JobTemplate.java | 21 +- .../webapi/job/NotificationController.java | 117 ---- .../webapi/mvc/GlobalExceptionHandler.java | 28 +- .../org/ohdsi/webapi/mvc/MigrationUtils.java | 16 +- .../ohdsi/webapi/mvc/ResponseConverters.java | 33 +- .../CohortResultsMvcController.java | 4 +- .../webapi/reusable/ReusableController.java | 231 ------- .../webapi/security/PermissionController.java | 196 ------ .../ohdsi/webapi/security/SSOController.java | 96 --- .../webapi/service/AbstractDaoService.java | 14 +- .../ohdsi/webapi/service/ActivityService.java | 49 -- .../webapi/service/CDMResultsService.java | 76 +-- .../webapi/service/CohortAnalysisService.java | 27 +- .../service/CohortDefinitionService.java | 182 ++---- .../webapi/service/CohortResultsService.java | 576 +++++++----------- .../webapi/service/CohortSampleService.java | 95 ++- .../ohdsi/webapi/service/CohortService.java | 17 +- .../webapi/service/ConceptSetService.java | 189 ++---- .../org/ohdsi/webapi/service/DDLService.java | 33 +- .../ohdsi/webapi/service/EvidenceService.java | 150 ++--- .../webapi/service/FeasibilityService.java | 60 +- .../org/ohdsi/webapi/service/HttpClient.java | 49 -- .../org/ohdsi/webapi/service/JobService.java | 43 +- .../webapi/service/SqlRenderService.java | 10 - .../org/ohdsi/webapi/service/UserService.java | 84 +-- .../webapi/service/VocabularyService.java | 283 ++------- .../filters/UpdateAccessTokenFilter.java | 7 +- .../datasource/BaseDataSourceAccessor.java | 5 +- .../ohdsi/webapi/source/SourceController.java | 415 ------------- .../controller/StatisticController.java | 261 -------- .../org/ohdsi/webapi/tag/TagController.java | 161 ----- .../org/ohdsi/webapi/tag/TagGroupService.java | 7 +- .../ohdsi/webapi/tag/TagSecurityUtils.java | 5 +- .../java/org/ohdsi/webapi/tag/TagService.java | 8 +- .../org/ohdsi/webapi/tool/ToolController.java | 62 -- .../user/importer/UserImportController.java | 161 ----- .../importer/UserImportJobController.java | 163 ----- .../org/ohdsi/webapi/util/ExceptionUtils.java | 8 +- .../webapi/util/GenericExceptionMapper.java | 119 ---- .../java/org/ohdsi/webapi/util/HttpUtils.java | 18 +- .../ohdsi/webapi/util/OutputStreamWriter.java | 41 -- .../versioning/service/VersionService.java | 7 +- .../webapi/tagging/ReusableTaggingTest.java | 12 +- .../org/ohdsi/webapi/test/JobServiceIT.java | 4 +- .../webapi/test/SecurityIT.java.disabled | 219 +++++++ 58 files changed, 843 insertions(+), 4237 deletions(-) delete mode 100644 src/main/java/org/ohdsi/webapi/JdbcExceptionMapper.java delete mode 100644 src/main/java/org/ohdsi/webapi/PageableValueFactoryProvider.java delete mode 100644 src/main/java/org/ohdsi/webapi/cache/CacheService.java delete mode 100644 src/main/java/org/ohdsi/webapi/exampleapplication/ExampleApplicationWithJobService.java delete mode 100644 src/main/java/org/ohdsi/webapi/i18n/I18nController.java delete mode 100644 src/main/java/org/ohdsi/webapi/i18n/LocaleFilter.java delete mode 100644 src/main/java/org/ohdsi/webapi/job/NotificationController.java delete mode 100644 src/main/java/org/ohdsi/webapi/reusable/ReusableController.java delete mode 100644 src/main/java/org/ohdsi/webapi/security/PermissionController.java delete mode 100644 src/main/java/org/ohdsi/webapi/security/SSOController.java delete mode 100644 src/main/java/org/ohdsi/webapi/service/ActivityService.java delete mode 100644 src/main/java/org/ohdsi/webapi/service/HttpClient.java delete mode 100644 src/main/java/org/ohdsi/webapi/source/SourceController.java delete mode 100644 src/main/java/org/ohdsi/webapi/statistic/controller/StatisticController.java delete mode 100644 src/main/java/org/ohdsi/webapi/tag/TagController.java delete mode 100644 src/main/java/org/ohdsi/webapi/tool/ToolController.java delete mode 100644 src/main/java/org/ohdsi/webapi/user/importer/UserImportController.java delete mode 100644 src/main/java/org/ohdsi/webapi/user/importer/UserImportJobController.java delete mode 100644 src/main/java/org/ohdsi/webapi/util/GenericExceptionMapper.java delete mode 100644 src/main/java/org/ohdsi/webapi/util/OutputStreamWriter.java create mode 100644 src/test/java/org/ohdsi/webapi/test/SecurityIT.java.disabled diff --git a/pom.xml b/pom.xml index 8c46083e64..526ba865c3 100644 --- a/pom.xml +++ b/pom.xml @@ -22,10 +22,7 @@ 2.0.2 - 1.5 - 1.12.1 - 3.1.9 1.19.1 3.1.2 6.0.5 @@ -199,7 +196,6 @@ /WebAPI 1.17.4 - 3.1.9 600000 12 10000 @@ -718,28 +714,6 @@ tomcat-juli 10.1.48 - - org.springframework.boot - spring-boot-starter-jersey - - - org.hibernate - hibernate-validator - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.core - jackson-core - - - org.springframework.boot spring-boot-starter-data-jpa @@ -790,17 +764,7 @@ cache-api 1.1.1 - - - javax.xml.bind - jaxb-api - 2.3.1 - - - org.glassfish.jaxb - jaxb-runtime - 2.3.1 - + org.ohdsi.sql SqlRender @@ -894,13 +858,7 @@ 9.1.6 - - - commons-fileupload - commons-fileupload - ${commons-fileupload.version} - - + org.postgresql postgresql @@ -912,11 +870,7 @@ mssql-jdbc 12.8.1.jre11 - - com.microsoft.azure - msal4j - 1.9.0 - + com.opencsv opencsv @@ -934,11 +888,7 @@ commons-csv 1.8 - - org.dom4j - dom4j - 2.1.3 - + org.apache.shiro shiro-core @@ -1106,10 +1056,12 @@ jasypt 1.9.3 + + - org.glassfish.jersey.media - jersey-media-multipart - ${jersey-media-multipart.version} + jakarta.xml.bind + jakarta.xml.bind-api + 4.0.0 org.glassfish @@ -1191,7 +1143,20 @@ org.ehcache ehcache - 3.9.11 + 3.11.1 + jakarta + + + org.glassfish.jaxb + jaxb-runtime + + + + + + org.glassfish.jaxb + jaxb-runtime + 3.0.2 com.opentable.components otj-pg-embedded @@ -1778,32 +1743,6 @@ - - com.sun.jersey - jersey-server - 1.19.4 - - - com.sun.jersey - jersey-core - 1.19.4 - - - javax.ws.rs - jsr311-api - - - - - com.sun.jersey - jersey-client - 1.19.4 - - - com.sun.jersey - jersey-json - 1.19.4 - diff --git a/src/main/java/org/ohdsi/webapi/JdbcExceptionMapper.java b/src/main/java/org/ohdsi/webapi/JdbcExceptionMapper.java deleted file mode 100644 index 230ca8baf4..0000000000 --- a/src/main/java/org/ohdsi/webapi/JdbcExceptionMapper.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.ohdsi.webapi; - -import org.ohdsi.webapi.arachne.logging.event.FailedDbConnectEvent; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.jdbc.CannotGetJdbcConnectionException; - -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.ext.ExceptionMapper; -import jakarta.ws.rs.ext.Provider; - -@Provider -public class JdbcExceptionMapper implements ExceptionMapper { - - @Autowired - private ApplicationEventPublisher eventPublisher; - - @Override - public Response toResponse(CannotGetJdbcConnectionException exception) { - eventPublisher.publishEvent(new FailedDbConnectEvent(this, exception.getMessage())); - return Response.ok().build(); - } -} \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/PageableValueFactoryProvider.java b/src/main/java/org/ohdsi/webapi/PageableValueFactoryProvider.java deleted file mode 100644 index 6f985e799b..0000000000 --- a/src/main/java/org/ohdsi/webapi/PageableValueFactoryProvider.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * - * 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; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; -import jakarta.inject.Inject; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.QueryParam; -import org.glassfish.hk2.api.ServiceLocator; -import org.glassfish.jersey.server.model.Parameter; -import org.glassfish.jersey.server.spi.internal.ValueParamProvider; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; - -/** - * Jersey 3 value provider for Spring Data Pageable injection. - * - * Extracts pagination parameters from query strings: - * - page: Page number (0-indexed, default: 0) - * - size: Page size (default: 10) - * - sort: Sort specification in format "property,direction" (e.g., "name,asc") - */ -public class PageableValueFactoryProvider implements ValueParamProvider { - - private static final int DEFAULT_PAGE = 0; - private static final int DEFAULT_SIZE = 10; - - private final ServiceLocator locator; - - @Inject - public PageableValueFactoryProvider(ServiceLocator locator) { - this.locator = locator; - } - - @Override - public Function getValueProvider(Parameter parameter) { - if (parameter.getRawType() == Pageable.class - && parameter.isAnnotationPresent(Pagination.class)) { - return this::extractPageable; - } - return null; - } - - private Pageable extractPageable(org.glassfish.jersey.server.ContainerRequest request) { - int page = getQueryParamAsInt(request, "page", DEFAULT_PAGE); - int size = getQueryParamAsInt(request, "size", DEFAULT_SIZE); - - List sortParams = request.getUriInfo().getQueryParameters().get("sort"); - Sort sort = parseSort(sortParams); - - return PageRequest.of(page, size, sort); - } - - private int getQueryParamAsInt(org.glassfish.jersey.server.ContainerRequest request, String param, int defaultValue) { - String value = request.getUriInfo().getQueryParameters().getFirst(param); - if (value != null) { - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return defaultValue; - } - } - return defaultValue; - } - - private Sort parseSort(List sortParams) { - if (sortParams == null || sortParams.isEmpty()) { - return Sort.unsorted(); - } - - List orders = new ArrayList<>(); - for (String sortParam : sortParams) { - String[] parts = sortParam.split(","); - String property = parts[0].trim(); - Sort.Direction direction = Sort.Direction.ASC; - - if (parts.length > 1) { - String directionStr = parts[1].trim().toUpperCase(); - if ("DESC".equals(directionStr)) { - direction = Sort.Direction.DESC; - } - } - - orders.add(new Sort.Order(direction, property)); - } - - return Sort.by(orders); - } - - @Override - public PriorityType getPriority() { - return Priority.NORMAL; - } -} \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/audittrail/AuditTrailServiceImpl.java b/src/main/java/org/ohdsi/webapi/audittrail/AuditTrailServiceImpl.java index 25935a6d9c..8d2fe009ad 100644 --- a/src/main/java/org/ohdsi/webapi/audittrail/AuditTrailServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/audittrail/AuditTrailServiceImpl.java @@ -13,7 +13,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; -import jakarta.ws.rs.core.Response; +import org.springframework.http.ResponseEntity; import java.io.File; import java.util.Collection; import java.util.Map; @@ -173,9 +173,9 @@ private String getAdditionalInfo(final AuditTrailEntry entry) { } // File entry log - if (entry.getReturnedObject() instanceof Response) { + if (entry.getReturnedObject() instanceof ResponseEntity) { try { - final Object entity = ((Response) entry.getReturnedObject()).getEntity(); + final Object entity = ((ResponseEntity) entry.getReturnedObject()).getBody(); if (entity instanceof File) { final File file = (File) entity; return String.format(FILE_TEMPLATE, file.getName(), file.length()); diff --git a/src/main/java/org/ohdsi/webapi/cache/CacheService.java b/src/main/java/org/ohdsi/webapi/cache/CacheService.java deleted file mode 100644 index e9fa67efd8..0000000000 --- a/src/main/java/org/ohdsi/webapi/cache/CacheService.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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 jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.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/cohortanalysis/CohortAnalysisTasklet.java b/src/main/java/org/ohdsi/webapi/cohortanalysis/CohortAnalysisTasklet.java index 042471a2e3..5d8357f92f 100644 --- a/src/main/java/org/ohdsi/webapi/cohortanalysis/CohortAnalysisTasklet.java +++ b/src/main/java/org/ohdsi/webapi/cohortanalysis/CohortAnalysisTasklet.java @@ -27,7 +27,8 @@ import org.springframework.transaction.TransactionException; import org.springframework.transaction.support.TransactionTemplate; -import jakarta.ws.rs.NotFoundException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; public class CohortAnalysisTasklet implements Tasklet { @@ -98,7 +99,7 @@ public RepeatStatus execute(final StepContribution contribution, final ChunkCont CohortDefinition cohortDef = cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null); CohortAnalysisGenerationInfo info = cohortDef.getCohortAnalysisGenerationInfoList().stream() .filter(a -> a.getSourceId() == task.getSource().getSourceId()) - .findFirst().orElseThrow(NotFoundException::new); + .findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); info.setProgress(progress); cohortDefinitionRepository.save(cohortDef); return null; @@ -127,7 +128,7 @@ public RepeatStatus execute(final StepContribution contribution, final ChunkCont CohortDefinition cohortDef = cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null); CohortAnalysisGenerationInfo info = cohortDef.getCohortAnalysisGenerationInfoList().stream() .filter(a -> a.getSourceId() == task.getSource().getSourceId()) - .findFirst().orElseThrow(NotFoundException::new); + .findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); info.setExecutionDuration((int)(Calendar.getInstance().getTime().getTime()- info.getLastExecution().getTime())); if (f_successful) { diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/CohortSamplingService.java b/src/main/java/org/ohdsi/webapi/cohortsample/CohortSamplingService.java index a9ce09ffca..a9240814db 100644 --- a/src/main/java/org/ohdsi/webapi/cohortsample/CohortSamplingService.java +++ b/src/main/java/org/ohdsi/webapi/cohortsample/CohortSamplingService.java @@ -18,8 +18,10 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionCallback; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.NotFoundException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -69,7 +71,7 @@ public List listSamples(int cohortDefinitionId, int sourceId) { public CohortSampleDTO getSample(int sampleId, boolean withRecordCounts) { CohortSample sample = sampleRepository.findById(sampleId); if (sample == null) { - throw new NotFoundException("Cohort sample with ID " + sampleId + " not found"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort sample with ID " + sampleId + " not found"); } Source source = getSourceRepository().findBySourceId(sample.getSourceId()); List sampleElements = findSampleElements(source, sample.getId(), withRecordCounts); @@ -198,7 +200,7 @@ public void refreshSample(Integer sampleId) { CohortSample sample = sampleRepository.findById(sampleId).orElse(null); if (sample == null) { - throw new NotFoundException("Cohort sample with ID " + sampleId + " not found"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort sample with ID " + sampleId + " not found"); } Source source = getSourceRepository().findBySourceId(sample.getSourceId()); @@ -454,13 +456,13 @@ public void deleteSample(int cohortDefinitionId, Source source, int sampleId) { sampleId).getSql(); CohortSample sample = sampleRepository.findById(sampleId); if (sample == null) { - throw new NotFoundException("Sample with ID " + sampleId + " does not exist"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Sample with ID " + sampleId + " does not exist"); } if (sample.getCohortDefinitionId() != cohortDefinitionId) { - throw new BadRequestException("Cohort definition ID " + sample.getCohortDefinitionId() + " does not match provided cohort definition id " + cohortDefinitionId); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cohort definition ID " + sample.getCohortDefinitionId() + " does not match provided cohort definition id " + cohortDefinitionId); } if (sample.getSourceId() != source.getId()) { - throw new BadRequestException("Source " + sample.getSourceId() + " does not match provided source " + source.getId()); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Source " + sample.getSourceId() + " does not match provided source " + source.getId()); } getTransactionTemplate().execute((TransactionCallback) transactionStatus -> { diff --git a/src/main/java/org/ohdsi/webapi/cohortsample/dto/SampleParametersDTO.java b/src/main/java/org/ohdsi/webapi/cohortsample/dto/SampleParametersDTO.java index 2ccbdea9c4..2891d42765 100644 --- a/src/main/java/org/ohdsi/webapi/cohortsample/dto/SampleParametersDTO.java +++ b/src/main/java/org/ohdsi/webapi/cohortsample/dto/SampleParametersDTO.java @@ -3,7 +3,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonValue; -import jakarta.ws.rs.BadRequestException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -30,13 +31,13 @@ public class SampleParametersDTO { */ public void validate() { if (name == null) { - throw new BadRequestException("Sample must have a name"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Sample must have a name"); } if (size <= 0) { - throw new BadRequestException("sample parameter size must fall in the range (1, " + SIZE_MAX + ")"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sample parameter size must fall in the range (1, " + SIZE_MAX + ")"); } if (size > SIZE_MAX) { - throw new BadRequestException("sample parameter size must fall in the range (1, " + SIZE_MAX + ")"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sample parameter size must fall in the range (1, " + SIZE_MAX + ")"); } if (age != null && !age.validate()) { age = null; @@ -182,7 +183,7 @@ public static class AgeDTO { public boolean validate() { if (mode == null) { if (min != null || max != null || value != null) { - throw new BadRequestException("Cannot specify age without a mode to use age with."); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot specify age without a mode to use age with."); } else { return false; } @@ -194,28 +195,28 @@ public boolean validate() { case GREATER_THAN_OR_EQUAL: case EQUAL_TO: if (value == null) { - throw new BadRequestException("Cannot use single age comparison mode " + mode.getSerialName() + " without age property."); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot use single age comparison mode " + mode.getSerialName() + " without age property."); } if (min != null || max != null) { - throw new BadRequestException("Cannot use age range property with comparison mode " + mode.getSerialName() + "."); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot use age range property with comparison mode " + mode.getSerialName() + "."); } break; case BETWEEN: case NOT_BETWEEN: if (min == null || max == null) { - throw new BadRequestException("Cannot use age range comparison mode " + mode.getSerialName() + " without ageMin and ageMax properties."); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot use age range comparison mode " + mode.getSerialName() + " without ageMin and ageMax properties."); } if (value != null) { - throw new BadRequestException("Cannot use single age property with comparison mode " + mode.getSerialName() + "."); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot use single age property with comparison mode " + mode.getSerialName() + "."); } if (min < 0) { - throw new BadRequestException("Minimum age may not be less than 0"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Minimum age may not be less than 0"); } if (max >= AGE_MAX) { - throw new BadRequestException("Maximum age must be smaller than " + AGE_MAX); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Maximum age must be smaller than " + AGE_MAX); } if (min > max) { - throw new BadRequestException("Maximum age " + max + " may not be less than minimum age " + min); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Maximum age " + max + " may not be less than minimum age " + min); } break; } diff --git a/src/main/java/org/ohdsi/webapi/exampleapplication/ExampleApplicationWithJobService.java b/src/main/java/org/ohdsi/webapi/exampleapplication/ExampleApplicationWithJobService.java deleted file mode 100644 index 92cd3e81d5..0000000000 --- a/src/main/java/org/ohdsi/webapi/exampleapplication/ExampleApplicationWithJobService.java +++ /dev/null @@ -1,222 +0,0 @@ -package org.ohdsi.webapi.exampleapplication; - -import org.apache.commons.lang3.RandomStringUtils; -import org.ohdsi.circe.vocabulary.Concept; -import org.ohdsi.webapi.exampleapplication.model.Widget; -import org.ohdsi.webapi.exampleapplication.repository.WidgetRepository; -import org.ohdsi.webapi.job.JobExecutionResource; -import org.ohdsi.webapi.job.JobTemplate; -import org.ohdsi.webapi.service.AbstractDaoService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.transaction.TransactionException; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; - -import jakarta.persistence.EntityManager; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import java.util.ArrayList; -import java.util.List; - -import static org.ohdsi.webapi.util.SecurityUtils.whitelist; - - /** - * Example REST service - will be depreciated - * in a future release - * - * @deprecated - * @summary Example - */ -@Path("/example") -public class ExampleApplicationWithJobService extends AbstractDaoService { - - public static final String EXAMPLE_JOB_NAME = "OhdsiExampleJob"; - - public static final String EXAMPLE_STEP_NAME = "OhdsiExampleStep"; - - @Autowired - private JobTemplate jobTemplate; - - @Autowired - private WidgetRepository widgetRepository; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Autowired - private EntityManager em; - - public static class ExampleApplicationTasklet implements Tasklet { - - private static final Logger log = LoggerFactory.getLogger(ExampleApplicationTasklet.class); - - private final List concepts; - - public ExampleApplicationTasklet(final List concepts) { - this.concepts = concepts; - } - - @Override - public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext) throws Exception { - // set contextual data in JobExecutionContext - chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext() - .put("concepts", this.concepts); - log.info("Tasklet execution >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - // Thread.sleep(14000L); - return RepeatStatus.FINISHED; - } - } - - /** - * Example REST service - will be depreciated - * in a future release - * - * @deprecated - * @summary DO NOT USE - */ - @POST - @Produces(MediaType.APPLICATION_JSON) - public JobExecutionResource queueJob() throws Exception { - //Allow unique combinations of JobParameters to run in parallel. An empty JobParameters() would only allow a JobInstance to run at a time. - final JobParameters jobParameters = new JobParametersBuilder().addString("param", "parameter with 250 char limit") - .addLong("time", System.currentTimeMillis()).toJobParameters(); - final List concepts = new ArrayList(); - final Concept c1 = new Concept(); - c1.conceptName = "c1"; - final Concept c2 = new Concept(); - c2.conceptName = "c2"; - concepts.add(c1); - concepts.add(c2); - return this.jobTemplate.launchTasklet(EXAMPLE_JOB_NAME, EXAMPLE_STEP_NAME, new ExampleApplicationTasklet(concepts), - jobParameters); - } - - /** - * Example REST service - will be depreciated - * in a future release - * - * @deprecated - * @summary DO NOT USE - */ - @GET - @Path("widget") - @Produces(MediaType.APPLICATION_JSON) - public List findAllWidgets() { - Page page = this.widgetRepository.findAll(PageRequest.of(0, 10)); - return page.getContent(); - } - - /** - * Example REST service - will be depreciated - * in a future release - * - * @deprecated - * @summary DO NOT USE - */ - @POST - @Path("widget") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - //Wrapping in transaction (e.g. TransactionTemplate) not necessary as SimpleJpaRepository.save is annotated with @Transactional. - public Widget createWidget(Widget w) { - return this.widgetRepository.save(w); - } - - private List createWidgets() { - List widgets = new ArrayList(); - for (int x = 0; x < 20; x++) { - Widget w = new Widget(); - w.setName(RandomStringUtils.randomAlphanumeric(10)); - widgets.add(w); - } - return widgets; - } - - /** - * Example REST service - will be depreciated - * in a future release - * - * @deprecated - * @summary DO NOT USE - */ - @POST - @Path("widgets/batch") - public void batchWriteWidgets() { - final List widgets = createWidgets(); - this.transactionTemplate.execute(new TransactionCallback() { - @Override - public Void doInTransaction(TransactionStatus status) { - int i = 0; - for (Widget w : widgets) { - em.persist(w); - if (i % 5 == 0) { //5, same as the JDBC batch size - //flush a batch of inserts and release memory: - log.info("Flushing, clearing"); - em.flush(); - em.clear(); - } - i++; - } - return null; - } - }); - log.info("Persisted {} widgets", widgets.size()); - } - - /** - * Example REST service - will be depreciated - * in a future release - * - * @deprecated - * @summary DO NOT USE - */ - @POST - @Path("widgets") - public void writeWidgets() { - final List widgets = createWidgets(); - this.widgetRepository.saveAll(widgets); - log.info("Persisted {} widgets", widgets.size()); - } - - /** - * Example REST service - will be depreciated - * in a future release - * - * @deprecated - * @param w DO NOT USE - * @summary DO NOT USE - */ - @POST - @Path("widget2") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - //@Transactional do not work with JAX-RS default config. Review caveots with @Transactional usage (proxy requirements). - //Note that SimpleJpaRepository.save is annotated with @Transactional and will use default (e.g. Propagations.REQUIRES). Illustration of deviating from default propagation. - public Widget createWidgetWith(final Widget w) { - try { - final Widget ret = getTransactionTemplateRequiresNew().execute(new TransactionCallback() { - - @Override - public Widget doInTransaction(final TransactionStatus status) { - return widgetRepository.save(w); - } - }); - return ret; - } catch (final TransactionException e) { - log.error(whitelist(e)); - throw e; - } - - } -} diff --git a/src/main/java/org/ohdsi/webapi/i18n/I18nController.java b/src/main/java/org/ohdsi/webapi/i18n/I18nController.java deleted file mode 100644 index 18f27f4fd5..0000000000 --- a/src/main/java/org/ohdsi/webapi/i18n/I18nController.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.ohdsi.webapi.i18n; - -import com.google.common.collect.ImmutableList; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Controller; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import java.util.List; -import java.util.Locale; -import java.util.Objects; - -@Path("/i18n/") -@Controller -public class I18nController { - - @Value("${i18n.enabled}") - private boolean i18nEnabled = true; - - @Value("${i18n.defaultLocale}") - private String defaultLocale = "en"; - - @Autowired - private I18nService i18nService; - - @GET - @Path("/") - @Produces(MediaType.APPLICATION_JSON) - public Response getResources(@Context ContainerRequestContext requestContext) { - - Locale locale = (Locale) requestContext.getProperty("language"); - if (!this.i18nEnabled || locale == null || !isLocaleSupported(locale.getLanguage())) { - locale = Locale.forLanguageTag(defaultLocale); - } - String messages = i18nService.getLocaleResource(locale); - return Response.ok(messages).build(); - } - - private boolean isLocaleSupported(String code) { - - return i18nService.getAvailableLocales().stream().anyMatch(l -> Objects.equals(code, l.getCode())); - } - - @GET - @Path("/locales") - @Produces(MediaType.APPLICATION_JSON) - public List getAvailableLocales() { - if (this.i18nEnabled) { - return i18nService.getAvailableLocales(); - } - - // if i18n is disabled, then return only default locale - return ImmutableList.of(new LocaleDTO(this.defaultLocale, null, true)); - } -} diff --git a/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java b/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java index da1eabf668..cc9ce89690 100644 --- a/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java @@ -8,7 +8,8 @@ import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; -import jakarta.ws.rs.InternalServerErrorException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; import java.io.IOException; import java.net.URL; import java.util.Collections; @@ -53,7 +54,7 @@ public String translate(String key, String defaultValue) { JsonNode node = root.at(pointer); return node.isValueNode() ? node.asText() : defaultValue; }catch (IOException e) { - throw new InternalServerErrorException(e); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage(), e); } } diff --git a/src/main/java/org/ohdsi/webapi/i18n/LocaleFilter.java b/src/main/java/org/ohdsi/webapi/i18n/LocaleFilter.java deleted file mode 100644 index 6b881f3b57..0000000000 --- a/src/main/java/org/ohdsi/webapi/i18n/LocaleFilter.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.ohdsi.webapi.i18n; - -import org.apache.commons.lang3.StringUtils; -import org.ohdsi.webapi.Constants; -import org.springframework.context.i18n.LocaleContextHolder; - -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.ext.Provider; -import java.util.Locale; - -@Provider -public class LocaleFilter implements ContainerRequestFilter { - - private String ACCEPT_LANGUAGE_HEADER = "Accept-Language"; - private String LANG_PARAM = "lang"; - - private String defaultLocale = "en"; - - @Override - public void filter(ContainerRequestContext requestContext) { - - Locale locale = Locale.forLanguageTag(defaultLocale); - String userHeader = requestContext.getHeaderString(Constants.Headers.USER_LANGAUGE); - if (StringUtils.isNotBlank(userHeader)) { - locale = Locale.forLanguageTag(userHeader); - } else if (requestContext.getUriInfo().getQueryParameters().containsKey(LANG_PARAM)) { - locale = Locale.forLanguageTag(requestContext.getUriInfo().getQueryParameters().getFirst(LANG_PARAM)); - } else if (requestContext.getHeaderString(ACCEPT_LANGUAGE_HEADER) != null) { - locale = Locale.forLanguageTag(requestContext.getHeaderString(ACCEPT_LANGUAGE_HEADER)); - } - requestContext.setProperty("language", locale); - LocaleContextHolder.setLocale(locale); - } -} diff --git a/src/main/java/org/ohdsi/webapi/info/InfoService.java b/src/main/java/org/ohdsi/webapi/info/InfoService.java index e822974beb..01445aba3f 100644 --- a/src/main/java/org/ohdsi/webapi/info/InfoService.java +++ b/src/main/java/org/ohdsi/webapi/info/InfoService.java @@ -18,10 +18,6 @@ package org.ohdsi.webapi.info; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; import org.apache.commons.lang3.StringUtils; import org.ohdsi.info.ConfigurationInfo; @@ -31,7 +27,6 @@ import java.util.List; import java.util.stream.Collectors; -@Path("/info") @Controller public class InfoService { @@ -51,9 +46,6 @@ public InfoService(BuildProperties buildProperties, BuildInfo buildInfo, List list( - @QueryParam("hide_statuses") String hideStatuses, - @DefaultValue("FALSE") @QueryParam("refreshJobs") Boolean refreshJobs) { - List statuses = new ArrayList<>(); - if (StringUtils.isNotEmpty(hideStatuses)) { - for (String status : hideStatuses.split(",")) { - try { - statuses.add(BatchStatus.valueOf(status)); - } catch (IllegalArgumentException e) { - log.warn("Invalid argument passed as batch status: {}", status); - } - } - } - List executionInfos; - if (refreshJobs) { - executionInfos = service.findRefreshCacheLastJobs(); - } else { - executionInfos = service.findLastJobs(statuses); - } - return executionInfos.stream().map(this::toDTO).collect(Collectors.toList()); - } - - /** - * Gets the date when notifications were last viewed - * - * @summary Get notification last viewed date - * @return The date when notifications were last viewed - */ - @GET - @Path("/viewed") - @Produces(MediaType.APPLICATION_JSON) - @Transactional(readOnly = true) - public Date getLastViewedTime() { - try { - return service.getLastViewedTime(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Sets the date when notifications were last viewed - * - * @summary Set notification last viewed date - * @param stamp - */ - @POST - @Path("/viewed") - @Produces(MediaType.APPLICATION_JSON) - public void setLastViewedTime(Date stamp) { - try { - service.setLastViewedTime(stamp); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private JobExecutionResource toDTO(JobExecutionInfo entity) { - return conversionService.convert(entity, JobExecutionResource.class); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java b/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java index 54dc105c39..29cb48ddf8 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java +++ b/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java @@ -22,9 +22,12 @@ import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.NotFoundException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; @@ -78,7 +81,7 @@ public ResponseEntity handleDataIntegrityViolation(DataIntegrityVi /** * Handle authorization/permission exceptions */ - @ExceptionHandler({UnauthorizedException.class, ForbiddenException.class}) + @ExceptionHandler(UnauthorizedException.class) public ResponseEntity handleAuthorizationException(Exception ex) { logException(ex); ex.setStackTrace(new StackTraceElement[0]); @@ -87,14 +90,15 @@ public ResponseEntity handleAuthorizationException(Exception ex) { } /** - * Handle not found exceptions + * Handle Spring ResponseStatusException (replaces JAX-RS NotFoundException, ForbiddenException, etc.) */ - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(NotFoundException ex) { + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity handleResponseStatusException(ResponseStatusException ex) { logException(ex); - ex.setStackTrace(new StackTraceElement[0]); - ErrorMessage errorMessage = new ErrorMessage(ex); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorMessage); + RuntimeException sanitizedException = new RuntimeException(ex.getReason() != null ? ex.getReason() : ex.getMessage()); + sanitizedException.setStackTrace(new StackTraceElement[0]); + ErrorMessage errorMessage = new ErrorMessage(sanitizedException); + return ResponseEntity.status(ex.getStatusCode()).body(errorMessage); } /** @@ -113,7 +117,7 @@ public ResponseEntity handleResourceNotFoundException(Exception ex /** * Handle bad request exceptions */ - @ExceptionHandler({BadRequestException.class, ConceptNotExistException.class}) + @ExceptionHandler(ConceptNotExistException.class) public ResponseEntity handleBadRequestException(Exception ex) { logException(ex); ex.setStackTrace(new StackTraceElement[0]); @@ -144,7 +148,7 @@ public ResponseEntity handleUndeclaredThrowable(UndeclaredThrowabl Throwable responseException; if (Objects.nonNull(throwable)) { - if (throwable instanceof UnauthorizedException || throwable instanceof ForbiddenException) { + if (throwable instanceof UnauthorizedException) { status = HttpStatus.FORBIDDEN; responseException = throwable; } else if (throwable instanceof BadRequestAtlasException || throwable instanceof ConceptNotExistException) { diff --git a/src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java b/src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java index 928459f919..674d2ceba4 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java +++ b/src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java @@ -1,6 +1,6 @@ package org.ohdsi.webapi.mvc; -import jakarta.ws.rs.core.MediaType; +import org.springframework.http.MediaType; /** * Utility methods to assist with Jersey to Spring MVC migration. @@ -11,16 +11,16 @@ public class MigrationUtils { * Convert JAX-RS MediaType constant to Spring media type string */ public static String toSpringMediaType(String jaxrsMediaType) { - // JAX-RS uses constants like MediaType.APPLICATION_JSON + // JAX-RS uses constants like MediaType.APPLICATION_JSON_VALUE // Spring uses constants like MediaType.APPLICATION_JSON_VALUE // This is mainly for documentation/reference return switch (jaxrsMediaType) { - case MediaType.APPLICATION_JSON -> org.springframework.http.MediaType.APPLICATION_JSON_VALUE; - case MediaType.APPLICATION_XML -> org.springframework.http.MediaType.APPLICATION_XML_VALUE; - case MediaType.TEXT_PLAIN -> org.springframework.http.MediaType.TEXT_PLAIN_VALUE; - case MediaType.TEXT_HTML -> org.springframework.http.MediaType.TEXT_HTML_VALUE; - case MediaType.MULTIPART_FORM_DATA -> org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; - case MediaType.APPLICATION_FORM_URLENCODED -> org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; + case "application/json" -> org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + case "application/xml" -> org.springframework.http.MediaType.APPLICATION_XML_VALUE; + case "text/plain" -> org.springframework.http.MediaType.TEXT_PLAIN_VALUE; + case "text/html" -> org.springframework.http.MediaType.TEXT_HTML_VALUE; + case "multipart/form-data" -> org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; + case "application/x-www-form-urlencoded" -> org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; default -> jaxrsMediaType; }; } diff --git a/src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java b/src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java index dbaa401637..4eff55db7d 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java +++ b/src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java @@ -3,26 +3,26 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import jakarta.ws.rs.core.Response; +import org.springframework.http.ResponseEntity; /** - * Utility class to convert JAX-RS Response objects to Spring ResponseEntity. + * Utility class to convert JAX-RS ResponseEntity objects to Spring ResponseEntity. * Used during migration to facilitate gradual conversion of endpoints. */ public class ResponseConverters { /** - * Convert JAX-RS Response to Spring ResponseEntity + * Convert JAX-RS ResponseEntity to Spring ResponseEntity */ - public static ResponseEntity toResponseEntity(Response response) { + public static ResponseEntity toResponseEntity(ResponseEntity response) { if (response == null) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } - HttpStatus status = HttpStatus.valueOf(response.getStatus()); + HttpStatus status = HttpStatus.valueOf(response.getStatusCode().value()); @SuppressWarnings("unchecked") - T body = (T) response.getEntity(); + T body = (T) response.getBody(); if (body == null) { return ResponseEntity.status(status).build(); @@ -31,26 +31,7 @@ public static ResponseEntity toResponseEntity(Response response) { return ResponseEntity.status(status).body(body); } - /** - * Convert JAX-RS Response.Status to Spring HttpStatus - */ - public static HttpStatus toHttpStatus(Response.Status status) { - return HttpStatus.valueOf(status.getStatusCode()); - } - - /** - * Convert JAX-RS Response.StatusType to Spring HttpStatus - */ - public static HttpStatus toHttpStatus(Response.StatusType statusType) { - return HttpStatus.valueOf(statusType.getStatusCode()); - } - - /** - * Create ResponseEntity from JAX-RS status and entity - */ - public static ResponseEntity fromJaxRs(Response.Status status, T entity) { - return ResponseEntity.status(toHttpStatus(status)).body(entity); - } + // JAX-RS conversion methods removed - no longer needed after migration to Spring MVC /** * Create ResponseEntity from status code and entity diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java index c8e1b851c8..324d8b67db 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java @@ -112,8 +112,8 @@ public ResponseEntity exportCohortResults( @PathVariable("id") int id, @PathVariable("sourceKey") String sourceKey) { - jakarta.ws.rs.core.Response jerseyResponse = cohortResultsService.exportCohortResults(id, sourceKey); - ByteArrayOutputStream baos = (ByteArrayOutputStream) jerseyResponse.getEntity(); + ResponseEntity jerseyResponse = cohortResultsService.exportCohortResults(id, sourceKey); + ByteArrayOutputStream baos = (ByteArrayOutputStream) jerseyResponse.getBody(); ByteArrayResource resource = new ByteArrayResource(baos.toByteArray()); diff --git a/src/main/java/org/ohdsi/webapi/reusable/ReusableController.java b/src/main/java/org/ohdsi/webapi/reusable/ReusableController.java deleted file mode 100644 index c10a4fe0cf..0000000000 --- a/src/main/java/org/ohdsi/webapi/reusable/ReusableController.java +++ /dev/null @@ -1,231 +0,0 @@ -package org.ohdsi.webapi.reusable; - -import org.ohdsi.webapi.Pagination; -import org.ohdsi.webapi.reusable.dto.ReusableDTO; -import org.ohdsi.webapi.reusable.dto.ReusableVersionFullDTO; -import org.ohdsi.webapi.tag.dto.TagNameListRequestDTO; -import org.ohdsi.webapi.versioning.dto.VersionDTO; -import org.ohdsi.webapi.versioning.dto.VersionUpdateDTO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Controller; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import java.util.Collections; -import java.util.List; - -@Path("/reusable") -@Controller -public class ReusableController { - private final ReusableService reusableService; - - @Autowired - public ReusableController(ReusableService reusableService) { - this.reusableService = reusableService; - } - - @POST - @Path("/") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public ReusableDTO create(final ReusableDTO dto) { - return reusableService.create(dto); - } - - @GET - @Path("/") - @Produces(MediaType.APPLICATION_JSON) - public Page page(@Pagination Pageable pageable) { - return reusableService.page(pageable); - } - - @PUT - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public ReusableDTO update(@PathParam("id") final Integer id, final ReusableDTO dto) { - return reusableService.update(id, dto); - } - - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - @Path("/{id}") - public ReusableDTO copy(@PathParam("id") final int id) { - return reusableService.copy(id); - } - - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public ReusableDTO get(@PathParam("id") final Integer id) { - return reusableService.getDTOById(id); - } - - @GET - @Path("/{id}/exists") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public boolean exists(@PathParam("id") @DefaultValue("0") final int id, @QueryParam("name") String name) { - return reusableService.exists(id, name); - } - - @DELETE - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public void delete(@PathParam("id") final Integer id) { - reusableService.delete(id); - } - - /** - * Assign tag to Reusable - * - * @param id - * @param tagId - */ - @POST - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/tag/") - public void assignTag(@PathParam("id") final int id, final int tagId) { - reusableService.assignTag(id, tagId); - } - - /** - * Unassign tag from Reusable - * - * @param id - * @param tagId - */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/tag/{tagId}") - public void unassignTag(@PathParam("id") final int id, @PathParam("tagId") final int tagId) { - reusableService.unassignTag(id, tagId); - } - - /** - * Assign protected tag to Reusable - * - * @param id - * @param tagId - */ - @POST - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/protectedtag/") - public void assignPermissionProtectedTag(@PathParam("id") int id, final int tagId) { - reusableService.assignTag(id, tagId); - } - - /** - * Unassign protected tag from Reusable - * - * @param id - * @param tagId - */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/protectedtag/{tagId}") - public void unassignPermissionProtectedTag(@PathParam("id") final int id, @PathParam("tagId") final int tagId) { - reusableService.unassignTag(id, tagId); - } - - /** - * Get list of versions of Reusable - * - * @param id - * @return - */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/") - public List getVersions(@PathParam("id") final long id) { - return reusableService.getVersions(id); - } - - /** - * Get version of Reusable - * - * @param id - * @param version - * @return - */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") - public ReusableVersionFullDTO getVersion(@PathParam("id") final int id, @PathParam("version") final int version) { - return reusableService.getVersion(id, version); - } - - /** - * Update version of Reusable - * - * @param id - * @param version - * @param updateDTO - * @return - */ - @PUT - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") - public VersionDTO updateVersion(@PathParam("id") final int id, @PathParam("version") final int version, - VersionUpdateDTO updateDTO) { - return reusableService.updateVersion(id, version, updateDTO); - } - - /** - * Delete version of Reusable - * - * @param id - * @param version - */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") - public void deleteVersion(@PathParam("id") final int id, @PathParam("version") final int version) { - reusableService.deleteVersion(id, version); - } - - /** - * Create a new asset form version of Reusable - * - * @param id - * @param version - * @return - */ - @PUT - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}/createAsset") - public ReusableDTO copyAssetFromVersion(@PathParam("id") final int id, @PathParam("version") final int version) { - return reusableService.copyAssetFromVersion(id, version); - } - - /** - * Get list of reusables with assigned tags - * - * @param requestDTO - * @return - */ - @POST - @Path("/byTags") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public List listByTags(TagNameListRequestDTO requestDTO) { - if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { - return Collections.emptyList(); - } - return reusableService.listByTags(requestDTO); - } -} diff --git a/src/main/java/org/ohdsi/webapi/security/PermissionController.java b/src/main/java/org/ohdsi/webapi/security/PermissionController.java deleted file mode 100644 index 5ab0df974e..0000000000 --- a/src/main/java/org/ohdsi/webapi/security/PermissionController.java +++ /dev/null @@ -1,196 +0,0 @@ -package org.ohdsi.webapi.security; - -import org.ohdsi.webapi.security.dto.AccessRequestDTO; -import org.ohdsi.webapi.security.dto.RoleDTO; -import org.ohdsi.webapi.security.model.EntityType; -import org.ohdsi.webapi.service.UserService; -import org.ohdsi.webapi.shiro.Entities.PermissionEntity; -import org.ohdsi.webapi.shiro.Entities.RoleEntity; -import org.ohdsi.webapi.shiro.PermissionManager; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.core.convert.ConversionService; -import org.springframework.stereotype.Controller; -import org.springframework.transaction.annotation.Transactional; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -/** - * REST Services related to working with security permissions - * - * @summary Notifications - */ -@Controller -@Path(value = "/permission") -@Transactional -public class PermissionController { - - private final PermissionService permissionService; - private final PermissionManager permissionManager; - private final ConversionService conversionService; - - public PermissionController(PermissionService permissionService, PermissionManager permissionManager, @Qualifier("conversionService") ConversionService conversionService) { - - this.permissionService = permissionService; - this.permissionManager = permissionManager; - this.conversionService = conversionService; - } - - /** - * Get the list of permissions for a user - * - * @return A list of permissions - */ - @GET - @Path("") - @Produces(MediaType.APPLICATION_JSON) - public List getPermissions() { - - Iterable permissionEntities = this.permissionManager.getPermissions(); - return StreamSupport.stream(permissionEntities.spliterator(), false) - .map(UserService.Permission::new) - .collect(Collectors.toList()); - } - - /** - * Get the roles matching the roleSearch value - * - * @summary Role search - * @param roleSearch The role to search - * @return The list of roles - */ - @GET - @Path("/access/suggest") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public List listAccessesForEntity(@QueryParam("roleSearch") String roleSearch) { - - List roles = permissionService.suggestRoles(roleSearch); - return roles.stream().map(re -> conversionService.convert(re, RoleDTO.class)).collect(Collectors.toList()); - } - - /** - * Get roles that have a permission type (READ/WRITE) to entity - * - * @summary Get roles that have a specific permission (READ/WRITE) for the - * entity - * @param entityType The entity type - * @param entityId The entity ID - * @return The list of permissions for the permission type - * @throws Exception - */ - @GET - @Path("/access/{entityType}/{entityId}/{permType}") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public List listAccessesForEntityByPermType( - @PathParam("entityType") EntityType entityType, - @PathParam("entityId") Integer entityId, - @PathParam("permType") AccessType permType - ) throws Exception { - - permissionService.checkCommonEntityOwnership(entityType, entityId); - Set permissionTemplates = null; - permissionTemplates = permissionService.getTemplatesForType(entityType, permType).keySet(); - - List permissions = permissionTemplates - .stream() - .map(pt -> permissionService.getPermission(pt, entityId)) - .collect(Collectors.toList()); - - List roles = permissionService.finaAllRolesHavingPermissions(permissions); - - return roles.stream().map(re -> conversionService.convert(re, RoleDTO.class)).collect(Collectors.toList()); - } - - /** - * Get roles that have a permission type (READ/WRITE) to entity - * - * @summary Get roles that have a specific permission (READ/WRITE) for the - * entity - * @param entityType The entity type - * @param entityId The entity ID - * @return The list of permissions for the permission type - * @throws Exception - */ - @GET - @Path("/access/{entityType}/{entityId}") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public List listAccessesForEntity( - @PathParam("entityType") EntityType entityType, - @PathParam("entityId") Integer entityId - ) throws Exception { - return listAccessesForEntityByPermType(entityType, entityId, AccessType.WRITE); - } - - /** - * Grant group of permissions (READ / WRITE / ...) for the specified entity to the given role. - * Only owner of the entity can do that. - * - * @summary Grant permissions - * @param entityType The entity type - * @param entityId The entity ID - * @param roleId The role ID - * @param accessRequestDTO The access request object - * @throws Exception - */ - @POST - @Path("/access/{entityType}/{entityId}/role/{roleId}") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public void grantEntityPermissionsForRole( - @PathParam("entityType") EntityType entityType, - @PathParam("entityId") Integer entityId, - @PathParam("roleId") Long roleId, - AccessRequestDTO accessRequestDTO - ) throws Exception { - - permissionService.checkCommonEntityOwnership(entityType, entityId); - - Map permissionTemplates = permissionService.getTemplatesForType(entityType, accessRequestDTO.getAccessType()); - - RoleEntity role = permissionManager.getRole(roleId); - permissionManager.addPermissionsFromTemplate(role, permissionTemplates, entityId.toString()); - } - - /** - * Remove group of permissions for the specified entity to the given role. - * - * @summary Remove permissions - * @param entityType The entity type - * @param entityId The entity ID - * @param roleId The role ID - * @param accessRequestDTO The access request object - * @throws Exception - */ - @DELETE - @Path("/access/{entityType}/{entityId}/role/{roleId}") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public void revokeEntityPermissionsFromRole( - @PathParam("entityType") EntityType entityType, - @PathParam("entityId") Integer entityId, - @PathParam("roleId") Long roleId, - AccessRequestDTO accessRequestDTO - ) throws Exception { - - permissionService.checkCommonEntityOwnership(entityType, entityId); - Map permissionTemplates = permissionService.getTemplatesForType(entityType, accessRequestDTO.getAccessType()); - permissionService.removePermissionsFromRole(permissionTemplates, entityId, roleId); - } -} diff --git a/src/main/java/org/ohdsi/webapi/security/SSOController.java b/src/main/java/org/ohdsi/webapi/security/SSOController.java deleted file mode 100644 index a1a47d99d0..0000000000 --- a/src/main/java/org/ohdsi/webapi/security/SSOController.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * - * Copyright 2018 Odysseus Data Services, inc. - * 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. - * - * Company: Odysseus Data Services, Inc. - * Product Owner/Architecture: Gregory Klebanov - * Authors: Pavel Grafkin, Vitaly Koulakov, Maria Pozhidaeva - * Created: April 4, 2018 - * - */ - -package org.ohdsi.webapi.security; - -import com.google.common.net.HttpHeaders; -import org.apache.commons.io.IOUtils; -import org.pac4j.core.context.HttpConstants; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.ClassPathResource; -import org.springframework.stereotype.Controller; - -import jakarta.servlet.http.HttpServletResponse; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; - -/** - * REST Services related to working with Single Sign-On and SAML-based - * Services - * - * @summary Single Sign On - */ -@Controller -@Path("/saml/") -public class SSOController { - @Value("${security.saml.metadataLocation}") - private String metadataLocation; - @Value("${security.saml.sloUrl}") - private String sloUri; - @Value("${security.origin}") - private String origin; - - /** - * Get the SAML metadata - * - * @summary Get metadata - * @param response The response context - * @throws IOException - */ - @GET - @Path("/saml-metadata") - public void samlMetadata(@Context HttpServletResponse response) throws IOException { - - ClassPathResource resource = new ClassPathResource(metadataLocation); - final InputStream is = resource.getInputStream(); - response.setContentType(MediaType.APPLICATION_XML); - response.setHeader(HttpHeaders.CONTENT_TYPE, "application/xml"); - response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); - response.setHeader(HttpHeaders.PRAGMA, "no-cache"); - response.setHeader(HttpHeaders.EXPIRES, "0"); - IOUtils.copy(is, response.getOutputStream()); - response.flushBuffer(); - } - - /** - * Log out of the service - * - * @summary Log out - * @return Response - * @throws URISyntaxException - */ - @GET - @Path("/slo") - public Response logout() throws URISyntaxException { - return Response.status(HttpConstants.TEMPORARY_REDIRECT) - .header(HttpConstants.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, this.origin) - .location(new URI(sloUri)) - .build(); - } -} diff --git a/src/main/java/org/ohdsi/webapi/service/AbstractDaoService.java b/src/main/java/org/ohdsi/webapi/service/AbstractDaoService.java index dcec15890f..e27926059e 100644 --- a/src/main/java/org/ohdsi/webapi/service/AbstractDaoService.java +++ b/src/main/java/org/ohdsi/webapi/service/AbstractDaoService.java @@ -52,8 +52,10 @@ import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.ForbiddenException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; import java.io.File; import java.io.IOException; import java.sql.ResultSet; @@ -429,7 +431,7 @@ protected void checkOwnerOrAdmin(UserEntity owner) { Long ownerId = Objects.nonNull(owner) ? owner.getId() : null; if (!(user.getId().equals(ownerId) || isAdmin())) { - throw new ForbiddenException(); + throw new ResponseStatusException(HttpStatus.FORBIDDEN); } } @@ -442,7 +444,7 @@ protected void checkOwnerOrAdminOrModerator(UserEntity owner) { Long ownerId = Objects.nonNull(owner) ? owner.getId() : null; if (!(user.getId().equals(ownerId) || isAdmin() || isModerator())) { - throw new ForbiddenException(); + throw new ResponseStatusException(HttpStatus.FORBIDDEN); } } @@ -455,7 +457,7 @@ protected void checkOwnerOrAdminOrGranted(CommonEntity entity) { Long ownerId = Objects.nonNull(entity.getCreatedBy()) ? entity.getCreatedBy().getId() : null; if (!(user.getId().equals(ownerId) || isAdmin() || permissionService.hasWriteAccess(entity))) { - throw new ForbiddenException(); + throw new ResponseStatusException(HttpStatus.FORBIDDEN); } } @@ -468,7 +470,7 @@ protected void checkOwnerOrAdminOrGrantedOrTagManager(CommonEntity entity) { Long ownerId = Objects.nonNull(entity.getCreatedBy()) ? entity.getCreatedBy().getId() : null; if (!(user.getId().equals(ownerId) || isAdmin() || permissionService.hasWriteAccess(entity) || TagSecurityUtils.canManageTags())) { - throw new ForbiddenException(); + throw new ResponseStatusException(HttpStatus.FORBIDDEN); } } diff --git a/src/main/java/org/ohdsi/webapi/service/ActivityService.java b/src/main/java/org/ohdsi/webapi/service/ActivityService.java deleted file mode 100644 index c916ad6519..0000000000 --- a/src/main/java/org/ohdsi/webapi/service/ActivityService.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2015 fdefalco. - * - * 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.service; - -import java.util.ArrayList; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import org.ohdsi.webapi.activity.Tracker; -import org.springframework.stereotype.Component; - - /** - * Example REST service - will be depreciated - * in a future release - * - * @deprecated - * @summary Activity - */ -@Path("/activity/") -@Component -public class ActivityService { - /** - * Example REST service - will be depreciated - * in a future release - * - * @deprecated - * @summary DO NOT USE - */ - @Path("latest") - @GET - @Produces(MediaType.APPLICATION_JSON) - public Object[] getLatestActivity() { - return Tracker.getActivity(); - } -} diff --git a/src/main/java/org/ohdsi/webapi/service/CDMResultsService.java b/src/main/java/org/ohdsi/webapi/service/CDMResultsService.java index a66bb59649..ce46582110 100644 --- a/src/main/java/org/ohdsi/webapi/service/CDMResultsService.java +++ b/src/main/java/org/ohdsi/webapi/service/CDMResultsService.java @@ -46,14 +46,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.PlatformTransactionManager; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collection; @@ -71,11 +63,12 @@ import static org.ohdsi.webapi.cdmresults.AchillesCacheTasklet.OBSERVATION_PERIOD; import static org.ohdsi.webapi.cdmresults.AchillesCacheTasklet.PERSON; import static org.ohdsi.webapi.cdmresults.AchillesCacheTasklet.TREEMAP; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; /** * @author fdefalco */ -@Path("/cdmresults") @Component @DependsOn({"jobInvalidator", "flyway"}) public class CDMResultsService extends AbstractDaoService implements InitializingBean { @@ -176,11 +169,7 @@ public void scheduledWarmCaches(){ *

* For concept id "201826" in the SYNPUF5PCT data source the record count is 612861 and the descendant record count is 653173. */ - @Path("{sourceKey}/conceptRecordCount") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public List>> getConceptRecordCount(@PathParam("sourceKey") String sourceKey, List identifiers) { + public List>> getConceptRecordCount(String sourceKey, List identifiers) { Source source = sourceService.findBySourceKey(sourceKey); if (source != null) { List entities = cdmCacheService.findAndCache(source, identifiers); @@ -209,12 +198,8 @@ private List>> convertToResponse(Collection */ - @GET - @Path("{sourceKey}/{domain}/") - @Produces(MediaType.APPLICATION_JSON) @AchillesCache(TREEMAP) public ArrayNode getTreemap( - @PathParam("domain") final String domain, - @PathParam("sourceKey") final String sourceKey) { return getRawTreeMap(domain, sourceKey); @@ -407,15 +365,9 @@ public ArrayNode getRawTreeMap(String domain, String sourceKey) { * @param sourceKey The source key * @return The JSON results */ - @GET - @Path("{sourceKey}/{domain}/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) @AchillesCache(DRILLDOWN) - public JsonNode getDrilldown(@PathParam("domain") - final String domain, - @PathParam("conceptId") + public JsonNode getDrilldown(final String domain, final int conceptId, - @PathParam("sourceKey") final String sourceKey) { return getRawDrilldown(domain, conceptId, sourceKey); diff --git a/src/main/java/org/ohdsi/webapi/service/CohortAnalysisService.java b/src/main/java/org/ohdsi/webapi/service/CohortAnalysisService.java index f86236c4f6..d8c71c06aa 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortAnalysisService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortAnalysisService.java @@ -8,13 +8,6 @@ import java.util.List; import java.util.stream.Collectors; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Joiner; @@ -49,7 +42,6 @@ * * @summary Cohort Analysis (a.k.a Heracles) */ -@Path("/cohortanalysis/") @Component public class CohortAnalysisService extends AbstractDaoService implements GeneratesNotification { @@ -124,8 +116,6 @@ private void mapAnalysis(final Analysis analysis, final ResultSet rs, final int * @summary Get all cohort analyses * @return List of all cohort analyses */ - @GET - @Produces(MediaType.APPLICATION_JSON) public List getCohortAnalyses() { String sqlPath = "/resources/cohortanalysis/sql/getCohortAnalyses.sql"; String search = "ohdsi_database_schema"; @@ -143,10 +133,7 @@ public List getCohortAnalyses() { * @return List of all cohort analyses and their statuses * for the given cohort_definition_id */ - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - public List getCohortAnalysesForCohortDefinition(@PathParam("id") final int id) { + public List getCohortAnalysesForCohortDefinition(final int id) { String sqlPath = "/resources/cohortanalysis/sql/getCohortAnalysesForCohort.sql"; String tqName = "ohdsi_database_schema"; String tqValue = getOhdsiSchema(); @@ -163,10 +150,7 @@ public List getCohortAnalysesForCohortDefinition(@PathParam("id" * statuses for this cohort, and a base set of common cohort results that may or may not * yet have been ran */ - @GET - @Path("/{id}/summary") - @Produces(MediaType.APPLICATION_JSON) - public CohortSummary getCohortSummary(@PathParam("id") final int id) { + public CohortSummary getCohortSummary(final int id) { CohortSummary summary = new CohortSummary(); try { @@ -189,10 +173,6 @@ public CohortSummary getCohortSummary(@PathParam("id") final int id) { * @return - SQL for the given CohortAnalysisTask translated and rendered to * the current dialect */ - @POST - @Path("/preview") - @Produces(MediaType.TEXT_PLAIN) - @Consumes(MediaType.APPLICATION_JSON) public String getRunCohortAnalysisSql(CohortAnalysisTask task) { task.setSmallCellCount(Integer.parseInt(this.smallCellCount)); return heraclesQueryBuilder.buildHeraclesAnalysisQuery(task); @@ -232,9 +212,6 @@ public String[] getRunCohortAnalysisSqlBatch(CohortAnalysisTask task) { * @return information about the Cohort Analysis Job * @throws Exception */ - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public JobExecutionResource queueCohortAnalysisJob(CohortAnalysisTask task) throws Exception { if (task == null) { return null; diff --git a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java index 166ecf3992..f866441797 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java @@ -101,21 +101,6 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.support.TransactionTemplate; import org.hibernate.Hibernate; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.io.ByteArrayOutputStream; import java.math.BigDecimal; import java.math.RoundingMode; @@ -139,7 +124,6 @@ import java.util.stream.Collectors; import javax.cache.CacheManager; import javax.cache.configuration.MutableConfiguration; -import jakarta.ws.rs.core.Response.ResponseBuilder; import static org.ohdsi.webapi.Constants.Params.COHORT_DEFINITION_ID; import static org.ohdsi.webapi.Constants.Params.JOB_NAME; @@ -151,6 +135,13 @@ import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.MediaType; /** * Provides REST services for working with cohort definitions. @@ -158,7 +149,6 @@ * @summary Provides REST services for working with cohort definitions. * @author cknoll1 */ -@Path("/cohortdefinition") @Component public class CohortDefinitionService extends AbstractDaoService implements HasTags { @@ -413,7 +403,6 @@ public GenerateSqlRequest() { public CohortExpressionQueryBuilder.BuildExpressionQueryOptions options; } - @Context ServletContext context; /** @@ -423,10 +412,6 @@ public GenerateSqlRequest() { * @param request A GenerateSqlRequest containing the cohort expression and options. * @return The OHDSI template SQL needed to generate the input cohort definition as a character string */ - @Path("sql") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public GenerateSqlResult generateSql(GenerateSqlRequest request) { CohortExpressionQueryBuilder.BuildExpressionQueryOptions options = request.options; GenerateSqlResult result = new GenerateSqlResult(); @@ -446,9 +431,6 @@ public GenerateSqlResult generateSql(GenerateSqlRequest request) { * @return List of metadata about all cohort definitions in WebAPI * @see org.ohdsi.webapi.cohortdefinition.CohortMetadataDTO */ - @GET - @Path("/") - @Produces(MediaType.APPLICATION_JSON) @Transactional @Cacheable(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()") public List getCohortDefinitionList() { @@ -474,11 +456,7 @@ public List getCohortDefinitionList() { * @param dto The cohort definition to create. * @return The newly created cohort definition */ - @POST - @Path("/") @Transactional - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) public CohortDTO createCohortDefinition(CohortDTO dto) { @@ -519,10 +497,7 @@ public CohortDTO createCohortDefinition(CohortDTO dto) { * @param id The cohort definition id * @return The cohort definition JSON expression */ - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - public CohortRawDTO getCohortDefinitionRaw(@PathParam("id") final int id) { + public CohortRawDTO getCohortDefinitionRaw(final int id) { return getTransactionTemplate().execute(transactionStatus -> { CohortDefinition d = this.cohortDefinitionRepository.findOneWithDetail(id); ExceptionUtils.throwNotFoundExceptionIfNull(d, String.format("There is no cohort definition with id = %d.", id)); @@ -559,10 +534,7 @@ public CohortDTO getCohortDefinition(final int id) { * @return 1 if the a cohort with the given name and id exist in WebAPI and 0 * otherwise */ - @GET - @Path("/{id}/exists") - @Produces(MediaType.APPLICATION_JSON) - public int getCountCDefWithSameName(@PathParam("id") @DefaultValue("0") final int id, @QueryParam("name") String name) { + public int getCountCDefWithSameName(final int id, String name) { return cohortDefinitionRepository.getCountCDefWithSameName(id, name); } @@ -577,13 +549,9 @@ public int getCountCDefWithSameName(@PathParam("id") @DefaultValue("0") final in * @param id The cohort definition id * @return The updated CohortDefinition */ - @PUT - @Path("/{id}") - @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) { + public CohortDTO saveCohortDefinition(final int id, CohortDTO def) { Date currentTime = Calendar.getInstance().getTime(); saveVersion(id); @@ -611,12 +579,9 @@ public CohortDTO saveCohortDefinition(@PathParam("id") final int id, CohortDTO d * @param sourceKey The source to execute the cohort generation * @return the job info for the cohort generation */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/generate/{sourceKey}") - public JobExecutionResource generateCohort(@PathParam("id") final int id, - @PathParam("sourceKey") final String sourceKey, - @QueryParam("demographic") boolean demographicStat) { + public JobExecutionResource generateCohort(final int id, + final String sourceKey, + boolean demographicStat) { // Load entities within a transaction and eagerly initialize all lazy fields Source source = transactionTemplate.execute(status -> { Source s = getSourceRepository().findBySourceKey(sourceKey); @@ -667,13 +632,10 @@ public JobExecutionResource generateCohort(@PathParam("id") final int id, * @param sourceKey the sourceKey for the target database for generation * @return */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/cancel/{sourceKey}") - public Response cancelGenerateCohort(@PathParam("id") final int id, @PathParam("sourceKey") final String sourceKey) { + public ResponseEntity cancelGenerateCohort(final int id, final String sourceKey) { final Source source = Optional.ofNullable(getSourceRepository().findBySourceKey(sourceKey)) - .orElseThrow(NotFoundException::new); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); getTransactionTemplateRequiresNew().execute(status -> { CohortDefinition currentDefinition = cohortDefinitionRepository.findById(id).orElse(null); if (Objects.nonNull(currentDefinition)) { @@ -693,7 +655,7 @@ public Response cancelGenerateCohort(@PathParam("id") final int id, @PathParam(" && Objects.equals(parameters.getString(SOURCE_ID), Integer.toString(source.getSourceId())) && Objects.equals(Constants.GENERATE_COHORT, jobName); }); - return Response.status(Response.Status.OK).build(); + return ResponseEntity.status(HttpStatus.OK).build(); } /** @@ -709,11 +671,8 @@ public Response cancelGenerateCohort(@PathParam("id") final int id, @PathParam(" * @return information about the Cohort Analysis Job for each source * @throws NotFoundException */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/info") @Transactional - public List getInfo(@PathParam("id") final int id) { + public List getInfo(final int id) { CohortDefinition def = this.cohortDefinitionRepository.findById(id).orElse(null); ExceptionUtils.throwNotFoundExceptionIfNull(def, String.format("There is no cohort definition with id = %d.", id)); @@ -739,12 +698,9 @@ public List getInfo(@PathParam("id") final int id) { * @param id - the Cohort Definition ID to copy * @return the copied cohort definition as a CohortDTO */ - @GET - @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) { + public CohortDTO copy(final int id) { CohortDTO sourceDef = getCohortDefinition(id); sourceDef.setId(null); // clear the ID sourceDef.setTags(null); @@ -768,11 +724,8 @@ public List getNamesLike(String copyName) { * @summary Delete Cohort Definition * @param id - the Cohort Definition ID to delete */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}") @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) - public void delete(@PathParam("id") final int id) { + public void delete(final int id) { // perform the JPA update in a separate transaction this.getTransactionTemplateRequiresNew().execute(new TransactionCallbackWithoutResult() { @Override @@ -852,19 +805,15 @@ private List getConceptSetExports(CohortDefinition def, Source * @param id a cohort definition id * @return a binary stream containing the zip file with concept sets. */ - @GET - @Path("/{id}/export/conceptset") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response exportConceptSets(@PathParam("id") final int id) { + public ResponseEntity exportConceptSets(final int id) { Source source = sourceService.getPriorityVocabularySource(); if (Objects.isNull(source)) { - throw new ForbiddenException(); + throw new ResponseStatusException(HttpStatus.FORBIDDEN); } CohortDefinition def = this.cohortDefinitionRepository.findOneWithDetail(id); if (Objects.isNull(def)) { - throw new NotFoundException(); + throw new ResponseStatusException(HttpStatus.NOT_FOUND); } List exports = getConceptSetExports(def, new SourceInfo(source)); @@ -887,14 +836,11 @@ public Response exportConceptSets(@PathParam("id") final int id) { * @param modeId the mode of the report: 0 = all events, 1 = best event * @return a binary stream containing the zip file with concept sets. */ - @GET - @Path("/{id}/report/{sourceKey}") - @Produces(MediaType.APPLICATION_JSON) @Transactional public InclusionRuleReport getInclusionRuleReport( - @PathParam("id") final int id, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("0") @QueryParam("mode") int modeId, @QueryParam("ccGenerateId") String ccGenerateId) { + final int id, + final String sourceKey, + int modeId, String ccGenerateId) { Source source = this.getSourceRepository().findBySourceKey(sourceKey); @@ -921,10 +867,6 @@ public InclusionRuleReport getInclusionRuleReport( * The cohort definition expression * @return The cohort check result */ - @POST - @Path("/check") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) @Transactional public CheckResultDTO runDiagnostics(CohortExpression expression) { Checker checker = new Checker(); @@ -942,10 +884,6 @@ public CheckResultDTO runDiagnostics(CohortExpression expression) { * @param cohortDTO The cohort definition expression * @return The cohort check result */ - @POST - @Path("/checkV2") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) @Transactional public CheckResult runDiagnosticsWithTags(CohortDTO cohortDTO) { Checker checker = new Checker(); @@ -973,10 +911,7 @@ public CheckResult runDiagnosticsWithTags(CohortDTO cohortDTO) { * @return an HTTP response with the content, with the appropriate MediaType * based on the format that was requested. */ - @POST - @Path("/printfriendly/cohort") - @Consumes(MediaType.APPLICATION_JSON) - public Response cohortPrintFriendly(CohortExpression expression, @DefaultValue("html") @QueryParam("format") String format) { + public ResponseEntity cohortPrintFriendly(CohortExpression expression, String format) { String markdown = convertCohortExpressionToMarkdown(expression); return printFrindly(markdown, format); } @@ -995,10 +930,7 @@ public Response cohortPrintFriendly(CohortExpression expression, @DefaultValue(" * @return an HTTP response with the content, with the appropriate MediaType * based on the format that was requested. */ - @POST - @Path("/printfriendly/conceptsets") - @Consumes(MediaType.APPLICATION_JSON) - public Response conceptSetListPrintFriendly(List conceptSetList, @DefaultValue("html") @QueryParam("format") String format) { + public ResponseEntity conceptSetListPrintFriendly(List conceptSetList, String format) { String markdown = markdownPF.renderConceptSetList(conceptSetList.toArray(new ConceptSet[0])); return printFrindly(markdown, format); } @@ -1014,18 +946,15 @@ public String convertMarkdownToHTML(String markdown){ return renderer.render(document); } - private Response printFrindly(String markdown, String format) { - - ResponseBuilder res; + private ResponseEntity printFrindly(String markdown, String format) { if ("html".equalsIgnoreCase(format)) { String html = convertMarkdownToHTML(markdown); - res = Response.ok(html, MediaType.TEXT_HTML); + return ResponseEntity.ok().contentType(org.springframework.http.MediaType.TEXT_HTML).body(html); } else if ("markdown".equals(format)) { - res = Response.ok(markdown, MediaType.TEXT_PLAIN); + return ResponseEntity.ok().contentType(org.springframework.http.MediaType.TEXT_PLAIN).body(markdown); } else { - res = Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE); + return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).build(); } - return res.build(); } /** @@ -1035,12 +964,9 @@ private Response printFrindly(String markdown, String format) { * @param id the cohort definition id * @param tagId the tag id */ - @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) { + public void assignTag(final Integer id, final int tagId) { CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); assignTag(entity, tagId); } @@ -1052,12 +978,9 @@ public void assignTag(@PathParam("id") final Integer id, final int tagId) { * @param id the cohort definition id * @param tagId the tag id */ - @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) { + public void unassignTag(final Integer id, final int tagId) { CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); unassignTag(entity, tagId); } @@ -1071,11 +994,8 @@ public void unassignTag(@PathParam("id") final Integer id, @PathParam("tagId") f * @param id * @param tagId */ - @POST - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/protectedtag/") @Transactional - public void assignPermissionProtectedTag(@PathParam("id") final int id, final int tagId) { + public void assignPermissionProtectedTag(final int id, final int tagId) { assignTag(id, tagId); } @@ -1086,11 +1006,8 @@ public void assignPermissionProtectedTag(@PathParam("id") final int id, final in * @param id * @param tagId */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/protectedtag/{tagId}") @Transactional - public void unassignPermissionProtectedTag(@PathParam("id") final int id, @PathParam("tagId") final int tagId) { + public void unassignPermissionProtectedTag(final int id, final int tagId) { unassignTag(id, tagId); } @@ -1102,11 +1019,8 @@ public void unassignPermissionProtectedTag(@PathParam("id") final int id, @PathP * @return the list of VersionDTO containing version info for the cohort * definition */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/") @Transactional - public List getVersions(@PathParam("id") final long id) { + public List getVersions(final long id) { List versions = versionService.getVersions(VersionType.COHORT, id); return versions.stream() .map(v -> conversionService.convert(v, VersionDTO.class)) @@ -1121,11 +1035,8 @@ public List getVersions(@PathParam("id") final long id) { * @param version The version to fetch * @return */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") @Transactional - public CohortVersionFullDTO getVersion(@PathParam("id") final int id, @PathParam("version") final int version) { + public CohortVersionFullDTO getVersion(final int id, final int version) { checkVersion(id, version, false); CohortVersion cohortVersion = versionService.getById(VersionType.COHORT, id, version); @@ -1144,11 +1055,8 @@ public CohortVersionFullDTO getVersion(@PathParam("id") final int id, @PathParam * @param updateDTO the new version data * @return the updated version state as VersionDTO */ - @PUT - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") @Transactional - public VersionDTO updateVersion(@PathParam("id") final int id, @PathParam("version") final int version, + public VersionDTO updateVersion(final int id, final int version, VersionUpdateDTO updateDTO) { checkVersion(id, version); updateDTO.setAssetId(id); @@ -1165,11 +1073,8 @@ public VersionDTO updateVersion(@PathParam("id") final int id, @PathParam("versi * @param id the cohort definition id * @param version the version id */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") @Transactional - public void deleteVersion(@PathParam("id") final int id, @PathParam("version") final int version) { + public void deleteVersion(final int id, final int version) { checkVersion(id, version); versionService.delete(VersionType.COHORT, id, version); } @@ -1186,12 +1091,9 @@ public void deleteVersion(@PathParam("id") final int id, @PathParam("version") f * @param version the version id * @return */ - @PUT - @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) { + public CohortDTO copyAssetFromVersion(final int id, final int version) { checkVersion(id, version, false); CohortVersion cohortVersion = versionService.getById(VersionType.COHORT, id, version); CohortVersionFullDTO fullDTO = conversionService.convert(cohortVersion, CohortVersionFullDTO.class); @@ -1213,10 +1115,6 @@ public CohortDTO copyAssetFromVersion(@PathParam("id") final int id, @PathParam( * @param requestDTO contains a list of tag names * @return the set of cohort definitions that match one of the included tag names. */ - @POST - @Path("/byTags") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) @Transactional public List listByTags(TagNameListRequestDTO requestDTO) { if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { diff --git a/src/main/java/org/ohdsi/webapi/service/CohortResultsService.java b/src/main/java/org/ohdsi/webapi/service/CohortResultsService.java index 89294c9de8..2c8173d7c6 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortResultsService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortResultsService.java @@ -8,8 +8,6 @@ import java.util.stream.Collectors; import jakarta.annotation.PostConstruct; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.math.NumberUtils; @@ -38,8 +36,11 @@ import java.sql.ResultSetMetaData; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import jakarta.ws.rs.core.Response; import java.util.Optional; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; /** @@ -53,7 +54,6 @@ * * @summary Cohort Analysis Results (a.k.a Heracles Results) */ -@Path("/cohortresults") @Component public class CohortResultsService extends AbstractDaoService { @@ -93,14 +93,11 @@ public void init() { * @param sourceKey the source to retrieve results * @return List of key, value pairs */ - @GET - @Path("{sourceKey}/{id}/raw/{analysis_group}/{analysis_name}") - @Produces(MediaType.APPLICATION_JSON) - public List> getCohortResultsRaw(@PathParam("id") final int id, @PathParam("analysis_group") final String analysisGroup, - @PathParam("analysis_name") final String analysisName, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") String sourceKey) { + public List> getCohortResultsRaw(final int id, final String analysisGroup, + final String analysisName, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + String sourceKey) { List> results; String sqlPath = BASE_SQL_PATH + "/" + analysisGroup + "/" + analysisName + ".sql"; @@ -145,10 +142,7 @@ protected PreparedStatementRenderer prepareGetCohortResultsRaw(final int id, * @param sourceKey The source Key * @return A response containing the .ZIP file of results */ - @GET - @Path("{sourceKey}/{id}/export.zip") - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response exportCohortResults(@PathParam("id") int id, @PathParam("sourceKey") String sourceKey) { + public ResponseEntity exportCohortResults(int id, String sourceKey) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zos = new ZipOutputStream(baos); @@ -228,10 +222,9 @@ public Void mapRow(ResultSet rs, int arg1) throws SQLException { throw new RuntimeException(ex); } - Response response = Response - .ok(baos) - .type(MediaType.APPLICATION_OCTET_STREAM) - .build(); + ResponseEntity response = ResponseEntity.ok() + .contentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM) + .body(baos); return response; } @@ -245,10 +238,6 @@ public Void mapRow(ResultSet rs, int arg1) throws SQLException { * @param task The cohort analysis task * @return The number of report visualizations warmed */ - @POST - @Path("/warmup") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public int warmUpVisualizationData(CohortAnalysisTask task) { return this.queryRunner.warmupData(this.getSourceJdbcTemplate(task.getSource()), task); @@ -262,12 +251,8 @@ public int warmUpVisualizationData(CohortAnalysisTask task) { * @param sourceKey The source key * @return A list of visualization keys that are complete */ - @GET - @Path("{sourceKey}/{id}/completed") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getCompletedVisualiztion(@PathParam("id") final int id, - @PathParam("sourceKey") final String sourceKey) { + public Collection getCompletedVisualiztion(final int id, + final String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); List vizData = this.visualizationDataRepository.findByCohortDefinitionIdAndSourceId(id, source.getSourceId()); Set completed = new HashSet<>(); @@ -288,10 +273,7 @@ public Collection getCompletedVisualiztion(@PathParam("id") final int id * @param cohortDefinitionId The cohort definition id * @return The tornado plot data */ - @GET - @Path("{sourceKey}/{id}/tornado") - @Produces(MediaType.APPLICATION_JSON) - public TornadoReport getTornadoReport(@PathParam("sourceKey") final String sourceKey, @PathParam("id") final int cohortDefinitionId) { + public TornadoReport getTornadoReport(final String sourceKey, final int cohortDefinitionId) { Source source = getSourceRepository().findBySourceKey(sourceKey); TornadoReport tornadoReport = new TornadoReport(); tornadoReport.tornadoRecords = queryRunner.getTornadoRecords(getSourceJdbcTemplate(source), cohortDefinitionId, source); @@ -309,15 +291,12 @@ public TornadoReport getTornadoReport(@PathParam("sourceKey") final String sourc * @param demographicsOnly only render gender and age * @return CohortDashboard */ - @GET - @Path("{sourceKey}/{id}/dashboard") - @Produces(MediaType.APPLICATION_JSON) - public CohortDashboard getDashboard(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @QueryParam("demographics_only") final boolean demographicsOnly, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortDashboard getDashboard(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final boolean demographicsOnly, + final String sourceKey, + boolean refresh) { final String key = CohortResultsAnalysisRunner.DASHBOARD; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -353,13 +332,10 @@ public CohortDashboard getDashboard(@PathParam("id") final int id, * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/condition/") - @Produces(MediaType.APPLICATION_JSON) - public List getConditionTreemap(@PathParam("sourceKey") String sourceKey, @PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getConditionTreemap(String sourceKey, final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + boolean refresh) { Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.CONDITION; @@ -389,12 +365,9 @@ public List getConditionTreemap(@PathParam("sourceKey * @param refresh Boolean - refresh visualization data * @return Distinct person count as integer */ - @GET - @Path("{sourceKey}/{id}/distinctPersonCount/") - @Produces(MediaType.APPLICATION_JSON) - public Integer getRawDistinctPersonCount(@PathParam("sourceKey") String sourceKey, - @PathParam("id") String id, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public Integer getRawDistinctPersonCount(String sourceKey, + String id, + boolean refresh) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetRawDistinctPersonCount(id, source); Integer result = getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), new ResultSetExtractor() { @@ -430,15 +403,12 @@ protected PreparedStatementRenderer prepareGetRawDistinctPersonCount(String id, * @param refresh Boolean - refresh visualization data * @return The CohortConditionDrilldown detail object */ - @GET - @Path("{sourceKey}/{id}/condition/{conditionId}") - @Produces(MediaType.APPLICATION_JSON) - public CohortConditionDrilldown getConditionResults(@PathParam("sourceKey") String sourceKey, - @PathParam("id") final int id, - @PathParam("conditionId") final int conditionId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortConditionDrilldown getConditionResults(String sourceKey, + final int id, + final int conditionId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + boolean refresh) { CohortConditionDrilldown drilldown = null; final String key = CohortResultsAnalysisRunner.CONDITION_DRILLDOWN; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -469,14 +439,11 @@ public CohortConditionDrilldown getConditionResults(@PathParam("sourceKey") Stri * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/conditionera/") - @Produces(MediaType.APPLICATION_JSON) - public List getConditionEraTreemap(@PathParam("sourceKey") final String sourceKey, - @PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getConditionEraTreemap(final String sourceKey, + final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + boolean refresh) { Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.CONDITION_ERA; @@ -506,10 +473,7 @@ public List getConditionEraTreemap(@PathParam("source * @param id The cohort ID * @return A list of completed analysis IDs */ - @GET - @Path("{sourceKey}/{id}/analyses") - @Produces(MediaType.APPLICATION_JSON) - public List getCompletedAnalyses(@PathParam("sourceKey") String sourceKey, @PathParam("id") String id) { + public List getCompletedAnalyses(String sourceKey, String id) { Source source = getSourceRepository().findBySourceKey(sourceKey); int sourceId = source.getSourceId(); @@ -572,10 +536,7 @@ public void setProgress(Integer progress) { * @param id The cohort ID * @return The generation progress information */ - @GET - @Path("{sourceKey}/{id}/info") - @Produces(MediaType.APPLICATION_JSON) - public GenerationInfoDTO getAnalysisProgress(@PathParam("sourceKey") String sourceKey, @PathParam("id") Integer id) { + public GenerationInfoDTO getAnalysisProgress(String sourceKey, Integer id) { return getTransactionTemplateRequiresNew().execute(status -> { org.ohdsi.webapi.cohortdefinition.CohortDefinition def = cohortDefinitionRepository.findById(id).orElse(null); @@ -583,7 +544,7 @@ public GenerationInfoDTO getAnalysisProgress(@PathParam("sourceKey") String sour return def.getCohortAnalysisGenerationInfoList().stream() .filter(cd -> Objects.equals(cd.getSourceId(), source.getSourceId())) .findFirst().map(gen -> new GenerationInfoDTO(sourceKey, id, gen.getProgress())) - .orElseThrow(NotFoundException::new); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); }); } @@ -610,15 +571,12 @@ protected PreparedStatementRenderer prepareGetCompletedAnalysis(String id, int s * @param refresh Boolean - refresh visualization data * @return The CohortConditionEraDrilldown object */ - @GET - @Path("{sourceKey}/{id}/conditionera/{conditionId}") - @Produces(MediaType.APPLICATION_JSON) - public CohortConditionEraDrilldown getConditionEraDrilldown(@PathParam("id") final int id, - @PathParam("conditionId") final int conditionId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortConditionEraDrilldown getConditionEraDrilldown(final int id, + final int conditionId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortConditionEraDrilldown drilldown = null; final String key = CohortResultsAnalysisRunner.CONDITION_ERA_DRILLDOWN; @@ -652,14 +610,11 @@ public CohortConditionEraDrilldown getConditionEraDrilldown(@PathParam("id") fin * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/drug/") - @Produces(MediaType.APPLICATION_JSON) - public List getDrugTreemap(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getDrugTreemap(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.DRUG; @@ -699,14 +654,11 @@ public List getDrugTreemap(@PathParam("id") final int * @param refresh Boolean - refresh visualization data * @return */ - @GET - @Path("{sourceKey}/{id}/drug/{drugId}") - @Produces(MediaType.APPLICATION_JSON) - public CohortDrugDrilldown getDrugResults(@PathParam("id") final int id, @PathParam("drugId") final int drugId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortDrugDrilldown getDrugResults(final int id, final int drugId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortDrugDrilldown drilldown = null; final String key = CohortResultsAnalysisRunner.DRUG_DRILLDOWN; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -738,14 +690,11 @@ public CohortDrugDrilldown getDrugResults(@PathParam("id") final int id, @PathPa * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/drugera/") - @Produces(MediaType.APPLICATION_JSON) - public List getDrugEraTreemap(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getDrugEraTreemap(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { Source source = getSourceRepository().findBySourceKey(sourceKey); List res = null; @@ -785,14 +734,11 @@ public List getDrugEraTreemap(@PathParam("id") final * @param refresh Boolean - refresh visualization data * @return CohortDrugEraDrilldown */ - @GET - @Path("{sourceKey}/{id}/drugera/{drugId}") - @Produces(MediaType.APPLICATION_JSON) - public CohortDrugEraDrilldown getDrugEraResults(@PathParam("id") final int id, @PathParam("drugId") final int drugId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortDrugEraDrilldown getDrugEraResults(final int id, final int drugId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortDrugEraDrilldown drilldown = null; Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.DRUG_ERA_DRILLDOWN; @@ -824,14 +770,11 @@ public CohortDrugEraDrilldown getDrugEraResults(@PathParam("id") final int id, @ * @param refresh Boolean - refresh visualization data * @return */ - @GET - @Path("{sourceKey}/{id}/person") - @Produces(MediaType.APPLICATION_JSON) - public CohortPersonSummary getPersonResults(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortPersonSummary getPersonResults(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortPersonSummary person = null; final String key = CohortResultsAnalysisRunner.PERSON; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -862,14 +805,11 @@ public CohortPersonSummary getPersonResults(@PathParam("id") final int id, * @param refresh Boolean - refresh visualization data * @return CohortSpecificSummary */ - @GET - @Path("{sourceKey}/{id}/cohortspecific") - @Produces(MediaType.APPLICATION_JSON) - public CohortSpecificSummary getCohortSpecificResults(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortSpecificSummary getCohortSpecificResults(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortSpecificSummary summary = null; Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.COHORT_SPECIFIC; @@ -900,14 +840,11 @@ public CohortSpecificSummary getCohortSpecificResults(@PathParam("id") final int * @param refresh Boolean - refresh visualization data * @return CohortSpecificTreemap */ - @GET - @Path("{sourceKey}/{id}/cohortspecifictreemap") - @Produces(MediaType.APPLICATION_JSON) - public CohortSpecificTreemap getCohortSpecificTreemapResults(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortSpecificTreemap getCohortSpecificTreemapResults(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortSpecificTreemap summary = null; final String key = CohortResultsAnalysisRunner.COHORT_SPECIFIC_TREEMAP; @@ -940,15 +877,12 @@ public CohortSpecificTreemap getCohortSpecificTreemapResults(@PathParam("id") fi * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/cohortspecificprocedure/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) - public List getCohortProcedureDrilldown(@PathParam("id") final int id, - @PathParam("conceptId") final int conceptId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getCohortProcedureDrilldown(final int id, + final int conceptId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { List records = new ArrayList<>(); Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -989,15 +923,12 @@ public List getCohortProcedureDrilldown(@PathParam("id") fina * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/cohortspecificdrug/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) - public List getCohortDrugDrilldown(@PathParam("id") final int id, - @PathParam("conceptId") final int conceptId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getCohortDrugDrilldown(final int id, + final int conceptId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { List records = new ArrayList(); Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1029,15 +960,12 @@ public List getCohortDrugDrilldown(@PathParam("id") final int * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/cohortspecificcondition/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) - public List getCohortConditionDrilldown(@PathParam("id") final int id, - @PathParam("conceptId") final int conceptId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getCohortConditionDrilldown(final int id, + final int conceptId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { List records = null; @@ -1071,14 +999,11 @@ public List getCohortConditionDrilldown(@PathParam("id") fina * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/observation") - @Produces(MediaType.APPLICATION_JSON) - public List getCohortObservationResults(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getCohortObservationResults(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { List res = null; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1112,15 +1037,12 @@ public List getCohortObservationResults(@PathParam("i * @param refresh Boolean - refresh visualization data * @return CohortObservationDrilldown */ - @GET - @Path("{sourceKey}/{id}/observation/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) - public CohortObservationDrilldown getCohortObservationResultsDrilldown(@PathParam("id") final int id, - @PathParam("conceptId") final int conceptId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortObservationDrilldown getCohortObservationResultsDrilldown(final int id, + final int conceptId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortObservationDrilldown drilldown = new CohortObservationDrilldown(); final String key = CohortResultsAnalysisRunner.OBSERVATION_DRILLDOWN; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1151,14 +1073,11 @@ public CohortObservationDrilldown getCohortObservationResultsDrilldown(@PathPara * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/measurement") - @Produces(MediaType.APPLICATION_JSON) - public List getCohortMeasurementResults(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getCohortMeasurementResults(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { List res = null; Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.MEASUREMENT; @@ -1197,14 +1116,11 @@ public List getCohortMeasurementResults(@PathParam("i * @param refresh Boolean - refresh visualization data * @return CohortMeasurementDrilldown */ - @GET - @Path("{sourceKey}/{id}/measurement/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) - public CohortMeasurementDrilldown getCohortMeasurementResultsDrilldown(@PathParam("id") final int id, @PathParam("conceptId") final int conceptId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortMeasurementDrilldown getCohortMeasurementResultsDrilldown(final int id, final int conceptId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortMeasurementDrilldown drilldown = new CohortMeasurementDrilldown(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.MEASUREMENT_DRILLDOWN; @@ -1235,14 +1151,11 @@ public CohortMeasurementDrilldown getCohortMeasurementResultsDrilldown(@PathPara * @param refresh Boolean - refresh visualization data * @return CohortObservationPeriod */ - @GET - @Path("{sourceKey}/{id}/observationperiod") - @Produces(MediaType.APPLICATION_JSON) - public CohortObservationPeriod getCohortObservationPeriod(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortObservationPeriod getCohortObservationPeriod(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortObservationPeriod obsPeriod = new CohortObservationPeriod(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.OBSERVATION_PERIOD; @@ -1272,14 +1185,11 @@ public CohortObservationPeriod getCohortObservationPeriod(@PathParam("id") final * @param refresh Boolean - refresh visualization data * @return CohortDataDensity */ - @GET - @Path("{sourceKey}/{id}/datadensity") - @Produces(MediaType.APPLICATION_JSON) - public CohortDataDensity getCohortDataDensity(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortDataDensity getCohortDataDensity(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortDataDensity data = new CohortDataDensity(); Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1311,14 +1221,11 @@ public CohortDataDensity getCohortDataDensity(@PathParam("id") final int id, * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/procedure/") - @Produces(MediaType.APPLICATION_JSON) - public List getProcedureTreemap(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getProcedureTreemap(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { List res = null; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1352,15 +1259,12 @@ public List getProcedureTreemap(@PathParam("id") fina * @param refresh Boolean - refresh visualization data * @return CohortProceduresDrillDown */ - @GET - @Path("{sourceKey}/{id}/procedure/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) - public CohortProceduresDrillDown getCohortProceduresDrilldown(@PathParam("id") final int id, - @PathParam("conceptId") final int conceptId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortProceduresDrillDown getCohortProceduresDrilldown(final int id, + final int conceptId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortProceduresDrillDown drilldown = new CohortProceduresDrillDown(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.PROCEDURE_DRILLDOWN; @@ -1391,14 +1295,11 @@ public CohortProceduresDrillDown getCohortProceduresDrilldown(@PathParam("id") f * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/visit/") - @Produces(MediaType.APPLICATION_JSON) - public List getVisitTreemap(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getVisitTreemap(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { List res = null; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1432,15 +1333,12 @@ public List getVisitTreemap(@PathParam("id") final in * @param refresh Boolean - refresh visualization data * @return */ - @GET - @Path("{sourceKey}/{id}/visit/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) - public CohortVisitsDrilldown getCohortVisitsDrilldown(@PathParam("id") final int id, - @PathParam("conceptId") final int conceptId, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortVisitsDrilldown getCohortVisitsDrilldown(final int id, + final int conceptId, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortVisitsDrilldown drilldown = new CohortVisitsDrilldown(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.VISIT_DRILLDOWN; @@ -1466,11 +1364,8 @@ public CohortVisitsDrilldown getCohortVisitsDrilldown(@PathParam("id") final int * @param sourceKey The source key * @return CohortSummary */ - @GET - @Path("{sourceKey}/{id}/summarydata") - @Produces(MediaType.APPLICATION_JSON) - public CohortSummary getCohortSummaryData(@PathParam("id") final int id, - @PathParam("sourceKey") String sourceKey) { + public CohortSummary getCohortSummaryData(final int id, + String sourceKey) { CohortSummary summary = new CohortSummary(); @@ -1509,14 +1404,11 @@ public CohortSummary getCohortSummaryData(@PathParam("id") final int id, * @param refresh Boolean - refresh visualization data * @return CohortDeathData */ - @GET - @Path("{sourceKey}/{id}/death") - @Produces(MediaType.APPLICATION_JSON) - public CohortDeathData getCohortDeathData(@PathParam("id") final int id, - @QueryParam("min_covariate_person_count") final Integer minCovariatePersonCountParam, - @QueryParam("min_interval_person_count") final Integer minIntervalPersonCountParam, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public CohortDeathData getCohortDeathData(final int id, + final Integer minCovariatePersonCountParam, + final Integer minIntervalPersonCountParam, + final String sourceKey, + boolean refresh) { CohortDeathData data = new CohortDeathData(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.DEATH; @@ -1542,10 +1434,7 @@ public CohortDeathData getCohortDeathData(@PathParam("id") final int id, * @param sourceKey The source key * @return CohortSummary */ - @GET - @Path("{sourceKey}/{id}/summaryanalyses") - @Produces(MediaType.APPLICATION_JSON) - public CohortSummary getCohortSummaryAnalyses(@PathParam("id") final int id, @PathParam("sourceKey") String sourceKey) { + public CohortSummary getCohortSummaryAnalyses(final int id, String sourceKey) { CohortSummary summary = new CohortSummary(); try { @@ -1566,10 +1455,7 @@ public CohortSummary getCohortSummaryAnalyses(@PathParam("id") final int id, @Pa * @param sourceKey The source key * @return Collection */ - @GET - @Path("{sourceKey}/{id}/breakdown") - @Produces(MediaType.APPLICATION_JSON) - public Collection getCohortBreakdown(@PathParam("id") final int id, @PathParam("sourceKey") String sourceKey) { + public Collection getCohortBreakdown(final int id, String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/cohortresults/sql/raw/getCohortBreakdown.sql"; String resultsTqName = "resultsTableQualifier"; @@ -1592,10 +1478,7 @@ public Collection getCohortBreakdown(@PathParam("id") final int * @param sourceKey The source key * @return The cohort count */ - @GET - @Path("{sourceKey}/{id}/members/count") - @Produces(MediaType.APPLICATION_JSON) - public Long getCohortMemberCount(@PathParam("id") final int id, @PathParam("sourceKey") String sourceKey) { + public Long getCohortMemberCount(final int id, String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/cohortresults/sql/raw/getMemberCount.sql"; String tqName = "tableQualifier"; @@ -1615,12 +1498,9 @@ public Long getCohortMemberCount(@PathParam("id") final int id, @PathParam("sour * @return List of all cohort analyses and their statuses for the given * cohort_defintion_id */ - @GET - @Path("{sourceKey}/{id}") - @Produces(MediaType.APPLICATION_JSON) - public List getCohortAnalysesForCohortDefinition(@PathParam("id") final int id, - @PathParam("sourceKey") String sourceKey, - @DefaultValue("true") @QueryParam("fullDetail") boolean retrieveFullDetail) { + public List getCohortAnalysesForCohortDefinition(final int id, + String sourceKey, + boolean retrieveFullDetail) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sql; @@ -1648,11 +1528,7 @@ public List getCohortAnalysesForCohortDefinition(@PathParam("id" * @param search The exposure cohort search * @return List */ - @POST - @Path("{sourceKey}/exposurecohortrates") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public List getExposureOutcomeCohortRates(@PathParam("sourceKey") String sourceKey, ExposureCohortSearch search) { + public List getExposureOutcomeCohortRates(String sourceKey, ExposureCohortSearch search) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetExposureOutcomeCohortRates(search, source); @@ -1693,11 +1569,7 @@ protected PreparedStatementRenderer prepareGetExposureOutcomeCohortRates( * @param search The exposure cohort search * @return List */ - @POST - @Path("{sourceKey}/timetoevent") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public List getTimeToEventDrilldown(@PathParam("sourceKey") String sourceKey, ExposureCohortSearch search) { + public List getTimeToEventDrilldown(String sourceKey, ExposureCohortSearch search) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetTimeToEventDrilldown(search, source); @@ -1736,11 +1608,7 @@ protected PreparedStatementRenderer prepareGetTimeToEventDrilldown( * @param search The exposure cohort search * @return List */ - @POST - @Path("{sourceKey}/predictors") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public List getExposureOutcomeCohortPredictors(@PathParam("sourceKey") String sourceKey, ExposureCohortSearch search) { + public List getExposureOutcomeCohortPredictors(String sourceKey, ExposureCohortSearch search) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetExposureOutcomeCohortPredictors(search, source); @@ -1776,12 +1644,9 @@ public List getExposureOutcomeCohortPredictors(@PathParam("sour * @param refresh Boolean - refresh visualization data * @return List */ - @GET - @Path("{sourceKey}/{id}/heraclesheel") - @Produces(MediaType.APPLICATION_JSON) - public List getHeraclesHeel(@PathParam("id") final int id, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("false") @QueryParam("refresh") boolean refresh) { + public List getHeraclesHeel(final int id, + final String sourceKey, + boolean refresh) { List attrs = new ArrayList(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.HERACLES_HEEL; @@ -1820,11 +1685,8 @@ public List getCohortAnalysesForDataCompleteness(final int id, * @param sourceKey The source key * @return List */ - @GET - @Path("{sourceKey}/{id}/datacompleteness") - @Produces(MediaType.APPLICATION_JSON) - public List getDataCompleteness(@PathParam("id") final int id, - @PathParam("sourceKey") String sourceKey) { + public List getDataCompleteness(final int id, + String sourceKey) { List arl = this.getCohortAnalysesForDataCompleteness(id, sourceKey); List dcal = new ArrayList<>(); @@ -1912,10 +1774,7 @@ public List getCohortAnalysesEntropy(final int id, String sourc * @param sourceKey The source key * @return List */ - @GET - @Path("{sourceKey}/{id}/entropy") - @Produces(MediaType.APPLICATION_JSON) - public List getEntropy(@PathParam("id") final int id, @PathParam("sourceKey") String sourceKey) { + public List getEntropy(final int id, String sourceKey) { List arl = this.getCohortAnalysesEntropy(id, sourceKey, 2031); List el = new ArrayList<>(); @@ -1938,10 +1797,7 @@ public List getEntropy(@PathParam("id") final int id, @PathParam("s * @param sourceKey The source key * @return List */ - @GET - @Path("{sourceKey}/{id}/allentropy") - @Produces(MediaType.APPLICATION_JSON) - public List getAllEntropy(@PathParam("id") final int id, @PathParam("sourceKey") String sourceKey) { + public List getAllEntropy(final int id, String sourceKey) { List arl = this.getCohortAnalysesEntropy(id, sourceKey, 2031); List el = new ArrayList(); @@ -1979,12 +1835,9 @@ public List getAllEntropy(@PathParam("id") final int id, @PathParam * @param periodType The period type * @return HealthcareExposureReport */ - @GET - @Path("{sourceKey}/{id}/healthcareutilization/exposure/{window}") - @Produces(MediaType.APPLICATION_JSON) - public HealthcareExposureReport getHealthcareUtilizationExposureReport(@PathParam("id") final int id, @PathParam("sourceKey") String sourceKey - , @PathParam("window") final WindowType window - , @DefaultValue("ww") @QueryParam("periodType") final PeriodType periodType) { + public HealthcareExposureReport getHealthcareUtilizationExposureReport(final int id, String sourceKey + , final WindowType window + , final PeriodType periodType) { Source source = getSourceRepository().findBySourceKey(sourceKey); HealthcareExposureReport exposureReport = queryRunner.getHealthcareExposureReport(getSourceJdbcTemplate(source), id, window, periodType, source); return exposureReport; @@ -1999,13 +1852,10 @@ public HealthcareExposureReport getHealthcareUtilizationExposureReport(@PathPara * @param window The time window * @return A list of the periods */ - @GET - @Path("{sourceKey}/{id}/healthcareutilization/periods/{window}") - @Produces(MediaType.APPLICATION_JSON) public List getHealthcareUtilizationPeriods( - @PathParam("id") final int id - , @PathParam("sourceKey") final String sourceKey - , @PathParam("window") final WindowType window) { + final int id + , final String sourceKey + , final WindowType window) { final Source source = getSourceRepository().findBySourceKey(sourceKey); final List periodTypes = queryRunner.getHealthcarePeriodTypes(getSourceJdbcTemplate(source), id, window, source); return periodTypes; @@ -2026,17 +1876,14 @@ public List getHealthcareUtilizationPeriods( * @param costTypeConcept The cost type concept ID * @return HealthcareVisitUtilizationReport */ - @GET - @Path("{sourceKey}/{id}/healthcareutilization/visit/{window}/{visitStat}") - @Produces(MediaType.APPLICATION_JSON) - public HealthcareVisitUtilizationReport getHealthcareUtilizationVisitReport(@PathParam("id") final int id - , @PathParam("sourceKey") String sourceKey - , @PathParam("window") final WindowType window - , @PathParam("visitStat") final VisitStatType visitStat - , @DefaultValue("ww") @QueryParam("periodType") final PeriodType periodType - , @QueryParam("visitConcept") final Long visitConcept - , @QueryParam("visitTypeConcept") final Long visitTypeConcept - , @DefaultValue("31968") @QueryParam("costTypeConcept") final Long costTypeConcept) { + public HealthcareVisitUtilizationReport getHealthcareUtilizationVisitReport(final int id + , String sourceKey + , final WindowType window + , final VisitStatType visitStat + , final PeriodType periodType + , final Long visitConcept + , final Long visitTypeConcept + , final Long costTypeConcept) { Source source = getSourceRepository().findBySourceKey(sourceKey); HealthcareVisitUtilizationReport visitUtilizationReport = queryRunner.getHealthcareVisitReport(getSourceJdbcTemplate(source), id, window, visitStat, periodType, visitConcept, visitTypeConcept, costTypeConcept, source); return visitUtilizationReport; @@ -2054,14 +1901,11 @@ public HealthcareVisitUtilizationReport getHealthcareUtilizationVisitReport(@Pat * @param costTypeConceptId The cost type concept ID * @return HealthcareDrugUtilizationSummary */ - @GET - @Path("{sourceKey}/{id}/healthcareutilization/drug/{window}") - @Produces(MediaType.APPLICATION_JSON) - public HealthcareDrugUtilizationSummary getHealthcareUtilizationDrugSummaryReport(@PathParam("id") final int id - , @PathParam("sourceKey") String sourceKey - , @PathParam("window") final WindowType window - , @QueryParam("drugType") final Long drugTypeConceptId - , @DefaultValue("31968") @QueryParam("costType") final Long costTypeConceptId + public HealthcareDrugUtilizationSummary getHealthcareUtilizationDrugSummaryReport(final int id + , String sourceKey + , final WindowType window + , final Long drugTypeConceptId + , final Long costTypeConceptId ) { Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -2083,16 +1927,13 @@ public HealthcareDrugUtilizationSummary getHealthcareUtilizationDrugSummaryRepor * @param costTypeConceptId The cost type concept ID * @return HealthcareDrugUtilizationDetail */ - @GET - @Path("{sourceKey}/{id}/healthcareutilization/drug/{window}/{drugConceptId}") - @Produces(MediaType.APPLICATION_JSON) - public HealthcareDrugUtilizationDetail getHealthcareUtilizationDrugDetailReport(@PathParam("id") final int id - , @PathParam("sourceKey") String sourceKey - , @PathParam("window") final WindowType window - , @PathParam("drugConceptId") final Long drugConceptId - , @DefaultValue("ww") @QueryParam("periodType") final PeriodType periodType - , @QueryParam("drugType") final Long drugTypeConceptId - , @DefaultValue("31968") @QueryParam("costType") final Long costTypeConceptId + public HealthcareDrugUtilizationDetail getHealthcareUtilizationDrugDetailReport(final int id + , String sourceKey + , final WindowType window + , final Long drugConceptId + , final PeriodType periodType + , final Long drugTypeConceptId + , final Long costTypeConceptId ) { Source source = getSourceRepository().findBySourceKey(sourceKey); HealthcareDrugUtilizationDetail report = queryRunner.getHealthcareDrugUtilizationReport(getSourceJdbcTemplate(source), id, window, drugConceptId, drugTypeConceptId, periodType, costTypeConceptId, source); @@ -2108,12 +1949,9 @@ public HealthcareDrugUtilizationDetail getHealthcareUtilizationDrugDetailReport( * @param drugConceptId The drug concept ID * @return A list of concepts of drug types */ - @GET - @Path("{sourceKey}/{id}/healthcareutilization/drugtypes") - @Produces(MediaType.APPLICATION_JSON) - public List getDrugTypes(@PathParam("id") final int id - , @PathParam("sourceKey") String sourceKey - , @QueryParam("drugConceptId") final Long drugConceptId) + public List getDrugTypes(final int id + , String sourceKey + , final Long drugConceptId) { Source source = getSourceRepository().findBySourceKey(sourceKey); return queryRunner.getDrugTypes(getSourceJdbcTemplate(source), id, drugConceptId, source); diff --git a/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java b/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java index 4da9844a36..92a41b148c 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java @@ -14,25 +14,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.util.*; import java.util.Optional; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; -@Path("/cohortsample") @Component -@Produces(MediaType.APPLICATION_JSON) public class CohortSampleService { private final CohortDefinitionRepository cohortDefinitionRepository; private final CohortGenerationInfoRepository generationInfoRepository; @@ -59,11 +49,9 @@ public CohortSampleService( * @param sourceKey * @return JSON containing information about cohort samples */ - @Path("/{cohortDefinitionId}/{sourceKey}") - @GET public CohortSampleListDTO listCohortSamples( - @PathParam("cohortDefinitionId") int cohortDefinitionId, - @PathParam("sourceKey") String sourceKey + int cohortDefinitionId, + String sourceKey ) { Source source = getSource(sourceKey); CohortSampleListDTO result = new CohortSampleListDTO(); @@ -89,13 +77,11 @@ public CohortSampleListDTO listCohortSamples( * @param fields * @return personId, gender, age of each person in the cohort sample */ - @Path("/{cohortDefinitionId}/{sourceKey}/{sampleId}") - @GET public CohortSampleDTO getCohortSample( - @PathParam("cohortDefinitionId") int cohortDefinitionId, - @PathParam("sourceKey") String sourceKey, - @PathParam("sampleId") Integer sampleId, - @DefaultValue("") @QueryParam("fields") String fields + int cohortDefinitionId, + String sourceKey, + Integer sampleId, + String fields ) { List returnFields = Arrays.asList(fields.split(",")); boolean withRecordCounts = returnFields.contains("recordCount"); @@ -111,13 +97,11 @@ public CohortSampleDTO getCohortSample( * @param fields * @return A sample of persons from a cohort */ - @Path("/{cohortDefinitionId}/{sourceKey}/{sampleId}/refresh") - @POST public CohortSampleDTO refreshCohortSample( - @PathParam("cohortDefinitionId") int cohortDefinitionId, - @PathParam("sourceKey") String sourceKey, - @PathParam("sampleId") Integer sampleId, - @DefaultValue("") @QueryParam("fields") String fields + int cohortDefinitionId, + String sourceKey, + Integer sampleId, + String fields ) { List returnFields = Arrays.asList(fields.split(",")); boolean withRecordCounts = returnFields.contains("recordCount"); @@ -130,10 +114,8 @@ public CohortSampleDTO refreshCohortSample( * @param cohortDefinitionId * @return true or false */ - @Path("/has-samples/{cohortDefinitionId}") - @GET public Map hasSamples( - @PathParam("cohortDefinitionId") int cohortDefinitionId + int cohortDefinitionId ) { int nSamples = this.samplingService.countSamples(cohortDefinitionId); return Collections.singletonMap("hasSamples", nSamples > 0); @@ -145,11 +127,9 @@ public Map hasSamples( * @param cohortDefinitionId * @return true or false */ - @Path("/has-samples/{cohortDefinitionId}/{sourceKey}") - @GET public Map hasSamples( - @PathParam("sourceKey") String sourceKey, - @PathParam("cohortDefinitionId") int cohortDefinitionId + String sourceKey, + int cohortDefinitionId ) { Source source = getSource(sourceKey); int nSamples = this.samplingService.countSamples(cohortDefinitionId, source.getId()); @@ -163,23 +143,20 @@ public Map hasSamples( * @param sampleParameters * @return */ - @Path("/{cohortDefinitionId}/{sourceKey}") - @POST - @Consumes(MediaType.APPLICATION_JSON) public CohortSampleDTO createCohortSample( - @PathParam("sourceKey") String sourceKey, - @PathParam("cohortDefinitionId") int cohortDefinitionId, + String sourceKey, + int cohortDefinitionId, SampleParametersDTO sampleParameters ) { sampleParameters.validate(); Source source = getSource(sourceKey); if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { - throw new NotFoundException("Cohort definition " + cohortDefinitionId + " does not exist."); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort definition " + cohortDefinitionId + " does not exist."); } CohortGenerationInfo generationInfo = generationInfoRepository.findById( new CohortGenerationInfoId(cohortDefinitionId, source.getId())).orElse(null); if (generationInfo == null || generationInfo.getStatus() != GenerationStatus.COMPLETE) { - throw new BadRequestException("Cohort is not yet generated"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cohort is not yet generated"); } return samplingService.createSample(source, cohortDefinitionId, sampleParameters); } @@ -191,19 +168,17 @@ public CohortSampleDTO createCohortSample( * @param sampleId * @return */ - @Path("/{cohortDefinitionId}/{sourceKey}/{sampleId}") - @DELETE - public Response deleteCohortSample( - @PathParam("sourceKey") String sourceKey, - @PathParam("cohortDefinitionId") int cohortDefinitionId, - @PathParam("sampleId") int sampleId + public ResponseEntity deleteCohortSample( + String sourceKey, + int cohortDefinitionId, + int sampleId ) { Source source = getSource(sourceKey); if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { - throw new NotFoundException("Cohort definition " + cohortDefinitionId + " does not exist."); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort definition " + cohortDefinitionId + " does not exist."); } samplingService.deleteSample(cohortDefinitionId, source, sampleId); - return Response.status(Response.Status.NO_CONTENT).build(); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } /** @@ -212,24 +187,22 @@ public Response deleteCohortSample( * @param cohortDefinitionId * @return */ - @Path("/{cohortDefinitionId}/{sourceKey}") - @DELETE - public Response deleteCohortSamples( - @PathParam("sourceKey") String sourceKey, - @PathParam("cohortDefinitionId") int cohortDefinitionId + public ResponseEntity deleteCohortSamples( + String sourceKey, + int cohortDefinitionId ) { Source source = getSource(sourceKey); if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { - throw new NotFoundException("Cohort definition " + cohortDefinitionId + " does not exist."); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort definition " + cohortDefinitionId + " does not exist."); } samplingService.launchDeleteSamplesTasklet(cohortDefinitionId, source.getId()); - return Response.status(Response.Status.ACCEPTED).build(); + return ResponseEntity.status(HttpStatus.ACCEPTED).build(); } private Source getSource(String sourceKey) { Source source = sourceRepository.findBySourceKey(sourceKey); if (source == null) { - throw new NotFoundException("Source " + sourceKey + " does not exist"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Source " + sourceKey + " does not exist"); } return source; } diff --git a/src/main/java/org/ohdsi/webapi/service/CohortService.java b/src/main/java/org/ohdsi/webapi/service/CohortService.java index 6cc169639b..a6d28fdc4e 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortService.java @@ -3,13 +3,6 @@ import java.util.List; import jakarta.persistence.EntityManager; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; import org.ohdsi.webapi.cohort.CohortEntity; import org.ohdsi.webapi.cohort.CohortRepository; @@ -22,7 +15,6 @@ /** * Service to read/write to the Cohort table */ -@Path("/cohort/") @Component public class CohortService { @@ -42,10 +34,7 @@ public class CohortService { * @param id Cohort Definition id * @return List of CohortEntity */ - @GET - @Path("{id}") - @Produces(MediaType.APPLICATION_JSON) - public List getCohortListById(@PathParam("id") final long id) { + public List getCohortListById(final long id) { List d = this.cohortRepository.getAllCohortsForId(id); return d; @@ -57,10 +46,6 @@ public List getCohortListById(@PathParam("id") final long id) { * @param cohort List of CohortEntity * @return status */ - @POST - @Path("import") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.TEXT_PLAIN) public String saveCohortListToCDM(final List cohort) { this.transactionTemplate.execute(new TransactionCallback() { diff --git a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java index eb03acc648..46b998d230 100644 --- a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java +++ b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java @@ -21,9 +21,6 @@ import java.util.stream.StreamSupport; import jakarta.transaction.Transactional; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -78,6 +75,10 @@ import org.springframework.core.convert.support.GenericConversionService; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.stereotype.Component; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; /** * Provides REST services for working with @@ -87,7 +88,6 @@ */ @Component @Transactional -@Path("/conceptset/") public class ConceptSetService extends AbstractDaoService implements HasTags { //create cache @Component @@ -158,10 +158,7 @@ public void customize(CacheManager cacheManager) { * @param id The concept set ID * @return The concept set definition */ - @Path("{id}") - @GET - @Produces(MediaType.APPLICATION_JSON) - public ConceptSetDTO getConceptSet(@PathParam("id") final int id) { + public ConceptSetDTO getConceptSet(final int id) { ConceptSet conceptSet = getConceptSetRepository().findById(id).orElse(null); ExceptionUtils.throwNotFoundExceptionIfNull(conceptSet, String.format("There is no concept set with id = %d.", id)); return conversionService.convert(conceptSet, ConceptSetDTO.class); @@ -173,9 +170,6 @@ public ConceptSetDTO getConceptSet(@PathParam("id") final int id) { * @summary Get all concept sets * @return A list of all concept sets in the WebAPI database */ - @GET - @Path("/") - @Produces(MediaType.APPLICATION_JSON) @Cacheable(cacheNames = ConceptSetService.CachingSetup.CONCEPT_SET_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()") public Collection getConceptSets() { return getTransactionTemplate().execute( @@ -198,10 +192,7 @@ public Collection getConceptSets() { * @param id The concept set identifier * @return A list of concept set items */ - @GET - @Path("{id}/items") - @Produces(MediaType.APPLICATION_JSON) - public Iterable getConceptSetItems(@PathParam("id") final int id) { + public Iterable getConceptSetItems(final int id) { return getConceptSetItemRepository().findAllByConceptSetId(id); } @@ -213,11 +204,8 @@ public Iterable getConceptSetItems(@PathParam("id") final int id * @param version The version identifier * @return The concept set expression */ - @GET - @Path("{id}/version/{version}/expression") - @Produces(MediaType.APPLICATION_JSON) - public ConceptSetExpression getConceptSetExpression(@PathParam("id") final int id, - @PathParam("version") final int version) { + public ConceptSetExpression getConceptSetExpression(final int id, + final int version) { SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); if (sourceInfo == null) { throw new UnauthorizedException(); @@ -237,12 +225,9 @@ public ConceptSetExpression getConceptSetExpression(@PathParam("id") final int i * @param sourceKey The source key * @return The concept set expression for the selected version */ - @GET - @Path("{id}/version/{version}/expression/{sourceKey}") - @Produces(MediaType.APPLICATION_JSON) - public ConceptSetExpression getConceptSetExpression(@PathParam("id") final int id, - @PathParam("version") final int version, - @PathParam("sourceKey") final String sourceKey) { + public ConceptSetExpression getConceptSetExpression(final int id, + final int version, + final String sourceKey) { SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); if (sourceInfo == null) { throw new UnauthorizedException(); @@ -257,10 +242,7 @@ public ConceptSetExpression getConceptSetExpression(@PathParam("id") final int i * @param id The concept set identifier * @return The concept set expression */ - @GET - @Path("{id}/expression") - @Produces(MediaType.APPLICATION_JSON) - public ConceptSetExpression getConceptSetExpression(@PathParam("id") final int id) { + public ConceptSetExpression getConceptSetExpression(final int id) { SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); if (sourceInfo == null) { throw new UnauthorizedException(); @@ -276,10 +258,7 @@ public ConceptSetExpression getConceptSetExpression(@PathParam("id") final int i * @param sourceKey The source key * @return The concept set expression */ - @GET - @Path("{id}/expression/{sourceKey}") - @Produces(MediaType.APPLICATION_JSON) - public ConceptSetExpression getConceptSetExpression(@PathParam("id") final int id, @PathParam("sourceKey") final String sourceKey) { + public ConceptSetExpression getConceptSetExpression(final int id, final String sourceKey) { Source source = sourceService.findBySourceKey(sourceKey); sourceAccessor.checkAccess(source); @@ -357,13 +336,12 @@ private ConceptSetExpression getConceptSetExpression(int id, Integer version, So * @return The concept set expression */ @Deprecated - @GET - @Path("{id}/{name}/exists") - @Produces(MediaType.APPLICATION_JSON) - public Response getConceptSetExistsDeprecated(@PathParam("id") final int id, @PathParam("name") String name) { + public ResponseEntity getConceptSetExistsDeprecated(final int id, String name) { String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: conceptset/{id}/exists?name={name}"; Collection cs = getConceptSetRepository().conceptSetExists(id, name); - return Response.ok(cs).header("Warning: 299", warningMessage).build(); + return ResponseEntity.ok() + .header("Warning", "299 - " + warningMessage) + .body(cs); } /** @@ -377,10 +355,7 @@ public Response getConceptSetExistsDeprecated(@PathParam("id") final int id, @Pa * @return The count of concept sets with the name, excluding the * specified concept set ID. */ - @GET - @Path("/{id}/exists") - @Produces(MediaType.APPLICATION_JSON) - public int getCountCSetWithSameName(@PathParam("id") @DefaultValue("0") final int id, @QueryParam("name") String name) { + public int getCountCSetWithSameName(final int id, String name) { return getConceptSetRepository().getCountCSetWithSameName(id, name); } @@ -397,11 +372,8 @@ public int getCountCSetWithSameName(@PathParam("id") @DefaultValue("0") final in * @param items An array of ConceptSetItems * @return Boolean: true if the save is successful */ - @PUT - @Path("{id}/items") - @Produces(MediaType.APPLICATION_JSON) @Transactional - public boolean saveConceptSetItems(@PathParam("id") final int id, ConceptSetItem[] items) { + public boolean saveConceptSetItems(final int id, ConceptSetItem[] items) { getConceptSetItemRepository().deleteByConceptSetId(id); for (ConceptSetItem csi : items) { @@ -425,11 +397,7 @@ public boolean saveConceptSetItems(@PathParam("id") final int id, ConceptSetItem * @return * @throws Exception */ - @GET - @Path("/exportlist") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response exportConceptSetList(@QueryParam("conceptsets") final String conceptSetList) throws Exception { + public ResponseEntity exportConceptSetList(final String conceptSetList) throws Exception { ArrayList conceptSetIds = new ArrayList<>(); try { String[] conceptSetItems = conceptSetList.split("\\+"); @@ -446,7 +414,7 @@ public Response exportConceptSetList(@QueryParam("conceptsets") final String con ByteArrayOutputStream baos; Source source = sourceService.getPriorityVocabularySource(); ArrayList cs = new ArrayList<>(); - Response response = null; + ResponseEntity response = null; try { // Load all of the concept sets requested for (int i = 0; i < conceptSetIds.size(); i++) { @@ -456,11 +424,11 @@ public Response exportConceptSetList(@QueryParam("conceptsets") final String con // Write Concept Set Expression to a CSV baos = ExportUtil.writeConceptSetExportToCSVAndZip(cs); - response = Response - .ok(baos) - .type(MediaType.APPLICATION_OCTET_STREAM) + response = ResponseEntity + .ok() + .contentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM) .header("Content-Disposition", "attachment; filename=\"conceptSetExport.zip\"") - .build(); + .body(baos); } catch (Exception ex) { throw ex; @@ -476,11 +444,7 @@ public Response exportConceptSetList(@QueryParam("conceptsets") final String con * @return A zip file containing the exported concept set * @throws Exception */ - @GET - @Path("{id}/export") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response exportConceptSetToCSV(@PathParam("id") final String id) throws Exception { + public ResponseEntity exportConceptSetToCSV(final String id) throws Exception { return this.exportConceptSetList(id); } @@ -491,10 +455,6 @@ public Response exportConceptSetToCSV(@PathParam("id") final String id) throws E * @param conceptSetDTO The concept set to save * @return The concept set saved with the concept set identifier */ - @Path("/") - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) public ConceptSetDTO createConceptSet(ConceptSetDTO conceptSetDTO) { @@ -519,10 +479,7 @@ public ConceptSetDTO createConceptSet(ConceptSetDTO conceptSetDTO) { * @return A map of the new concept set name and the existing concept set * name */ - @GET - @Path("/{id}/copy-name") - @Produces(MediaType.APPLICATION_JSON) - public Map getNameForCopy (@PathParam("id") final int id){ + public Map getNameForCopy (final int id){ ConceptSetDTO source = getConceptSet(id); String name = NameUtils.getNameForCopy(source.getName(), this::getNamesLike, getConceptSetRepository().findByName(source.getName())); return Collections.singletonMap(COPY_NAME, name); @@ -546,13 +503,9 @@ public List getNamesLike(String copyName) { * @return The * @throws Exception */ - @Path("/{id}") - @PUT - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) @Transactional @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public ConceptSetDTO updateConceptSet(@PathParam("id") final int id, ConceptSetDTO conceptSetDTO) throws Exception { + public ConceptSetDTO updateConceptSet(final int id, ConceptSetDTO conceptSetDTO) throws Exception { ConceptSet updated = getConceptSetRepository().findById(id).orElse(null); if (updated == null) { @@ -607,10 +560,7 @@ private ConceptSetExport getConceptSetForExport(int conceptSetId, SourceInfo voc * @param id The concept set identifier. * @return A collection of concept set generation info objects */ - @GET - @Path("{id}/generationinfo") - @Produces(MediaType.APPLICATION_JSON) - public Collection getConceptSetGenerationInfo(@PathParam("id") final int id) { + public Collection getConceptSetGenerationInfo(final int id) { return this.conceptSetGenerationInfoRepository.findAllByConceptSetId(id); } @@ -620,11 +570,9 @@ public Collection getConceptSetGenerationInfo(@PathPar * @summary Delete concept set * @param id The concept set ID */ - @DELETE @Transactional(rollbackOn = Exception.class, dontRollbackOn = EmptyResultDataAccessException.class) - @Path("{id}") @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public void deleteConceptSet(@PathParam("id") final int id) { + public void deleteConceptSet(final int id) { // Remove any generation info try { this.conceptSetGenerationInfoRepository.deleteByConceptSetId(id); @@ -667,12 +615,9 @@ public void deleteConceptSet(@PathParam("id") final int id) { * @param id The concept set ID * @param tagId The tag ID */ - @POST - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/tag/") @Transactional @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public void assignTag(@PathParam("id") final Integer id, final int tagId) { + public void assignTag(final Integer id, final int tagId) { ConceptSet entity = getConceptSetRepository().findById(id).orElse(null); assignTag(entity, tagId); } @@ -685,12 +630,9 @@ public void assignTag(@PathParam("id") final Integer id, final int tagId) { * @param id The concept set ID * @param tagId The tag ID */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/tag/{tagId}") @Transactional @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public void unassignTag(@PathParam("id") final Integer id, @PathParam("tagId") final int tagId) { + public void unassignTag(final Integer id, final int tagId) { ConceptSet entity = getConceptSetRepository().findById(id).orElse(null); unassignTag(entity, tagId); } @@ -703,11 +645,8 @@ public void unassignTag(@PathParam("id") final Integer id, @PathParam("tagId") f * @param id The concept set ID * @param tagId The tag ID */ - @POST - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/protectedtag/") @Transactional - public void assignPermissionProtectedTag(@PathParam("id") final int id, final int tagId) { + public void assignPermissionProtectedTag(final int id, final int tagId) { assignTag(id, tagId); } @@ -719,12 +658,9 @@ public void assignPermissionProtectedTag(@PathParam("id") final int id, final in * @param id The concept set ID * @param tagId The tag ID */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/protectedtag/{tagId}") @Transactional @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public void unassignPermissionProtectedTag(@PathParam("id") final int id, @PathParam("tagId") final int tagId) { + public void unassignPermissionProtectedTag(final int id, final int tagId) { unassignTag(id, tagId); } @@ -738,10 +674,6 @@ public void unassignPermissionProtectedTag(@PathParam("id") final int id, @PathP * @param conceptSetDTO The concept set * @return A check result */ - @POST - @Path("/check") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) @Transactional public CheckResult runDiagnostics(ConceptSetDTO conceptSetDTO) { return new CheckResult(checker.check(conceptSetDTO)); @@ -755,11 +687,8 @@ public CheckResult runDiagnostics(ConceptSetDTO conceptSetDTO) { * @param id The concept set ID * @return A list of version information */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/") @Transactional - public List getVersions(@PathParam("id") final int id) { + public List getVersions(final int id) { List versions = versionService.getVersions(VersionType.CONCEPT_SET, id); return versions.stream() .map(v -> conversionService.convert(v, VersionDTO.class)) @@ -775,11 +704,8 @@ public List getVersions(@PathParam("id") final int id) { * @param version The version ID * @return The concept set for the selected version */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") @Transactional - public ConceptSetVersionFullDTO getVersion(@PathParam("id") final int id, @PathParam("version") final int version) { + public ConceptSetVersionFullDTO getVersion(final int id, final int version) { checkVersion(id, version, false); ConceptSetVersion conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version); @@ -796,11 +722,8 @@ public ConceptSetVersionFullDTO getVersion(@PathParam("id") final int id, @PathP * @param updateDTO The version update * @return The version information */ - @PUT - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") @Transactional - public VersionDTO updateVersion(@PathParam("id") final int id, @PathParam("version") final int version, + public VersionDTO updateVersion(final int id, final int version, VersionUpdateDTO updateDTO) { checkVersion(id, version); updateDTO.setAssetId(id); @@ -818,11 +741,8 @@ public VersionDTO updateVersion(@PathParam("id") final int id, @PathParam("versi * @param id The concept ID * @param version THe version ID */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") @Transactional - public void deleteVersion(@PathParam("id") final int id, @PathParam("version") final int version) { + public void deleteVersion(final int id, final int version) { checkVersion(id, version); versionService.delete(VersionType.CONCEPT_SET, id, version); } @@ -837,12 +757,9 @@ public void deleteVersion(@PathParam("id") final int id, @PathParam("version") f * @param version The version ID * @return The concept set copy */ - @PUT - @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) { + public ConceptSetDTO copyAssetFromVersion(final int id, final int version) { checkVersion(id, version, false); ConceptSetVersion conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version); @@ -865,10 +782,6 @@ public ConceptSetDTO copyAssetFromVersion(@PathParam("id") final int id, @PathPa * @param requestDTO The tagNameListRequest * @return A list of concept sets with their assigned tags */ - @POST - @Path("/byTags") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public List listByTags(TagNameListRequestDTO requestDTO) { if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { return Collections.emptyList(); @@ -917,11 +830,8 @@ private ConceptSetVersion saveVersion(int id) { * @return Boolean: true if the save is successful * @summary Create new or delete concept set annotation items */ - @PUT - @Path("/{id}/annotation") - @Produces(MediaType.APPLICATION_JSON) @Transactional - public boolean saveConceptSetAnnotation(@PathParam("id") final int conceptSetId, SaveConceptSetAnnotationsRequest request) { + public boolean saveConceptSetAnnotation(final int conceptSetId, SaveConceptSetAnnotationsRequest request) { removeAnnotations(conceptSetId, request); if (request.getNewAnnotation() != null && !request.getNewAnnotation().isEmpty()) { List annotationList = request.getNewAnnotation() @@ -959,9 +869,6 @@ private void removeAnnotations(int id, SaveConceptSetAnnotationsRequest request) } } } - @POST - @Path("/copy-annotations") - @Produces(MediaType.APPLICATION_JSON) @Transactional public void copyAnnotations(CopyAnnotationsRequest copyAnnotationsRequest ) { List sourceAnnotations = getConceptSetAnnotationRepository().findByConceptSetId(copyAnnotationsRequest.getSourceConceptSetId()); @@ -990,11 +897,7 @@ private String appendCopiedFromConceptSetId(String copiedFromConceptSetIds, int } return copiedFromConceptSetIds.concat(",").concat(Integer.toString(sourceConceptSetId)); } - - @GET - @Path("/{id}/annotation") - @Produces(MediaType.APPLICATION_JSON) - public List getConceptSetAnnotation(@PathParam("id") final int id) { + public List getConceptSetAnnotation(final int id) { List annotationList = getConceptSetAnnotationRepository().findByConceptSetId(id); return annotationList.stream() .map(this::convertAnnotationEntityToDTO) @@ -1027,15 +930,11 @@ private AnnotationDTO convertAnnotationEntityToDTO(ConceptSetAnnotation conceptS annotationDTO.setCreatedDate(conceptSetAnnotation.getCreatedDate() != null ? conceptSetAnnotation.getCreatedDate().toString() : null); return annotationDTO; } - - @DELETE - @Path("/{conceptSetId}/annotation/{annotationId}") - @Produces(MediaType.APPLICATION_JSON) - public Response deleteConceptSetAnnotation(@PathParam("conceptSetId") final int conceptSetId, @PathParam("annotationId") final int annotationId) { + public ResponseEntity deleteConceptSetAnnotation(final int conceptSetId, final int annotationId) { ConceptSetAnnotation conceptSetAnnotation = getConceptSetAnnotationRepository().findById(annotationId); if (conceptSetAnnotation != null) { getConceptSetAnnotationRepository().deleteById(annotationId); - return Response.ok().build(); - } else throw new NotFoundException("Concept set annotation not found"); + return ResponseEntity.ok().build(); + } else throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Concept set annotation not found"); } } \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/service/DDLService.java b/src/main/java/org/ohdsi/webapi/service/DDLService.java index 393f8011b6..0927c92dd1 100644 --- a/src/main/java/org/ohdsi/webapi/service/DDLService.java +++ b/src/main/java/org/ohdsi/webapi/service/DDLService.java @@ -28,11 +28,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; import org.apache.commons.lang3.ObjectUtils; import org.ohdsi.circe.helper.ResourceHelper; import org.ohdsi.webapi.sqlrender.SourceStatement; @@ -40,7 +35,6 @@ import org.ohdsi.webapi.util.SessionUtils; import org.springframework.stereotype.Component; -@Path("/ddl/") @Component public class DDLService { @@ -132,15 +126,12 @@ public class DDLService { * @param tempSchema * @return SQL to create tables in results schema */ - @GET - @Path("results") - @Produces("text/plain") public String generateResultSQL( - @QueryParam("dialect") String dialect, - @DefaultValue("vocab") @QueryParam("vocabSchema") String vocabSchema, - @DefaultValue("results") @QueryParam("schema") String resultSchema, - @DefaultValue("true") @QueryParam("initConceptHierarchy") Boolean initConceptHierarchy, - @QueryParam("tempSchema") String tempSchema) { + String dialect, + String vocabSchema, + String resultSchema, + Boolean initConceptHierarchy, + String tempSchema) { Collection resultDDLFilePaths = new ArrayList<>(RESULT_DDL_FILE_PATHS); @@ -171,10 +162,7 @@ private Collection getResultInitFilePaths(String dialect) { * @param schema schema name * @return SQL */ - @GET - @Path("cemresults") - @Produces("text/plain") - public String generateCemResultSQL(@QueryParam("dialect") String dialect, @DefaultValue("cemresults") @QueryParam("schema") String schema) { + public String generateCemResultSQL(String dialect, String schema) { Map params = new HashMap() {{ put(CEM_SCHEMA, schema); @@ -190,13 +178,10 @@ public String generateCemResultSQL(@QueryParam("dialect") String dialect, @Defau * @param resultSchema results schema * @return SQL */ - @GET - @Path("achilles") - @Produces("text/plain") public String generateAchillesSQL( - @QueryParam("dialect") String dialect, - @DefaultValue("vocab") @QueryParam("vocabSchema") String vocabSchema, - @DefaultValue("results") @QueryParam("schema") String resultSchema) { + String dialect, + String vocabSchema, + String resultSchema) { final Collection achillesDDLFilePaths = new ArrayList<>(ACHILLES_DDL_FILE_PATHS); diff --git a/src/main/java/org/ohdsi/webapi/service/EvidenceService.java b/src/main/java/org/ohdsi/webapi/service/EvidenceService.java index c225acea8f..fe3561b8ec 100644 --- a/src/main/java/org/ohdsi/webapi/service/EvidenceService.java +++ b/src/main/java/org/ohdsi/webapi/service/EvidenceService.java @@ -10,17 +10,7 @@ import java.sql.SQLException; import java.util.Arrays; import java.util.stream.Collectors; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DefaultValue; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; + import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.json.JSONException; @@ -68,6 +58,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Component; +import org.springframework.http.ResponseEntity; /** * Provides REST services for querying the Common Evidence Model @@ -75,7 +66,6 @@ * @summary REST services for querying the Common Evidence Model See * https://github.com/OHDSI/CommonEvidenceModel */ -@Path("/evidence") @Component public class EvidenceService extends AbstractDaoService implements GeneratesNotification { @@ -152,10 +142,7 @@ public String getSourceIds() { * @param cohortId The cohort Id * @return A list of studies related to the cohort */ - @GET - @Path("study/{cohortId}") - @Produces(MediaType.APPLICATION_JSON) - public Collection getCohortStudyMapping(@PathParam("cohortId") int cohortId) { + public Collection getCohortStudyMapping(int cohortId) { return cohortStudyMappingRepository.findByCohortDefinitionId(cohortId); } @@ -168,10 +155,7 @@ public Collection getCohortStudyMapping(@PathParam("cohortId * @param conceptId The concept Id of interest * @return A list of cohorts for the specified conceptId */ - @GET - @Path("mapping/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) - public Collection getConceptCohortMapping(@PathParam("conceptId") int conceptId) { + public Collection getConceptCohortMapping(int conceptId) { return mappingRepository.findByConceptId(conceptId); } @@ -186,10 +170,7 @@ public Collection getConceptCohortMapping(@PathParam("conc * @param conceptId The conceptId of interest * @return A list of concepts based on the conceptId of interest */ - @GET - @Path("conceptofinterest/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) - public Collection getConceptOfInterest(@PathParam("conceptId") int conceptId) { + public Collection getConceptOfInterest(int conceptId) { return conceptOfInterestMappingRepository.findAllByConceptId(conceptId); } @@ -205,10 +186,7 @@ public Collection getConceptOfInterest(@PathParam("con * @param setid The drug label setId * @return The set of drug labels that match the setId specified. */ - @GET - @Path("label/{setid}") - @Produces(MediaType.APPLICATION_JSON) - public Collection getDrugLabel(@PathParam("setid") String setid) { + public Collection getDrugLabel(String setid) { return drugLabelRepository.findAllBySetid(setid); } @@ -221,10 +199,7 @@ public Collection getDrugLabel(@PathParam("setid") String setid) { * @param searchTerm The search term * @return A list of drug labels matching the search term */ - @GET - @Path("labelsearch/{searchTerm}") - @Produces(MediaType.APPLICATION_JSON) - public Collection searchDrugLabels(@PathParam("searchTerm") String searchTerm) { + public Collection searchDrugLabels(String searchTerm) { return drugLabelRepository.searchNameContainsTerm(searchTerm); } @@ -236,10 +211,7 @@ public Collection searchDrugLabels(@PathParam("searchTerm") String se * @param sourceKey The source key containing the CEM daimon * @return A collection of evidence information stored in CEM */ - @GET - @Path("{sourceKey}/info") - @Produces(MediaType.APPLICATION_JSON) - public Collection getInfo(@PathParam("sourceKey") String sourceKey) { + public Collection getInfo(String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/evidence/sql/getInfo.sql"; String tqName = "cem_schema"; @@ -269,11 +241,7 @@ public Collection getInfo(@PathParam("sourceKey") String sourceKey * @param searchParams * @return */ - @POST - @Path("{sourceKey}/drugconditionpairs") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public Collection getDrugConditionPairs(@PathParam("sourceKey") String sourceKey, DrugConditionSourceSearchParams searchParams) { + public Collection getDrugConditionPairs(String sourceKey, DrugConditionSourceSearchParams searchParams) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sql = getDrugHoiEvidenceSQL(source, searchParams); return getSourceJdbcTemplate(source).query(sql, (rs, rowNum) -> { @@ -306,10 +274,7 @@ public Collection getDrugConditionPairs(@PathParam("sourceKey") * @param id - An RxNorm Drug Concept Id * @return A list of evidence */ - @GET - @Path("{sourceKey}/drug/{id}") - @Produces(MediaType.APPLICATION_JSON) - public Collection getDrugEvidence(@PathParam("sourceKey") String sourceKey, @PathParam("id") final Long id) { + public Collection getDrugEvidence(String sourceKey, final Long id) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); return getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { @@ -345,10 +310,7 @@ public Collection getDrugEvidence(@PathParam("sourceKey") String s * @param id The conceptId for the health outcome of interest * @return A list of evidence */ - @GET - @Path("{sourceKey}/hoi/{id}") - @Produces(MediaType.APPLICATION_JSON) - public Collection getHoiEvidence(@PathParam("sourceKey") String sourceKey, @PathParam("id") final Long id) { + public Collection getHoiEvidence(String sourceKey, final Long id) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); return getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { @@ -384,11 +346,7 @@ public Collection getHoiEvidence(@PathParam("sourceKey") String sou * @param identifiers The list of RxNorm Ingredients concepts or ancestors * @return A list of evidence for the drug and HOI */ - @Path("{sourceKey}/druglabel") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getDrugIngredientLabel(@PathParam("sourceKey") String sourceKey, long[] identifiers) { + public Collection getDrugIngredientLabel(String sourceKey, long[] identifiers) { Source source = getSourceRepository().findBySourceKey(sourceKey); return executeGetDrugLabels(identifiers, source); } @@ -402,10 +360,7 @@ public Collection getDrugIngredientLabel(@PathParam("sourceKey") * @param key The key must be structured as {drugConceptId}-{hoiConceptId} * @return A list of evidence for the drug and HOI */ - @GET - @Path("{sourceKey}/drughoi/{key}") - @Produces(MediaType.APPLICATION_JSON) - public List getDrugHoiEvidence(@PathParam("sourceKey") String sourceKey, @PathParam("key") final String key) { + public List getDrugHoiEvidence(String sourceKey, final String key) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetDrugHoiEvidence(key, source); return getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { @@ -447,13 +402,10 @@ public List getDrugHoiEvidence(@PathParam("sourceKey") String s * drug, branded drug) * @return A list of evidence rolled up */ - @GET - @Path("{sourceKey}/drugrollup/{filter}/{id}") - @Produces(MediaType.APPLICATION_JSON) - public Response getDrugRollupIngredientEvidence(@PathParam("sourceKey") String sourceKey, @PathParam("id") final Long id, @PathParam("filter") final String filter) { + public ResponseEntity getDrugRollupIngredientEvidence(String sourceKey, final Long id, final String filter) { String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; ArrayList evidence = new ArrayList<>(); - return Response.ok(evidence).header("Warning: 299", warningMessage).build(); + return ResponseEntity.ok().header("Warning: 299", warningMessage).body(evidence); } /** @@ -465,10 +417,7 @@ public Response getDrugRollupIngredientEvidence(@PathParam("sourceKey") String s * @param id The conceptId of interest * @return A list of evidence matching the conceptId of interest */ - @GET - @Path("{sourceKey}/{id}") - @Produces(MediaType.APPLICATION_JSON) - public Collection getEvidence(@PathParam("sourceKey") String sourceKey, @PathParam("id") final Long id) { + public Collection getEvidence(String sourceKey, final Long id) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); return getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { @@ -510,13 +459,10 @@ public Collection getEvidence(@PathParam("sourceKey") String sourceKey * @param evidenceGroup The evidence group * @return A summary of evidence */ - @GET - @Path("{sourceKey}/evidencesummary") - @Produces(MediaType.APPLICATION_JSON) - public Response getEvidenceSummaryBySource(@PathParam("sourceKey") String sourceKey, @QueryParam("conditionID") String conditionID, @QueryParam("drugID") String drugID, @QueryParam("evidenceGroup") String evidenceGroup) { + public ResponseEntity getEvidenceSummaryBySource(String sourceKey, String conditionID, String drugID, String evidenceGroup) { String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; ArrayList evidenceSummary = new ArrayList<>(); - return Response.ok(evidenceSummary).header("Warning: 299", warningMessage).build(); + return ResponseEntity.ok().header("Warning: 299", warningMessage).body(evidenceSummary); } /** @@ -532,17 +478,14 @@ public Response getEvidenceSummaryBySource(@PathParam("sourceKey") String source * @throws org.codehaus.jettison.json.JSONException * @throws java.io.IOException */ - @GET - @Path("{sourceKey}/evidencedetails") - @Produces(MediaType.APPLICATION_JSON) - public Response getEvidenceDetails(@PathParam("sourceKey") String sourceKey, - @QueryParam("conditionID") String conditionID, - @QueryParam("drugID") String drugID, - @QueryParam("evidenceType") String evidenceType) + public ResponseEntity getEvidenceDetails(String sourceKey, + String conditionID, + String drugID, + String evidenceType) throws JSONException, IOException { String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; ArrayList evidenceDetails = new ArrayList<>(); - return Response.ok(evidenceDetails).header("Warning: 299", warningMessage).build(); + return ResponseEntity.ok().header("Warning: 299", warningMessage).body(evidenceDetails); } /** @@ -556,14 +499,10 @@ public Response getEvidenceDetails(@PathParam("sourceKey") String sourceKey, * @throws JSONException * @throws IOException */ - @POST - @Path("{sourceKey}/spontaneousreports") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Response getSpontaneousReports(@PathParam("sourceKey") String sourceKey, EvidenceSearch search) throws JSONException, IOException { + public ResponseEntity getSpontaneousReports(String sourceKey, EvidenceSearch search) throws JSONException, IOException { String warningMessage = "This method will be deprecated in the next release."; ArrayList returnVal = new ArrayList<>(); - return Response.ok(returnVal).header("Warning: 299", warningMessage).build(); + return ResponseEntity.ok().header("Warning: 299", warningMessage).body(returnVal); } /** @@ -577,14 +516,10 @@ public Response getSpontaneousReports(@PathParam("sourceKey") String sourceKey, * @throws JSONException * @throws IOException */ - @POST - @Path("{sourceKey}/evidencesearch") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Response evidenceSearch(@PathParam("sourceKey") String sourceKey, EvidenceSearch search) throws JSONException, IOException { + public ResponseEntity evidenceSearch(String sourceKey, EvidenceSearch search) throws JSONException, IOException { String warningMessage = "This method will be deprecated in the next release."; ArrayList returnVal = new ArrayList<>(); - return Response.ok(returnVal).header("Warning: 299", warningMessage).build(); + return ResponseEntity.ok().header("Warning: 299", warningMessage).body(returnVal); } /** @@ -598,14 +533,10 @@ public Response evidenceSearch(@PathParam("sourceKey") String sourceKey, Evidenc * @throws JSONException * @throws IOException */ - @POST - @Path("{sourceKey}/labelevidence") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Response labelEvidence(@PathParam("sourceKey") String sourceKey, EvidenceSearch search) throws JSONException, IOException { + public ResponseEntity labelEvidence(String sourceKey, EvidenceSearch search) throws JSONException, IOException { String warningMessage = "This method will be deprecated in the next release."; ArrayList returnVal = new ArrayList<>(); - return Response.ok(returnVal).header("Warning: 299", warningMessage).build(); + return ResponseEntity.ok().header("Warning: 299", warningMessage).body(returnVal); } /** @@ -618,11 +549,7 @@ public Response labelEvidence(@PathParam("sourceKey") String sourceKey, Evidence * @return information about the negative control job * @throws Exception */ - @POST - @Path("{sourceKey}/negativecontrols") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public JobExecutionResource queueNegativeControlsJob(@PathParam("sourceKey") String sourceKey, NegativeControlTaskParameters task) throws Exception { + public JobExecutionResource queueNegativeControlsJob(String sourceKey, NegativeControlTaskParameters task) throws Exception { if (task == null) { return null; } @@ -719,11 +646,7 @@ public JobExecutionResource queueNegativeControlsJob(@PathParam("sourceKey") Str * @param conceptSetId The concept set id * @return The list of negative controls */ - @GET - @Path("{sourceKey}/negativecontrols/{conceptsetid}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getNegativeControls(@PathParam("sourceKey") String sourceKey, @PathParam("conceptsetid") int conceptSetId) throws Exception { + public Collection getNegativeControls(String sourceKey, int conceptSetId) throws Exception { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = this.prepareGetNegativeControls(source, conceptSetId); final List recs = getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), new NegativeControlMapper()); @@ -737,13 +660,10 @@ public Collection getNegativeControls(@PathParam("sourceKey" * @param sourceKey The source key of the CEM daimon * @return The list of negative controls */ - @GET - @Path("{sourceKey}/negativecontrols/sql") - @Produces(MediaType.TEXT_PLAIN) - public String getNegativeControlsSqlStatement(@PathParam("sourceKey") String sourceKey, - @DefaultValue("CONDITION") @QueryParam("conceptDomain") String conceptDomain, - @DefaultValue("DRUG") @QueryParam("targetDomain") String targetDomain, - @DefaultValue("192671") @QueryParam("conceptOfInterest") String conceptOfInterest) { + public String getNegativeControlsSqlStatement(String sourceKey, + String conceptDomain, + String targetDomain, + String conceptOfInterest) { NegativeControlTaskParameters task = new NegativeControlTaskParameters(); Source source = getSourceRepository().findBySourceKey(sourceKey); task.setSource(source); diff --git a/src/main/java/org/ohdsi/webapi/service/FeasibilityService.java b/src/main/java/org/ohdsi/webapi/service/FeasibilityService.java index f22d08d3ca..cc22e9bc28 100644 --- a/src/main/java/org/ohdsi/webapi/service/FeasibilityService.java +++ b/src/main/java/org/ohdsi/webapi/service/FeasibilityService.java @@ -36,15 +36,6 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; import jakarta.servlet.ServletContext; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; import org.apache.commons.lang3.StringUtils; import org.ohdsi.circe.cohortdefinition.CohortExpression; import org.ohdsi.circe.cohortdefinition.CriteriaGroup; @@ -101,7 +92,6 @@ * * @summary Feasibility analysis (DO NOT USE) */ -@Path("/feasibility/") @Component public class FeasibilityService extends AbstractDaoService { @@ -135,7 +125,6 @@ public class FeasibilityService extends AbstractDaoService { @Autowired private SourceService sourceService; - @Context ServletContext context; private StudyGenerationInfo findStudyGenerationInfoBySourceId(Collection infoList, Integer sourceId) { @@ -384,9 +373,6 @@ public FeasibilityStudyDTO feasibilityStudyToDTO(FeasibilityStudy study) { * @deprecated * @return List */ - @GET - @Path("/") - @Produces(MediaType.APPLICATION_JSON) public List getFeasibilityStudyList() { return getTransactionTemplate().execute(transactionStatus -> { @@ -415,10 +401,6 @@ public List getFeasibilityStudyList * @param study The feasibility study * @return Feasibility study */ - @PUT - @Path("/") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) @Transactional public FeasibilityService.FeasibilityStudyDTO createStudy(FeasibilityService.FeasibilityStudyDTO study) { @@ -479,12 +461,8 @@ public FeasibilityService.FeasibilityStudyDTO createStudy(FeasibilityService.Fea * @param id The study ID * @return Feasibility study */ - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) @Transactional(readOnly = true) - public FeasibilityService.FeasibilityStudyDTO getStudy(@PathParam("id") final int id) { + public FeasibilityService.FeasibilityStudyDTO getStudy(final int id) { return getTransactionTemplate().execute(transactionStatus -> { FeasibilityStudy s = this.feasibilityStudyRepository.findOneWithDetail(id); @@ -501,11 +479,8 @@ public FeasibilityService.FeasibilityStudyDTO getStudy(@PathParam("id") final in * @param study The study information * @return The updated study information */ - @PUT - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) @Transactional - public FeasibilityService.FeasibilityStudyDTO saveStudy(@PathParam("id") final int id, FeasibilityStudyDTO study) { + public FeasibilityService.FeasibilityStudyDTO saveStudy(final int id, FeasibilityStudyDTO study) { Date currentTime = Calendar.getInstance().getTime(); UserEntity user = userRepository.findByLogin(security.getSubject()); @@ -564,11 +539,7 @@ public FeasibilityService.FeasibilityStudyDTO saveStudy(@PathParam("id") final i * @param sourceKey The source key * @return JobExecutionResource */ - @GET - @Path("/{study_id}/generate/{sourceKey}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public JobExecutionResource performStudy(@PathParam("study_id") final int study_id, @PathParam("sourceKey") final String sourceKey) { + public JobExecutionResource performStudy(final int study_id, final String sourceKey) { Date startTime = Calendar.getInstance().getTime(); Source source = this.getSourceRepository().findBySourceKey(sourceKey); @@ -671,11 +642,8 @@ public JobExecutionResource performStudy(@PathParam("study_id") final int study_ * @param id The study ID * @return List */ - @GET - @Path("/{id}/info") - @Produces(MediaType.APPLICATION_JSON) @Transactional(readOnly = true) - public List getSimulationInfo(@PathParam("id") final int id) { + public List getSimulationInfo(final int id) { FeasibilityStudy study = this.feasibilityStudyRepository.findById(id).orElse(null); List result = new ArrayList<>(); @@ -697,11 +665,8 @@ public List getSimulationInfo(@PathParam("id") final int id) { * @param sourceKey The source key * @return FeasibilityReport */ - @GET - @Path("/{id}/report/{sourceKey}") - @Produces(MediaType.APPLICATION_JSON) @Transactional - public FeasibilityReport getSimulationReport(@PathParam("id") final int id, @PathParam("sourceKey") final String sourceKey) { + public FeasibilityReport getSimulationReport(final int id, final String sourceKey) { Source source = this.getSourceRepository().findBySourceKey(sourceKey); @@ -725,11 +690,8 @@ public FeasibilityReport getSimulationReport(@PathParam("id") final int id, @Pat * @param id - the Cohort Definition ID to copy * @return the copied feasibility study as a FeasibilityStudyDTO */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/copy") @jakarta.transaction.Transactional - public FeasibilityStudyDTO copy(@PathParam("id") final int id) { + public FeasibilityStudyDTO copy(final int id) { FeasibilityStudyDTO sourceStudy = getStudy(id); sourceStudy.id = null; // clear the ID sourceStudy.name = String.format(Constants.Templates.ENTITY_COPY_PREFIX, sourceStudy.name); @@ -744,10 +706,7 @@ public FeasibilityStudyDTO copy(@PathParam("id") final int id) { * @deprecated * @param id The study ID */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}") - public void delete(@PathParam("id") final int id) { + public void delete(final int id) { feasibilityStudyRepository.deleteById(id); } @@ -759,11 +718,8 @@ public void delete(@PathParam("id") final int id) { * @param id The study ID * @param sourceKey The source key */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/info/{sourceKey}") @Transactional - public void deleteInfo(@PathParam("id") final int id, @PathParam("sourceKey") final String sourceKey) { + public void deleteInfo(final int id, final String sourceKey) { FeasibilityStudy study = feasibilityStudyRepository.findById(id).orElse(null); StudyGenerationInfo itemToRemove = null; for (StudyGenerationInfo info : study.getStudyGenerationInfoList()) diff --git a/src/main/java/org/ohdsi/webapi/service/HttpClient.java b/src/main/java/org/ohdsi/webapi/service/HttpClient.java deleted file mode 100644 index 3bb5b4382b..0000000000 --- a/src/main/java/org/ohdsi/webapi/service/HttpClient.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.ohdsi.webapi.service; - -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import jakarta.annotation.PostConstruct; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.WebTarget; -import org.glassfish.jersey.media.multipart.MultiPartFeature; -import org.springframework.stereotype.Component; - -@Component -public class HttpClient { - - private Client client; - - @PostConstruct - private void init() throws KeyManagementException, NoSuchAlgorithmException { - this.client = getClient(); - } - - private Client getClient() throws NoSuchAlgorithmException, KeyManagementException { - TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - @Override - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return null; - } - @Override - public void checkClientTrusted( - java.security.cert.X509Certificate[] certs, String authType) { - } - @Override - public void checkServerTrusted( - java.security.cert.X509Certificate[] certs, String authType) { - } - } - }; - SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, trustAllCerts, null); - return ClientBuilder.newBuilder() - .sslContext(sslContext) - .register(MultiPartFeature.class) - .build(); - } -} diff --git a/src/main/java/org/ohdsi/webapi/service/JobService.java b/src/main/java/org/ohdsi/webapi/service/JobService.java index 0c0dd364d4..ff9bc0c719 100644 --- a/src/main/java/org/ohdsi/webapi/service/JobService.java +++ b/src/main/java/org/ohdsi/webapi/service/JobService.java @@ -29,13 +29,6 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -51,7 +44,6 @@ * * @summary Jobs */ -@Path("/job/") @Component public class JobService extends AbstractDaoService { @@ -80,10 +72,7 @@ public JobService(JobExplorer jobExplorer, SearchableJobExecutionDao jobExecutio * @param jobId The job ID * @return The job information */ - @GET - @Path("{jobId}") - @Produces(MediaType.APPLICATION_JSON) - public JobInstanceResource findJob(@PathParam("jobId") final Long jobId) { + public JobInstanceResource findJob(final Long jobId) { final JobInstance job = this.jobExplorer.getJobInstance(jobId); if (job == null) { return null;//TODO #8 conventions under review @@ -99,10 +88,7 @@ public JobInstanceResource findJob(@PathParam("jobId") final Long jobId) { * @param jobType The job type * @return JobExecutionResource */ - @GET - @Path("/type/{jobType}/name/{jobName}") - @Produces(MediaType.APPLICATION_JSON) - public JobExecutionResource findJobByName(@PathParam("jobName") final String jobName, @PathParam("jobType") final String jobType) { + public JobExecutionResource findJobByName(final String jobName, final String jobType) { final Optional jobExecution = jobExplorer.findRunningJobExecutions(jobType).stream() .filter(job -> jobName.equals(job.getJobParameters().getString(Constants.Params.JOB_NAME))) .findFirst(); @@ -117,11 +103,8 @@ public JobExecutionResource findJobByName(@PathParam("jobName") final String job * @param executionId The execution ID * @return JobExecutionResource */ - @GET - @Path("{jobId}/execution/{executionId}") - @Produces(MediaType.APPLICATION_JSON) - public JobExecutionResource findJobExecution(@PathParam("jobId") final Long jobId, - @PathParam("executionId") final Long executionId) { + public JobExecutionResource findJobExecution(final Long jobId, + final Long executionId) { return service(jobId, executionId); } @@ -132,10 +115,7 @@ public JobExecutionResource findJobExecution(@PathParam("jobId") final Long jobI * @param executionId The job execution ID * @return JobExecutionResource */ - @GET - @Path("/execution/{executionId}") - @Produces(MediaType.APPLICATION_JSON) - public JobExecutionResource findJobExecution(@PathParam("executionId") final Long executionId) { + public JobExecutionResource findJobExecution(final Long executionId) { return service(null, executionId); } @@ -156,8 +136,6 @@ private JobExecutionResource service(final Long jobId, final Long executionId) { * @summary Get list of jobs * @return A list of jobs */ - @GET - @Produces(MediaType.APPLICATION_JSON) public List findJobNames() { return this.jobExplorer.getJobNames(); } @@ -178,13 +156,10 @@ public List findJobNames() { * @return collection of JobExecutionInfo * @throws NoSuchJobException */ - @GET - @Path("/execution") - @Produces(MediaType.APPLICATION_JSON) - public Page list(@QueryParam("jobName") final String jobName, - @DefaultValue("0") @QueryParam("pageIndex") final Integer pageIndex, - @DefaultValue("20") @QueryParam("pageSize") final Integer pageSize, - @QueryParam("comprehensivePage") boolean comprehensivePage) + public Page list(final String jobName, + final Integer pageIndex, + final Integer pageSize, + boolean comprehensivePage) throws NoSuchJobException { List resources = null; diff --git a/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java b/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java index aa6c9bc58b..3cf0005dc7 100644 --- a/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java +++ b/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java @@ -5,11 +5,6 @@ import java.util.Collections; import java.util.Map; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; import org.apache.commons.lang3.StringUtils; import org.ohdsi.sql.SqlRender; import org.ohdsi.sql.SqlTranslate; @@ -21,17 +16,12 @@ * * @author Lee Evans */ -@Path("/sqlrender/") public class SqlRenderService { /** * Translate an OHDSI SQL to a supported target SQL dialect * @param sourceStatement JSON with parameters, source SQL, and target dialect * @return rendered and translated SQL */ - @Path("translate") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public TranslatedStatement translateSQLFromSourceStatement(SourceStatement sourceStatement) { if (sourceStatement == null) { return new TranslatedStatement(); diff --git a/src/main/java/org/ohdsi/webapi/service/UserService.java b/src/main/java/org/ohdsi/webapi/service/UserService.java index 8076e8e96b..9b50b4fa2e 100644 --- a/src/main/java/org/ohdsi/webapi/service/UserService.java +++ b/src/main/java/org/ohdsi/webapi/service/UserService.java @@ -12,8 +12,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; import java.util.*; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -23,7 +21,6 @@ * @author gennadiy.anisimov */ -@Path("/") @Component public class UserService { @@ -92,19 +89,11 @@ public int compareTo(Permission o) { return c.compare(this.id, o.id); } } - - @GET - @Path("user") - @Produces(MediaType.APPLICATION_JSON) public ArrayList getUsers() { Iterable userEntities = this.authorizer.getUsers(); ArrayList users = convertUsers(userEntities); return users; } - - @GET - @Path("user/me") - @Produces(MediaType.APPLICATION_JSON) public User getCurrentUser() throws Exception { UserEntity currentUser = this.authorizer.getCurrentUser(); @@ -120,31 +109,18 @@ public User getCurrentUser() throws Exception { return user; } - - @GET - @Path("user/{userId}/permissions") - @Produces(MediaType.APPLICATION_JSON) - public List getUsersPermissions(@PathParam("userId") Long userId) throws Exception { + public List getUsersPermissions(Long userId) throws Exception { Set permissionEntities = this.authorizer.getUserPermissions(userId); List permissions = convertPermissions(permissionEntities); Collections.sort(permissions); return permissions; } - - @GET - @Path("user/{userId}/roles") - @Produces(MediaType.APPLICATION_JSON) - public ArrayList getUserRoles(@PathParam("userId") Long userId) throws Exception { + public ArrayList getUserRoles(Long userId) throws Exception { Set roleEntities = this.authorizer.getUserRoles(userId); ArrayList roles = convertRoles(roleEntities); Collections.sort(roles); return roles; } - - @POST - @Path("role") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) public Role createRole(Role role) throws Exception { RoleEntity roleEntity = this.authorizer.addRole(role.role, true); RoleEntity personalRole = this.authorizer.getCurrentUserPersonalRole(); @@ -156,12 +132,7 @@ public Role createRole(Role role) throws Exception { eventPublisher.publishEvent(new AddRoleEvent(this, newRole.id, newRole.role)); return newRole; } - - @PUT - @Path("role/{roleId}") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public Role updateRole(@PathParam("roleId") Long id, Role role) throws Exception { + public Role updateRole(Long id, Role role) throws Exception { RoleEntity roleEntity = this.authorizer.getRole(id); if (roleEntity == null) { throw new Exception("Role doesn't exist"); @@ -171,46 +142,28 @@ public Role updateRole(@PathParam("roleId") Long id, Role role) throws Exception eventPublisher.publishEvent(new ChangeRoleEvent(this, id, role.role)); return new Role(roleEntity); } - - @GET - @Path("role") - @Produces(MediaType.APPLICATION_JSON) public ArrayList getRoles( - @DefaultValue("false") @QueryParam("include_personal") boolean includePersonalRoles) { + boolean includePersonalRoles) { Iterable roleEntities = this.authorizer.getRoles(includePersonalRoles); ArrayList roles = convertRoles(roleEntities); return roles; } - - @GET - @Path("role/{roleId}") - @Produces(MediaType.APPLICATION_JSON) - public Role getRole(@PathParam("roleId") Long id) { + public Role getRole(Long id) { RoleEntity roleEntity = this.authorizer.getRole(id); Role role = new Role(roleEntity); return role; } - - @DELETE - @Path("role/{roleId}") - public void removeRole(@PathParam("roleId") Long roleId) { + public void removeRole(Long roleId) { this.authorizer.removeRole(roleId); this.authorizer.removePermissionsFromTemplate(this.roleCreatorPermissionsTemplate, String.valueOf(roleId)); } - - @GET - @Path("role/{roleId}/permissions") - @Produces(MediaType.APPLICATION_JSON) - public List getRolePermissions(@PathParam("roleId") Long roleId) throws Exception { + public List getRolePermissions(Long roleId) throws Exception { Set permissionEntities = this.authorizer.getRolePermissions(roleId); List permissions = convertPermissions(permissionEntities); Collections.sort(permissions); return permissions; } - - @PUT - @Path("role/{roleId}/permissions/{permissionIdList}") - public void addPermissionToRole(@PathParam("roleId") Long roleId, @PathParam("permissionIdList") String permissionIdList) throws Exception { + public void addPermissionToRole(Long roleId, String permissionIdList) throws Exception { String[] ids = permissionIdList.split("\\+"); for (String permissionIdString : ids) { Long permissionId = Long.parseLong(permissionIdString); @@ -218,10 +171,7 @@ public void addPermissionToRole(@PathParam("roleId") Long roleId, @PathParam("pe eventPublisher.publishEvent(new AddPermissionEvent(this, permissionId, roleId)); } } - - @DELETE - @Path("role/{roleId}/permissions/{permissionIdList}") - public void removePermissionFromRole(@PathParam("roleId") Long roleId, @PathParam("permissionIdList") String permissionIdList) { + public void removePermissionFromRole(Long roleId, String permissionIdList) { String[] ids = permissionIdList.split("\\+"); for (String permissionIdString : ids) { Long permissionId = Long.parseLong(permissionIdString); @@ -229,20 +179,13 @@ public void removePermissionFromRole(@PathParam("roleId") Long roleId, @PathPara eventPublisher.publishEvent(new DeletePermissionEvent(this, permissionId, roleId)); } } - - @GET - @Path("role/{roleId}/users") - @Produces(MediaType.APPLICATION_JSON) - public ArrayList getRoleUsers(@PathParam("roleId") Long roleId) throws Exception { + public ArrayList getRoleUsers(Long roleId) throws Exception { Set userEntities = this.authorizer.getRoleUsers(roleId); ArrayList users = this.convertUsers(userEntities); Collections.sort(users); return users; } - - @PUT - @Path("role/{roleId}/users/{userIdList}") - public void addUserToRole(@PathParam("roleId") Long roleId, @PathParam("userIdList") String userIdList) throws Exception { + public void addUserToRole(Long roleId, String userIdList) throws Exception { String[] ids = userIdList.split("\\+"); for (String userIdString : ids) { Long userId = Long.parseLong(userIdString); @@ -250,10 +193,7 @@ public void addUserToRole(@PathParam("roleId") Long roleId, @PathParam("userIdLi eventPublisher.publishEvent(new AssignRoleEvent(this, roleId, userId)); } } - - @DELETE - @Path("role/{roleId}/users/{userIdList}") - public void removeUserFromRole(@PathParam("roleId") Long roleId, @PathParam("userIdList") String userIdList) { + public void removeUserFromRole(Long roleId, String userIdList) { String[] ids = userIdList.split("\\+"); for (String userIdString : ids) { Long userId = Long.parseLong(userIdString); diff --git a/src/main/java/org/ohdsi/webapi/service/VocabularyService.java b/src/main/java/org/ohdsi/webapi/service/VocabularyService.java index a12c2b8a2b..8e262395aa 100644 --- a/src/main/java/org/ohdsi/webapi/service/VocabularyService.java +++ b/src/main/java/org/ohdsi/webapi/service/VocabularyService.java @@ -16,19 +16,6 @@ import javax.cache.CacheManager; import javax.cache.configuration.MutableConfiguration; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -83,6 +70,13 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Component; import org.ohdsi.webapi.vocabulary.MappedRelatedConcept; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; /** * Provides REST services for working with @@ -90,7 +84,6 @@ * * @summary Vocabulary */ -@Path("vocabulary/") @Component public class VocabularyService extends AbstractDaoService { @@ -179,7 +172,7 @@ public Source getPriorityVocabularySource() { Source source = sourceService.getPriorityVocabularySource(); if (Objects.isNull(source)) { - throw new ForbiddenException(); + throw new ResponseStatusException(HttpStatus.FORBIDDEN); } return source; } @@ -206,11 +199,7 @@ public ConceptSetExport exportConceptSet(ConceptSet conceptSet, SourceInfo vocab * @param ids Concepts identifiers from concept set * @return A map of the form: {id -> List} */ - @Path("{sourceKey}/lookup/identifiers/ancestors") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Map> calculateAscendants(@PathParam("sourceKey") String sourceKey, Ids ids) { + public Map> calculateAscendants(String sourceKey, Ids ids) { Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -280,11 +269,7 @@ protected PreparedStatementRenderer prepareAscendantsCalculating(Long[] identifi * @param identifiers an array of concept identifiers * @return A collection of concepts */ - @Path("{sourceKey}/lookup/identifiers") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection executeIdentifierLookup(@PathParam("sourceKey") String sourceKey, long[] identifiers) { + public Collection executeIdentifierLookup(String sourceKey, long[] identifiers) { Source source = getSourceRepository().findBySourceKey(sourceKey); return executeIdentifierLookup(source, identifiers); } @@ -326,15 +311,11 @@ protected PreparedStatementRenderer prepareExecuteIdentifierLookup(long[] identi * @param identifiers an array of concept identifiers * @return A collection of concepts */ - @Path("lookup/identifiers") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Collection executeIdentifierLookup(long[] identifiers) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return executeIdentifierLookup(defaultSourceKey, identifiers); } @@ -363,11 +344,7 @@ public Collection executeIncludedConceptLookup(String sourceKey, Concep * @param sourcecodes array of source codes * @return A collection of concepts */ - @Path("{sourceKey}/lookup/sourcecodes") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection executeSourcecodeLookup(@PathParam("sourceKey") String sourceKey, String[] sourcecodes) { + public Collection executeSourcecodeLookup(String sourceKey, String[] sourcecodes) { if (sourcecodes.length == 0) { return new ArrayList<>(); } @@ -394,15 +371,11 @@ protected PreparedStatementRenderer prepareExecuteSourcecodeLookup(String[] sour * @param sourcecodes array of source codes * @return A collection of concepts */ - @Path("lookup/sourcecodes") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Collection executeSourcecodeLookup(String[] sourcecodes) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return executeSourcecodeLookup(defaultSourceKey, sourcecodes); } @@ -419,11 +392,7 @@ public Collection executeSourcecodeLookup(String[] sourcecodes) { * @param identifiers an array of concept identifiers * @return A collection of concepts */ - @Path("{sourceKey}/lookup/mapped") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection executeMappedLookup(@PathParam("sourceKey") String sourceKey, long[] identifiers) { + public Collection executeMappedLookup(String sourceKey, long[] identifiers) { Source source = getSourceRepository().findBySourceKey(sourceKey); return executeMappedLookup(source, identifiers); } @@ -467,15 +436,11 @@ protected PreparedStatementRenderer prepareExecuteMappedLookup(long[] identifier * @param identifiers an array of concept identifiers * @return A collection of concepts */ - @Path("lookup/mapped") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Collection executeMappedLookup(long[] identifiers) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return executeMappedLookup(defaultSourceKey, identifiers); } @@ -502,11 +467,7 @@ public Collection executeMappedLookup(String sourceKey, ConceptSetExpre * @param search The ConceptSearch parameters * @return A collection of concepts */ - @Path("{sourceKey}/search") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection executeSearch(@PathParam("sourceKey") String sourceKey, ConceptSearch search) { + public Collection executeSearch(String sourceKey, ConceptSearch search) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareExecuteSearch(search, source); @@ -662,15 +623,11 @@ protected PreparedStatementRenderer prepareExecuteSearch(ConceptSearch search, S * @param search The ConceptSearch parameters * @return A collection of concepts */ - @Path("search") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Collection executeSearch(ConceptSearch search) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return executeSearch(defaultSourceKey, search); } @@ -683,10 +640,7 @@ public Collection executeSearch(ConceptSearch search) { * @param query The query to use to search for concepts * @return A collection of concepts */ - @Path("{sourceKey}/search/{query}") - @GET - @Produces(MediaType.APPLICATION_JSON) - public Collection executeSearch(@PathParam("sourceKey") String sourceKey, @PathParam("query") String query) { + public Collection executeSearch(String sourceKey, String query) { return this.executeSearch(sourceKey, query, DEFAULT_SEARCH_ROWS); } @@ -700,10 +654,7 @@ public Collection executeSearch(@PathParam("sourceKey") String sourceKe * @param rows The number of rows to return. * @return A collection of concepts */ - @Path("{sourceKey}/search") - @GET - @Produces(MediaType.APPLICATION_JSON) - public Collection executeSearch(@PathParam("sourceKey") String sourceKey, @QueryParam("query") String query, @DefaultValue(DEFAULT_SEARCH_ROWS) @QueryParam("rows") String rows) { + public Collection executeSearch(String sourceKey, String query, String rows) { // Verify that the rows parameter contains an integer and is > 0 try { Integer r = Integer.parseInt(rows); @@ -743,15 +694,12 @@ public PreparedStatementRenderer prepareExecuteSearchWithQuery(String query, Sou * @param query The query to use to search for concepts * @return A collection of concepts */ - @Path("search/{query}") - @GET - @Produces(MediaType.APPLICATION_JSON) - public Collection executeSearch(@PathParam("query") String query) { + public Collection executeSearch(String query) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return executeSearch(defaultSourceKey, query); } @@ -765,11 +713,8 @@ public Collection executeSearch(@PathParam("query") String query) { * @param id The concept ID to find * @return The concept details */ - @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) { + public Concept getConcept(final String sourceKey, final long id) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/vocabulary/sql/getConcept.sql"; String tqValue = source.getTableQualifier(SourceDaimon.DaimonType.Vocabulary); @@ -780,7 +725,7 @@ public Concept getConcept(@PathParam("sourceKey") final String sourceKey, @PathP concept = getSourceJdbcTemplate(source).queryForObject(psr.getSql(), psr.getOrderedParams(), this.rowMapper); } catch (EmptyResultDataAccessException e) { log.error("Request for conceptId={} resulted in 0 results", id); - throw new NotFoundException(String.format("There is no concept with id = %d.", id)); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, String.format("There is no concept with id = %d.", id)); } return concept; } @@ -793,14 +738,11 @@ public Concept getConcept(@PathParam("sourceKey") final String sourceKey, @PathP * @param id The concept ID to find * @return The concept details */ - @GET - @Path("concept/{id}") - @Produces(MediaType.APPLICATION_JSON) - public Concept getConcept(@PathParam("id") final long id) { + public Concept getConcept(final long id) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getConcept(defaultSourceKey, id); @@ -817,11 +759,8 @@ public Concept getConcept(@PathParam("id") final long id) { * @param id The concept ID to find * @return A collection of related concepts */ - @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) { + public Collection getRelatedConcepts(String sourceKey, final Long id) { final Map concepts = new HashMap<>(); Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/vocabulary/sql/getRelatedConcepts.sql"; @@ -835,11 +774,7 @@ public Collection getRelatedConcepts(@PathParam("sourceKey") Str return concepts.values(); } - - @POST - @Path("{sourceKey}/related-standard") - @Produces(MediaType.APPLICATION_JSON) - public Collection getRelatedStandardMappedConcepts(@PathParam("sourceKey") String sourceKey, List allConceptIds) { + public Collection getRelatedStandardMappedConcepts(String sourceKey, List allConceptIds) { Source source = getSourceRepository().findBySourceKey(sourceKey); String relatedConceptsSQLPath = "/resources/vocabulary/sql/getRelatedStandardMappedConcepts.sql"; String relatedMappedFromIdsSQLPath = "/resources/vocabulary/sql/getRelatedStandardMappedConcepts_getMappedFromIds.sql"; @@ -898,7 +833,7 @@ void enrichResultCombinedMappedConcepts(Map resultCo resultCombinedMappedConcepts.put(standardConceptId,mappedRelatedConcept); } catch (JsonProcessingException e) { log.error("Could not convert RelatedConcept to MappedRelatedConcept", e); - throw new WebApplicationException(e); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage(), e); } } }); @@ -913,11 +848,8 @@ void enrichResultCombinedMappedConcepts(Map resultCo * @param id The concept ID * @return A collection of related concepts */ - @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) { + public Collection getConceptAncestorAndDescendant(String sourceKey, final Long id) { final Map concepts = new HashMap<>(); Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/vocabulary/sql/getConceptAncestorAndDescendant.sql"; @@ -940,14 +872,11 @@ public Collection getConceptAncestorAndDescendant(@PathParam("so * @param id The concept identifier * @return A collection of related concepts */ - @GET - @Path("concept/{id}/related") - @Produces(MediaType.APPLICATION_JSON) - public Collection getRelatedConcepts(@PathParam("id") final Long id) { + public Collection getRelatedConcepts(final Long id) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getRelatedConcepts(defaultSourceKey, id); } @@ -961,11 +890,7 @@ public Collection getRelatedConcepts(@PathParam("id") final Long * @param identifiers An array of concept identifiers * @return A collection of related concepts */ - @POST - @Path("{sourceKey}/commonAncestors") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getCommonAncestors(@PathParam("sourceKey") String sourceKey, Object[] identifiers) { + public Collection getCommonAncestors(String sourceKey, Object[] identifiers) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetCommonAncestors(identifiers, source); final Map concepts = new HashMap<>(); @@ -999,15 +924,11 @@ protected PreparedStatementRenderer prepareGetCommonAncestors(Object[] identifie * @param identifiers An array of concept identifiers * @return A collection of related concepts */ - @POST - @Path("/commonAncestors") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Collection getCommonAncestors(Object[] identifiers) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getCommonAncestors(defaultSourceKey, identifiers); } @@ -1021,11 +942,7 @@ public Collection getCommonAncestors(Object[] identifiers) { * @param conceptSetExpression A concept set expression * @return A collection of concept identifiers */ - @POST - @Path("{sourceKey}/resolveConceptSetExpression") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection resolveConceptSetExpression(@PathParam("sourceKey") String sourceKey, ConceptSetExpression conceptSetExpression) { + public Collection resolveConceptSetExpression(String sourceKey, ConceptSetExpression conceptSetExpression) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = new ConceptSetStrategy(conceptSetExpression).prepareStatement(source, null); final ArrayList identifiers = new ArrayList<>(); @@ -1047,15 +964,11 @@ public void processRow(ResultSet rs) throws SQLException { * @param conceptSetExpression A concept set expression * @return A collection of concept identifiers */ - @POST - @Path("resolveConceptSetExpression") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Collection resolveConceptSetExpression(ConceptSetExpression conceptSetExpression) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return resolveConceptSetExpression(defaultSourceKey, conceptSetExpression); } @@ -1069,11 +982,7 @@ public Collection resolveConceptSetExpression(ConceptSetExpression concept * @param conceptSetExpression A concept set expression * @return A count of included concepts */ - @POST - @Path("{sourceKey}/included-concepts/count") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Integer countIncludedConceptSets(@PathParam("sourceKey") String sourceKey, ConceptSetExpression conceptSetExpression) { + public Integer countIncludedConceptSets(String sourceKey, ConceptSetExpression conceptSetExpression) { Source source = getSourceRepository().findBySourceKey(sourceKey); String query = new ConceptSetStrategy(conceptSetExpression).prepareStatement(source, sql -> "select count(*) from (" + sql + ") Q;").getSql(); @@ -1088,15 +997,11 @@ public Integer countIncludedConceptSets(@PathParam("sourceKey") String sourceKey * @param conceptSetExpression A concept set expression * @return A count of included concepts */ - @POST - @Path("included-concepts/count") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Integer countIncludedConcepSets(ConceptSetExpression conceptSetExpression) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (Objects.isNull(defaultSourceKey)) { - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); } return countIncludedConceptSets(defaultSourceKey, conceptSetExpression); } @@ -1110,10 +1015,6 @@ public Integer countIncludedConcepSets(ConceptSetExpression conceptSetExpression * @param conceptSetExpression A concept set expression * @return SQL Statement as text */ - @POST - @Path("conceptSetExpressionSQL") - @Produces(MediaType.TEXT_PLAIN) - @Consumes(MediaType.APPLICATION_JSON) public String getConceptSetExpressionSQL(ConceptSetExpression conceptSetExpression) { ConceptSetExpressionQueryBuilder builder = new ConceptSetExpressionQueryBuilder(); String query = builder.buildExpressionQuery(conceptSetExpression); @@ -1130,10 +1031,7 @@ public String getConceptSetExpressionSQL(ConceptSetExpression conceptSetExpressi * @param id The concept identifier * @return A collection of concepts */ - @GET - @Path("{sourceKey}/concept/{id}/descendants") - @Produces(MediaType.APPLICATION_JSON) - public Collection getDescendantConcepts(@PathParam("sourceKey") String sourceKey, @PathParam("id") final Long id) { + public Collection getDescendantConcepts(String sourceKey, final Long id) { final Map concepts = new HashMap<>(); Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/vocabulary/sql/getDescendantConcepts.sql"; @@ -1158,14 +1056,11 @@ public Void mapRow(ResultSet resultSet, int arg1) throws SQLException { * @param id The concept identifier * @return A collection of concepts */ - @GET - @Path("concept/{id}/descendants") - @Produces(MediaType.APPLICATION_JSON) - public Collection getDescendantConcepts(@PathParam("id") final Long id) { + public Collection getDescendantConcepts(final Long id) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getDescendantConcepts(defaultSourceKey, id); } @@ -1178,10 +1073,7 @@ public Collection getDescendantConcepts(@PathParam("id") final L * @param sourceKey The source containing the vocabulary * @return A collection of domains */ - @GET - @Path("{sourceKey}/domains") - @Produces(MediaType.APPLICATION_JSON) - public Collection getDomains(@PathParam("sourceKey") String sourceKey) { + public Collection getDomains(String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); String tableQualifier = source.getTableQualifier(SourceDaimon.DaimonType.Vocabulary); String sqlPath = "/resources/vocabulary/sql/getDomains.sql"; @@ -1206,14 +1098,11 @@ public Domain mapRow(final ResultSet resultSet, final int arg1) throws SQLExcept * @summary Get domains (default vocabulary) * @return A collection of domains */ - @GET - @Path("domains") - @Produces(MediaType.APPLICATION_JSON) public Collection getDomains() { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getDomains(defaultSourceKey); } @@ -1226,10 +1115,7 @@ public Collection getDomains() { * @param sourceKey The source containing the vocabulary * @return A collection of vocabularies */ - @GET - @Path("{sourceKey}/vocabularies") - @Produces(MediaType.APPLICATION_JSON) - public Collection getVocabularies(@PathParam("sourceKey") String sourceKey) { + public Collection getVocabularies(String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/vocabulary/sql/getVocabularies.sql"; String tableQualifier = source.getTableQualifier(SourceDaimon.DaimonType.Vocabulary); @@ -1257,14 +1143,11 @@ public Vocabulary mapRow(final ResultSet resultSet, final int arg1) throws SQLEx * @param sourceKey The source containing the vocabulary * @return A collection of vocabularies */ - @GET - @Path("vocabularies") - @Produces(MediaType.APPLICATION_JSON) public Collection getVocabularies() { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getVocabularies(defaultSourceKey); } @@ -1306,10 +1189,7 @@ private void addRelationships(final Map concepts, final Re * @param sourceKey The source containing the vocabulary * @return The vocabulary info */ - @GET - @Path("{sourceKey}/info") - @Produces(MediaType.APPLICATION_JSON) - public VocabularyInfo getInfo(@PathParam("sourceKey") String sourceKey) { + public VocabularyInfo getInfo(String sourceKey) { if (vocabularyInfoCache == null) { vocabularyInfoCache = new Hashtable<>(); } @@ -1358,11 +1238,7 @@ public void clearCaches() { * @param search The descendant of ancestor search object * @return A collection of concepts */ - @POST - @Path("{sourceKey}/descendantofancestor") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getDescendantOfAncestorConcepts(@PathParam("sourceKey") String sourceKey, DescendentOfAncestorSearch search) { + public Collection getDescendantOfAncestorConcepts(String sourceKey, DescendentOfAncestorSearch search) { Tracker.trackActivity(ActivityType.Search, "getDescendantOfAncestorConcepts"); Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1391,15 +1267,11 @@ protected PreparedStatementRenderer prepareGetDescendantOfAncestorConcepts(Desce * @param search The descendant of ancestor search object * @return A collection of concepts */ - @POST - @Path("descendantofancestor") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Collection getDescendantOfAncestorConcepts(DescendentOfAncestorSearch search) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getDescendantOfAncestorConcepts(defaultSourceKey, search); } @@ -1413,11 +1285,7 @@ public Collection getDescendantOfAncestorConcepts(DescendentOfAncestorS * @param search The concept identifiers of interest * @return A collection of concepts */ - @Path("{sourceKey}/relatedconcepts") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getRelatedConcepts(@PathParam("sourceKey") String sourceKey, RelatedConceptSearch search) { + public Collection getRelatedConcepts(String sourceKey, RelatedConceptSearch search) { Tracker.trackActivity(ActivityType.Search, "getRelatedConcepts"); Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1457,15 +1325,11 @@ protected PreparedStatementRenderer prepareGetRelatedConcepts(RelatedConceptSear * @param search The concept identifiers of interest * @return A collection of concepts */ - @Path("relatedconcepts") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Collection getRelatedConcepts(RelatedConceptSearch search) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getRelatedConcepts(defaultSourceKey, search); } @@ -1479,11 +1343,7 @@ public Collection getRelatedConcepts(RelatedConceptSearch search) { * @param conceptList The list of concept identifiers * @return A collection of concepts */ - @Path("{sourceKey}/conceptlist/descendants") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getDescendantConceptsByList(@PathParam("sourceKey") String sourceKey, String[] conceptList) { + public Collection getDescendantConceptsByList(String sourceKey, String[] conceptList) { final Map concepts = new HashMap<>(); Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetDescendantConceptsByList(conceptList, source); @@ -1505,15 +1365,11 @@ public Void mapRow(ResultSet resultSet, int arg1) throws SQLException { * @param conceptList The list of concept identifiers * @return A collection of concepts */ - @Path("conceptlist/descendants") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Collection getDescendantConceptsByList(String[] conceptList) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getDescendantConceptsByList(defaultSourceKey, conceptList); } @@ -1535,11 +1391,7 @@ protected PreparedStatementRenderer prepareGetDescendantConceptsByList(String[] * @param conceptList The list of concept identifiers * @return A collection of recommended concepts */ - @Path("{sourceKey}/lookup/recommended") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getRecommendedConceptsByList(@PathParam("sourceKey") String sourceKey, long[] conceptList) { + public Collection getRecommendedConceptsByList(String sourceKey, long[] conceptList) { if (conceptList.length == 0) { return new ArrayList(); // empty list of recommendations } @@ -1594,11 +1446,7 @@ private void addRecommended(Map concepts, ResultSet re * @param conceptSetExpressionList Expects a list of exactly 2 concept set expressions * @return A collection of concept set comparisons */ - @Path("{sourceKey}/compare") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection compareConceptSets(@PathParam("sourceKey") String sourceKey, ConceptSetExpression[] conceptSetExpressionList) throws Exception { + public Collection compareConceptSets(String sourceKey, ConceptSetExpression[] conceptSetExpressionList) throws Exception { if (conceptSetExpressionList.length != 2) { throw new Exception("You must specify two concept set expressions in order to use this method."); } @@ -1624,12 +1472,7 @@ public Collection compareConceptSets(@PathParam("sourceKey return returnVal; } - - @Path("{sourceKey}/compare-arbitrary") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection compareConceptSetsCsv(final @PathParam("sourceKey") String sourceKey, + public Collection compareConceptSetsCsv(final String sourceKey, final CompareArbitraryDto dto) throws Exception { final ConceptSetExpression[] csExpressionList = dto.compareTargets; if (csExpressionList.length != 2) { @@ -1663,15 +1506,11 @@ public Collection compareConceptSetsCsv(final @PathParam(" * @param conceptSetExpressionList Expects a list of exactly 2 concept set expressions * @return A collection of concept set comparisons */ - @Path("compare") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public Collection compareConceptSets(ConceptSetExpression[] conceptSetExpressionList) throws Exception { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return compareConceptSets(defaultSourceKey, conceptSetExpressionList); } @@ -1686,15 +1525,11 @@ public Collection compareConceptSets(ConceptSetExpression[ * @param conceptSetExpression The concept set expression to optimize * @return A concept set optimization */ - @Path("optimize") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) public ConceptSetOptimizationResult optimizeConceptSet(ConceptSetExpression conceptSetExpression) throws Exception { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) - throw new WebApplicationException(new Exception("No vocabulary or cdm daimon was found in configured sources. Search failed."), Response.Status.SERVICE_UNAVAILABLE); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return optimizeConceptSet(defaultSourceKey, conceptSetExpression); } @@ -1708,11 +1543,7 @@ public ConceptSetOptimizationResult optimizeConceptSet(ConceptSetExpression conc * @param conceptSetExpression The concept set expression to optimize * @return A concept set optimization */ - @Path("{sourceKey}/optimize") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public ConceptSetOptimizationResult optimizeConceptSet(@PathParam("sourceKey") String sourceKey, ConceptSetExpression conceptSetExpression) throws Exception { + public ConceptSetOptimizationResult optimizeConceptSet(String sourceKey, ConceptSetExpression conceptSetExpression) throws Exception { // resolve the concept set to get included concepts Collection includedConcepts = this.resolveConceptSetExpression(sourceKey, conceptSetExpression); long[] includedConceptsArray = includedConcepts.stream().mapToLong(Long::longValue).toArray(); diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/UpdateAccessTokenFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/UpdateAccessTokenFilter.java index 26773cf15c..56793084d2 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/UpdateAccessTokenFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/UpdateAccessTokenFilter.java @@ -17,7 +17,7 @@ import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; -import jakarta.ws.rs.core.UriBuilder; +import org.springframework.web.util.UriComponentsBuilder; import org.apache.commons.lang3.StringUtils; import org.ohdsi.webapi.shiro.ServletBridge; import org.apache.shiro.SecurityUtils; @@ -166,7 +166,10 @@ private URI getFailUri(String failFragment) throws URISyntaxException { } else { sbFragment.append(fragment).append("/").append(failFragment).append("/"); } - return UriBuilder.fromUri(oauthFailURI).fragment(sbFragment.toString()).build(); + return UriComponentsBuilder.fromUri(oauthFailURI) + .fragment(sbFragment.toString()) + .build() + .toUri(); } private Date getExpirationDate(final int expirationIntervalInSeconds) { diff --git a/src/main/java/org/ohdsi/webapi/shiro/management/datasource/BaseDataSourceAccessor.java b/src/main/java/org/ohdsi/webapi/shiro/management/datasource/BaseDataSourceAccessor.java index 88613db83a..e0be994429 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/management/datasource/BaseDataSourceAccessor.java +++ b/src/main/java/org/ohdsi/webapi/shiro/management/datasource/BaseDataSourceAccessor.java @@ -6,7 +6,8 @@ import org.ohdsi.webapi.source.Source; import org.springframework.beans.factory.annotation.Autowired; -import jakarta.ws.rs.ForbiddenException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; public abstract class BaseDataSourceAccessor implements DataSourceAccessor { @@ -15,7 +16,7 @@ public abstract class BaseDataSourceAccessor implements DataSourceAccessor public void checkAccess(T s) { if (!hasAccess(s)) { - throw new ForbiddenException(); + throw new ResponseStatusException(HttpStatus.FORBIDDEN); } } diff --git a/src/main/java/org/ohdsi/webapi/source/SourceController.java b/src/main/java/org/ohdsi/webapi/source/SourceController.java deleted file mode 100644 index 5617382747..0000000000 --- a/src/main/java/org/ohdsi/webapi/source/SourceController.java +++ /dev/null @@ -1,415 +0,0 @@ -package org.ohdsi.webapi.source; - -import org.ohdsi.webapi.common.DBMSType; -import org.ohdsi.webapi.arachne.logging.event.AddDataSourceEvent; -import org.ohdsi.webapi.arachne.logging.event.ChangeDataSourceEvent; -import org.ohdsi.webapi.arachne.logging.event.DeleteDataSourceEvent; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.ohdsi.webapi.exception.SourceDuplicateKeyException; -import org.ohdsi.webapi.service.AbstractDaoService; -import org.ohdsi.webapi.service.VocabularyService; -import org.ohdsi.webapi.shiro.management.Security; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import jakarta.persistence.PersistenceException; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import java.io.IOException; -import java.io.InputStream; -import java.util.*; -import java.util.stream.Collectors; -import org.springframework.cache.annotation.CacheEvict; - -@Path("/source/") -@Component -@Transactional -public class SourceController extends AbstractDaoService { - - public static final String SECURE_MODE_ERROR = "This feature requires the administrator to enable security for the application"; - - @Autowired - private ApplicationEventPublisher publisher; - - @Autowired - private VocabularyService vocabularyService; - - @Autowired - private SourceService sourceService; - - @Autowired - private SourceRepository sourceRepository; - - @Autowired - private SourceDaimonRepository sourceDaimonRepository; - - @Autowired - private GenericConversionService conversionService; - - @Autowired - private Security securityManager; - - @Value("#{!'${security.provider}'.equals('DisabledSecurity')}") - private boolean securityEnabled; - - /** - * Gets the list of all Sources in WebAPI database. Sources with a non-null - * deleted_date are not returned (ie: these are soft deleted) - * - * @summary Get Sources - * @return A list of all CDM sources with the ID, name, SQL dialect, and key - * for each source. The {sourceKey} is used in other WebAPI endpoints to - * identify CDMs. - */ - @Path("sources") - @GET - @Produces(MediaType.APPLICATION_JSON) - public Collection getSources() { - - return sourceService.getSources().stream().map(SourceInfo::new).collect(Collectors.toList()); - } - - /** - * Refresh cached CDM database metadata - * - * @summary Refresh Sources - * @return A list of all CDM sources with the ID, name, SQL dialect, and key - * for each source (same as the 'sources' endpoint) after refreshing the cached sourced data. - */ - @Path("refresh") - @GET - @Produces(MediaType.APPLICATION_JSON) - public Collection refreshSources() { - sourceService.invalidateCache(); - vocabularyService.clearVocabularyInfoCache(); - sourceService.ensureSourceEncrypted(); - return getSources(); - } -/** - * Get the priority vocabulary source. - * - * WebAPI designates one CDM vocabulary as the priority vocabulary to be used for vocabulary searches in Atlas. - * - * @summary Get Priority Vocabulary Source - * @return The CDM metadata for the priority vocabulary. - */ - @Path("priorityVocabulary") - @GET - @Produces(MediaType.APPLICATION_JSON) - public SourceInfo getPriorityVocabularySourceInfo() { - return sourceService.getPriorityVocabularySourceInfo(); - } - -/** - * Get source by key - * @summary Get Source By Key - * @param sourceKey - * @return Metadata for a single Source that matches the sourceKey. - */ - @Path("{key}") - @GET - @Produces(MediaType.APPLICATION_JSON) - public SourceInfo getSource(@PathParam("key") final String sourceKey) { - return sourceRepository.findBySourceKey(sourceKey).getSourceInfo(); - } - - /** - * Get Source Details - * - * Source Details contains connection-specific information like JDBC url and authentication information. - - * @summary Get Source Details - * @param sourceId - * @return - */ - @Path("details/{sourceId}") - @GET - @Produces(MediaType.APPLICATION_JSON) - public SourceDetails getSourceDetails(@PathParam("sourceId") Integer sourceId) { - if (!securityEnabled) { - throw new NotAuthorizedException(SECURE_MODE_ERROR); - } - Source source = sourceRepository.findBySourceId(sourceId); - return new SourceDetails(source); - } - - /** - * Create a Source - * - * @summary Create Source - * @param file the keyfile - * @param fileDetail the keyfile details - * @param request contains the source information (name, key, etc) - * @return a new SourceInfo for the created source - * @throws Exception - */ - @Path("") - @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); - } - Source sourceByKey = sourceRepository.findBySourceKey(request.getKey()); - if (Objects.nonNull(sourceByKey)) { - throw new SourceDuplicateKeyException("The source key has been already used."); - } - Source source = conversionService.convert(request, Source.class); - if(source.getDaimons() != null) { - // First source should get priority = 1 - Iterable sources = sourceRepository.findAll(); - source.getDaimons() - .stream() - .filter(sd -> sd.getPriority() <= 0) - .filter(sd -> { - boolean accept = true; - // Check if source daimon of given type with priority > 0 already exists in other sources - for(Source innerSource: sources) { - accept = !innerSource.getDaimons() - .stream() - .anyMatch(innerDaimon -> innerDaimon.getPriority() > 0 - && innerDaimon.getDaimonType().equals(sd.getDaimonType())); - if(!accept) { - break; - } - } - return accept; - }) - .forEach(sd -> sd.setPriority(1)); - } - Source original = new Source(); - original.setSourceDialect(source.getSourceDialect()); - setKeyfileData(source, original, file); - source.setCreatedBy(getCurrentUser()); - source.setCreatedDate(new Date()); - try { - Source saved = sourceRepository.saveAndFlush(source); - sourceService.invalidateCache(); - SourceInfo sourceInfo = new SourceInfo(saved); - publisher.publishEvent(new AddDataSourceEvent(this, source.getSourceId(), source.getSourceName())); - return sourceInfo; - } catch (PersistenceException ex) { - throw new SourceDuplicateKeyException("You cannot use this Source Key, please use different one"); - } - } - - /** - * Updates a Source with the provided details from multiple files - * - * @summary Update Source - * @param file the keyfile - * @param fileDetail the keyfile details - * @param request contains the source information (name, key, etc) - * @return the updated SourceInfo for the source - * @throws Exception - */ - @Path("{sourceId}") - @PUT - @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); - } - Source updated = conversionService.convert(request, Source.class); - Source source = sourceRepository.findBySourceId(sourceId); - if (source != null) { - updated.setSourceId(sourceId); - updated.setSourceKey(source.getSourceKey()); - if (StringUtils.isBlank(updated.getUsername()) || - Objects.equals(updated.getUsername().trim(), Source.MASQUERADED_USERNAME)) { - updated.setUsername(source.getUsername()); - } - if (StringUtils.isBlank(updated.getPassword()) || - Objects.equals(updated.getPassword().trim(), Source.MASQUERADED_PASSWORD)) { - updated.setPassword(source.getPassword()); - } - setKeyfileData(updated, source, file); - transformIfRequired(updated); - if (request.isCheckConnection() == null) { - updated.setCheckConnection(source.isCheckConnection()); - } - updated.setModifiedBy(getCurrentUser()); - updated.setModifiedDate(new Date()); - - reuseDeletedDaimons(updated, source); - - List removed = source.getDaimons().stream().filter(d -> !updated.getDaimons().contains(d)) - .collect(Collectors.toList()); - // Delete MUST be called after fetching user or source data to prevent autoflush (see DefaultPersistEventListener.onPersist) - sourceDaimonRepository.deleteAll(removed); - Source result = sourceRepository.save(updated); - publisher.publishEvent(new ChangeDataSourceEvent(this, updated.getSourceId(), updated.getSourceName())); - sourceService.invalidateCache(); - return new SourceInfo(result); - } else { - throw new NotFoundException(); - } - } - - private void reuseDeletedDaimons(Source updated, Source source) { - List daimons = updated.getDaimons().stream().filter(d -> source.getDaimons().contains(d)) - .collect(Collectors.toList()); - List newDaimons = updated.getDaimons().stream().filter(d -> !source.getDaimons().contains(d)) - .collect(Collectors.toList()); - - List allDaimons = sourceDaimonRepository.findBySource(source); - - for (SourceDaimon newSourceDaimon: newDaimons) { - Optional reusedDaimonOpt = allDaimons.stream() - .filter(d -> d.equals(newSourceDaimon)) - .findFirst(); - if (reusedDaimonOpt.isPresent()) { - SourceDaimon reusedDaimon = reusedDaimonOpt.get(); - reusedDaimon.setPriority(newSourceDaimon.getPriority()); - reusedDaimon.setTableQualifier(newSourceDaimon.getTableQualifier()); - daimons.add(reusedDaimon); - } else { - daimons.add(newSourceDaimon); - } - } - updated.setDaimons(daimons); - } - - private void transformIfRequired(Source source) { - - if (DBMSType.BIGQUERY.getOhdsiDB().equals(source.getSourceDialect()) && ArrayUtils.isNotEmpty(source.getKeyfile())) { - String connStr = source.getSourceConnection().replaceAll("OAuthPvtKeyPath=.+?(;|\\z)", ""); - source.setSourceConnection(connStr); - } - } - - private void setKeyfileData(Source updated, Source source, InputStream file) throws IOException { - if (source.supportsKeyfile()) { - if (updated.getKeyfileName() != null) { - if (!Objects.equals(updated.getKeyfileName(), source.getKeyfileName())) { - byte[] fileBytes = IOUtils.toByteArray(file); - updated.setKeyfile(fileBytes); - } else { - updated.setKeyfile(source.getKeyfile()); - } - return; - } - } - updated.setKeyfile(null); - updated.setKeyfileName(null); - } - - /** - * Delete a source. - * - * @summary Delete Source - * @param sourceId - * @return - * @throws Exception - */ - @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(); - } - Source source = sourceRepository.findBySourceId(sourceId); - if (source != null) { - sourceRepository.delete(source); - publisher.publishEvent(new DeleteDataSourceEvent(this, sourceId, source.getSourceName())); - sourceService.invalidateCache(); - return Response.ok().build(); - } else { - throw new NotFoundException(); - } - } - - /** - * Check source connection. - * - * This method attempts to connect to the source by calling 'select 1' on the source connection. - * @summary Check connection - * @param sourceKey - * @return - */ - @Path("connection/{key}") - @GET - @Produces(MediaType.APPLICATION_JSON) - @Transactional(noRollbackFor = CannotGetJdbcConnectionException.class) - public SourceInfo checkConnection(@PathParam("key") final String sourceKey) { - - final Source source = sourceService.findBySourceKey(sourceKey); - sourceService.checkConnection(source); - return source.getSourceInfo(); - } - - /** - * Get the first daimon (ad associated source) that has priority. In the event - * of a tie, the first source searched wins. - * - * @summary Get Priority Daimons - * @return - */ - @Path("daimon/priority") - @GET - @Produces(MediaType.APPLICATION_JSON) - public Map getPriorityDaimons() { - - return sourceService.getPriorityDaimons() - .entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> new SourceInfo(e.getValue()) - )); - } - - /** - * Set priority of daimon - * - * Set the priority of the specified daimon of the specified source, and set the other daimons to 0. - * @summary Set Priority - * @param sourceKey - * @param daimonTypeName - * @return - */ - @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 - ) { - if (!securityEnabled) { - return getInsecureModeResponse(); - } - SourceDaimon.DaimonType daimonType = SourceDaimon.DaimonType.valueOf(daimonTypeName); - List daimonList = sourceDaimonRepository.findByDaimonType(daimonType); - daimonList.forEach(daimon -> { - Integer newPriority = daimon.getSource().getSourceKey().equals(sourceKey) ? 1 : 0; - daimon.setPriority(newPriority); - sourceDaimonRepository.save(daimon); - }); - sourceService.invalidateCache(); - return Response.ok().build(); - } - - private Response getInsecureModeResponse() { - return Response.status(Response.Status.UNAUTHORIZED) - .entity(SECURE_MODE_ERROR) - .build(); - } - -} \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticController.java b/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticController.java deleted file mode 100644 index 82640009cb..0000000000 --- a/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticController.java +++ /dev/null @@ -1,261 +0,0 @@ -package org.ohdsi.webapi.statistic.controller; - -import com.opencsv.CSVWriter; - -import org.ohdsi.webapi.statistic.dto.AccessTrendDto; -import org.ohdsi.webapi.statistic.dto.AccessTrendsDto; -import org.ohdsi.webapi.statistic.dto.EndpointDto; -import org.ohdsi.webapi.statistic.dto.SourceExecutionDto; -import org.ohdsi.webapi.statistic.dto.SourceExecutionsDto; -import org.ohdsi.webapi.statistic.service.StatisticService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Controller; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.InternalServerErrorException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import java.io.ByteArrayOutputStream; -import java.io.StringWriter; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -@Controller -@Path("/statistic/") -public class StatisticController { - - private static final Logger log = LoggerFactory.getLogger(StatisticController.class); - - private StatisticService service; - - @Value("${audit.trail.enabled}") - private boolean auditTrailEnabled; - - public enum ResponseFormat { - CSV, JSON - } - - private static final List EXECUTION_STATISTICS_CSV_RESULT_HEADER = new ArrayList() {{ - add(new String[]{"Date", "Source", "Execution Type"}); - }}; - - private static final List EXECUTION_STATISTICS_CSV_RESULT_HEADER_WITH_USER_ID = new ArrayList() {{ - add(new String[]{"Date", "Source", "Execution Type", "User ID"}); - }}; - - private static final List ACCESS_TRENDS_CSV_RESULT_HEADER = new ArrayList() {{ - add(new String[]{"Date", "Endpoint"}); - }}; - - private static final List ACCESS_TRENDS_CSV_RESULT_HEADER_WITH_USER_ID = new ArrayList() {{ - add(new String[]{"Date", "Endpoint", "User ID"}); - }}; - - @Autowired - public StatisticController(StatisticService service) { - this.service = service; - } - - /** - * Returns execution statistics - * @param executionStatisticsRequest - filter settings for statistics - */ - @POST - @Path("/executions") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Response executionStatistics(ExecutionStatisticsRequest executionStatisticsRequest) { - if (!auditTrailEnabled) { - throw new InternalServerErrorException("Audit Trail functionality should be enabled (audit.trail.enabled) to serve this endpoint"); - } - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - boolean showUserInformation = executionStatisticsRequest.isShowUserInformation(); - - SourceExecutionsDto sourceExecutions = service.getSourceExecutions(LocalDate.parse(executionStatisticsRequest.getStartDate(), formatter), - LocalDate.parse(executionStatisticsRequest.getEndDate(), formatter), executionStatisticsRequest.getSourceKey(), showUserInformation); - - if (ResponseFormat.CSV.equals(executionStatisticsRequest.getResponseFormat())) { - return prepareExecutionResultResponse(sourceExecutions.getExecutions(), "execution_statistics.zip", showUserInformation); - } else { - return Response.ok(sourceExecutions).build(); - } - } - - /** - * Returns access trends statistics - * @param accessTrendsStatisticsRequest - filter settings for statistics - */ - @POST - @Path("/accesstrends") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Response accessStatistics(AccessTrendsStatisticsRequest accessTrendsStatisticsRequest) { - if (!auditTrailEnabled) { - throw new InternalServerErrorException("Audit Trail functionality should be enabled (audit.trail.enabled) to serve this endpoint"); - } - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - boolean showUserInformation = accessTrendsStatisticsRequest.isShowUserInformation(); - - AccessTrendsDto trends = service.getAccessTrends(LocalDate.parse(accessTrendsStatisticsRequest.getStartDate(), formatter), - LocalDate.parse(accessTrendsStatisticsRequest.getEndDate(), formatter), accessTrendsStatisticsRequest.getEndpoints(), showUserInformation); - - if (ResponseFormat.CSV.equals(accessTrendsStatisticsRequest.getResponseFormat())) { - return prepareAccessTrendsResponse(trends.getTrends(), "execution_trends.zip", showUserInformation); - } else { - return Response.ok(trends).build(); - } - } - - private Response prepareExecutionResultResponse(List executions, String filename, boolean showUserInformation) { - List data = executions.stream() - .map(execution -> showUserInformation - ? new String[]{execution.getExecutionDate(), execution.getSourceName(), execution.getExecutionName(), execution.getUserId()} - : new String[]{execution.getExecutionDate(), execution.getSourceName(), execution.getExecutionName()} - ) - .collect(Collectors.toList()); - return prepareResponse(data, filename, showUserInformation ? EXECUTION_STATISTICS_CSV_RESULT_HEADER_WITH_USER_ID : EXECUTION_STATISTICS_CSV_RESULT_HEADER); - } - - private Response prepareAccessTrendsResponse(List trends, String filename, boolean showUserInformation) { - List data = trends.stream() - .map(trend -> showUserInformation - ? new String[]{trend.getExecutionDate().toString(), trend.getEndpointName(), trend.getUserID()} - : new String[]{trend.getExecutionDate().toString(), trend.getEndpointName()} - ) - .collect(Collectors.toList()); - return prepareResponse(data, filename, showUserInformation ? ACCESS_TRENDS_CSV_RESULT_HEADER_WITH_USER_ID : ACCESS_TRENDS_CSV_RESULT_HEADER); - } - - private Response prepareResponse(List data, String filename, List header) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - StringWriter sw = new StringWriter(); - CSVWriter csvWriter = new CSVWriter(sw, ',', CSVWriter.DEFAULT_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER)) { - - csvWriter.writeAll(header); - csvWriter.writeAll(data); - csvWriter.flush(); - baos.write(sw.getBuffer().toString().getBytes()); - - return Response - .ok(baos) - .type(MediaType.APPLICATION_OCTET_STREAM) - .header("Content-Disposition", String.format("attachment; filename=\"%s\"", filename)) - .build(); - } catch (Exception ex) { - log.error("An error occurred while building a response"); - throw new RuntimeException(ex); - } - } - - public static final class ExecutionStatisticsRequest { - // Format - yyyy-MM-dd - String startDate; - // Format - yyyy-MM-dd - String endDate; - String sourceKey; - ResponseFormat responseFormat; - boolean showUserInformation; - - public String getStartDate() { - return startDate; - } - - public void setStartDate(String startDate) { - this.startDate = startDate; - } - - public String getEndDate() { - return endDate; - } - - public void setEndDate(String endDate) { - this.endDate = endDate; - } - - public String getSourceKey() { - return sourceKey; - } - - public void setSourceKey(String sourceKey) { - this.sourceKey = sourceKey; - } - - public ResponseFormat getResponseFormat() { - return responseFormat; - } - - public void setResponseFormat(ResponseFormat responseFormat) { - this.responseFormat = responseFormat; - } - - public boolean isShowUserInformation() { - return showUserInformation; - } - - public void setShowUserInformation(boolean showUserInformation) { - this.showUserInformation = showUserInformation; - } - } - - public static final class AccessTrendsStatisticsRequest { - // Format - yyyy-MM-dd - String startDate; - // Format - yyyy-MM-dd - String endDate; - // Key - method (POST, GET) - // Value - endpoint ("{}" can be used as a placeholder, will be converted to ".*" in regular expression) - List endpoints; - ResponseFormat responseFormat; - boolean showUserInformation; - - public String getStartDate() { - return startDate; - } - - public void setStartDate(String startDate) { - this.startDate = startDate; - } - - public String getEndDate() { - return endDate; - } - - public void setEndDate(String endDate) { - this.endDate = endDate; - } - - public List getEndpoints() { - return endpoints; - } - - public void setEndpoints(List endpoints) { - this.endpoints = endpoints; - } - - public ResponseFormat getResponseFormat() { - return responseFormat; - } - - public void setResponseFormat(ResponseFormat responseFormat) { - this.responseFormat = responseFormat; - } - - public boolean isShowUserInformation() { - return showUserInformation; - } - - public void setShowUserInformation(boolean showUserInformation) { - this.showUserInformation = showUserInformation; - } - } -} diff --git a/src/main/java/org/ohdsi/webapi/tag/TagController.java b/src/main/java/org/ohdsi/webapi/tag/TagController.java deleted file mode 100644 index 9d4f8c3e84..0000000000 --- a/src/main/java/org/ohdsi/webapi/tag/TagController.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.ohdsi.webapi.tag; - -import org.apache.commons.lang3.StringUtils; -import org.ohdsi.webapi.tag.dto.TagDTO; -import org.ohdsi.webapi.tag.dto.TagGroupSubscriptionDTO; -import org.ohdsi.webapi.tag.dto.AssignmentPermissionsDTO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import java.util.Collections; -import java.util.List; - -@Path("/tag") -@Controller -public class TagController { - private final TagService tagService; - private final TagGroupService tagGroupService; - - @Autowired - public TagController(TagService pathwayService, - TagGroupService tagGroupService) { - this.tagService = pathwayService; - this.tagGroupService = tagGroupService; - } - - /** - * Creates a tag. - * - * @param dto - * @return - */ - @POST - @Path("/") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public TagDTO create(final TagDTO dto) { - return tagService.create(dto); - } - - /** - * Returns list of tags, which names contain a provided substring. - * - * @summary Search tags by name part - * @param namePart - * @return - */ - @GET - @Path("/search") - @Produces(MediaType.APPLICATION_JSON) - public List search(@QueryParam("namePart") String namePart) { - if (StringUtils.isBlank(namePart)) { - return Collections.emptyList(); - } - return tagService.listInfoDTO(namePart); - } - - /** - * Returns list of all tags. - * - * @return - */ - @GET - @Path("/") - @Produces(MediaType.APPLICATION_JSON) - public List list() { - return tagService.listInfoDTO(); - } - - /** - * Updates tag with ID={id}. - * - * @param id - * @param dto - * @return - */ - @PUT - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public TagDTO update(@PathParam("id") final Integer id, final TagDTO dto) { - return tagService.update(id, dto); - } - - /** - * Return tag by ID. - * - * @param id - * @return - */ - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public TagDTO get(@PathParam("id") final Integer id) { - return tagService.getDTOById(id); - } - - /** - * Deletes tag with ID={id}. - * - * @param id - */ - @DELETE - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public void delete(@PathParam("id") final Integer id) { - tagService.delete(id); - } - - /** - * Assignes group of tags to groups of assets. - * - * @param dto - * @return - */ - @POST - @Path("/multiAssign") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public void assignGroup(final TagGroupSubscriptionDTO dto) { - tagGroupService.assignGroup(dto); - } - - /** - * Unassignes group of tags from groups of assets. - * - * @param dto - * @return - */ - @POST - @Path("/multiUnassign") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public void unassignGroup(final TagGroupSubscriptionDTO dto) { - tagGroupService.unassignGroup(dto); - } - - /** - * Tags assignment permissions for current user - * - * @return - */ - @GET - @Path("/assignmentPermissions") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public AssignmentPermissionsDTO assignmentPermissions() { - return tagService.getAssignmentPermissions(); - } -} diff --git a/src/main/java/org/ohdsi/webapi/tag/TagGroupService.java b/src/main/java/org/ohdsi/webapi/tag/TagGroupService.java index dbc42ac52d..1fe204eb87 100644 --- a/src/main/java/org/ohdsi/webapi/tag/TagGroupService.java +++ b/src/main/java/org/ohdsi/webapi/tag/TagGroupService.java @@ -9,7 +9,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import jakarta.ws.rs.ForbiddenException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; import java.util.ArrayList; import java.util.List; @@ -56,7 +57,7 @@ private void assignGroup(HasTags service, List assetIds assetIds.forEach(id -> { try { service.assignTag(id, tagId); - } catch (final ForbiddenException e) { + } catch (final ResponseStatusException e) { log.warn("Tag {} cannot be assigned to entity {} in service {} - forbidden", tagId, id, service.getClass().getName()); throw e; } @@ -67,7 +68,7 @@ private void unassignGroup(HasTags service, List assetI assetIds.forEach(id -> { try { service.unassignTag(id, tagId); - } catch(final ForbiddenException e) { + } catch(final ResponseStatusException e) { log.warn("Tag {} cannot be unassigned from entity {} in service {} - forbidden", tagId, id, service.getClass().getName()); } }); diff --git a/src/main/java/org/ohdsi/webapi/tag/TagSecurityUtils.java b/src/main/java/org/ohdsi/webapi/tag/TagSecurityUtils.java index b7ba8308d7..502e9fdcc9 100644 --- a/src/main/java/org/ohdsi/webapi/tag/TagSecurityUtils.java +++ b/src/main/java/org/ohdsi/webapi/tag/TagSecurityUtils.java @@ -6,7 +6,8 @@ import org.ohdsi.webapi.model.CommonEntityExt; import org.ohdsi.webapi.reusable.domain.Reusable; -import jakarta.ws.rs.BadRequestException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; public class TagSecurityUtils { public static String COHORT_DEFINITION = "cohortdefinition"; @@ -39,7 +40,7 @@ public static boolean checkPermission(final String asset, final String method) { template = "%s:*:protectedtag:*:delete"; break; default: - throw new BadRequestException(String.format("Unsupported method: %s", method)); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, String.format("Unsupported method: %s", method)); } final String permission = String.format(template, asset); diff --git a/src/main/java/org/ohdsi/webapi/tag/TagService.java b/src/main/java/org/ohdsi/webapi/tag/TagService.java index 643d537d24..e786a9bacc 100644 --- a/src/main/java/org/ohdsi/webapi/tag/TagService.java +++ b/src/main/java/org/ohdsi/webapi/tag/TagService.java @@ -1,7 +1,6 @@ package org.ohdsi.webapi.tag; import org.apache.shiro.SecurityUtils; -import org.glassfish.jersey.internal.util.Producer; import org.ohdsi.webapi.service.AbstractDaoService; import org.ohdsi.webapi.tag.domain.Tag; import org.ohdsi.webapi.tag.domain.TagInfo; @@ -20,6 +19,7 @@ import jakarta.persistence.EntityManager; import java.util.*; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.Optional; @@ -31,7 +31,7 @@ public class TagService extends AbstractDaoService { private final EntityManager entityManager; private final ConversionService conversionService; - private final ArrayList>> infoProducers; + private final ArrayList>> infoProducers; @Autowired public TagService( @@ -171,9 +171,9 @@ public void refreshTagStatistics() { logger.info("Finishing tags statistics refreshing"); } - private void processTagInfo(Producer> infoProducer, + private void processTagInfo(Supplier> infoProducer, Map infoMap) { - List tagInfos = infoProducer.call(); + List tagInfos = infoProducer.get(); tagInfos.forEach(info -> { int id = info.getId(); TagDTO dto = infoMap.get(id); diff --git a/src/main/java/org/ohdsi/webapi/tool/ToolController.java b/src/main/java/org/ohdsi/webapi/tool/ToolController.java deleted file mode 100644 index 34b2449b0c..0000000000 --- a/src/main/java/org/ohdsi/webapi/tool/ToolController.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.ohdsi.webapi.tool; - -import org.ohdsi.webapi.tool.dto.ToolDTO; -import org.springframework.stereotype.Controller; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import java.util.List; - -@Controller -@Path("/tool") -public class ToolController { - private final ToolServiceImpl service; - - public ToolController(ToolServiceImpl service) { - this.service = service; - } - - @GET - @Path("") - @Produces(MediaType.APPLICATION_JSON) - public List getTools() { - return service.getTools(); - } - - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - public ToolDTO getToolById(@PathParam("id") Integer id) { - return service.getById(id); - } - - @POST - @Path("") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public ToolDTO createTool(ToolDTO dto) { - return service.saveTool(dto); - } - - @DELETE - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - public void delete(@PathParam("id") Integer id) { - service.delete(id); - } - - @PUT - @Path("") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public ToolDTO updateTool(ToolDTO toolDTO) { - return service.saveTool(toolDTO); - } -} diff --git a/src/main/java/org/ohdsi/webapi/user/importer/UserImportController.java b/src/main/java/org/ohdsi/webapi/user/importer/UserImportController.java deleted file mode 100644 index cc4653228e..0000000000 --- a/src/main/java/org/ohdsi/webapi/user/importer/UserImportController.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.ohdsi.webapi.user.importer; - -import org.ohdsi.webapi.arachne.scheduler.model.JobExecutingType; -import org.ohdsi.analysis.Utils; -import org.ohdsi.webapi.user.importer.converter.RoleGroupMappingConverter; -import org.ohdsi.webapi.user.importer.dto.UserImportJobDTO; -import org.ohdsi.webapi.user.importer.exception.JobAlreadyExistException; -import org.ohdsi.webapi.user.importer.model.AtlasUserRoles; -import org.ohdsi.webapi.user.importer.model.AuthenticationProviders; -import org.ohdsi.webapi.user.importer.model.ConnectionInfo; -import org.ohdsi.webapi.user.importer.model.LdapGroup; -import org.ohdsi.webapi.user.importer.model.LdapProviderType; -import org.ohdsi.webapi.user.importer.model.RoleGroupEntity; -import org.ohdsi.webapi.user.importer.model.RoleGroupMapping; -import org.ohdsi.webapi.user.importer.model.UserImportJob; -import org.ohdsi.webapi.user.importer.service.UserImportJobService; -import org.ohdsi.webapi.user.importer.service.UserImportService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.stereotype.Controller; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.NotAcceptableException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; - -import static org.ohdsi.webapi.Constants.JOB_IS_ALREADY_SCHEDULED; - -@Controller -@Path("/") -public class UserImportController { - - private static final Logger logger = LoggerFactory.getLogger(UserImportController.class); - - @Autowired - private UserImportService userImportService; - - @Autowired - private UserImportJobService userImportJobService; - - @Autowired - private GenericConversionService conversionService; - - @Value("${security.ad.url}") - private String adUrl; - - @Value("${security.ldap.url}") - private String ldapUrl; - - @GET - @Path("user/providers") - @Produces(MediaType.APPLICATION_JSON) - public AuthenticationProviders getAuthenticationProviders() { - AuthenticationProviders providers = new AuthenticationProviders(); - providers.setAdUrl(adUrl); - providers.setLdapUrl(ldapUrl); - return providers; - } - - @GET - @Path("user/import/{type}/test") - @Produces(MediaType.APPLICATION_JSON) - public Response testConnection(@PathParam("type") String type) { - LdapProviderType provider = LdapProviderType.fromValue(type); - ConnectionInfo result = new ConnectionInfo(); - userImportService.testConnection(provider); - result.setState(ConnectionInfo.ConnectionState.SUCCESS); - result.setMessage("Connection success"); - return Response.ok().entity(result).build(); - } - - @GET - @Path("user/import/{type}/groups") - @Produces(MediaType.APPLICATION_JSON) - public List findGroups(@PathParam("type") String type, @QueryParam("search") String searchStr) { - LdapProviderType provider = LdapProviderType.fromValue(type); - return userImportService.findGroups(provider, searchStr); - } - - @POST - @Path("user/import/{type}") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public List findDirectoryUsers(@PathParam("type") String type, RoleGroupMapping mapping){ - LdapProviderType provider = LdapProviderType.fromValue(type); - return userImportService.findUsers(provider, mapping); - } - - @POST - @Path("user/import") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public UserImportJobDTO importUsers(List users, - @QueryParam("provider") String provider, - @DefaultValue("TRUE") @QueryParam("preserve") Boolean preserveRoles) { - LdapProviderType providerType = LdapProviderType.fromValue(provider); - - UserImportJobDTO jobDto = new UserImportJobDTO(); - jobDto.setProviderType(providerType); - jobDto.setPreserveRoles(preserveRoles); - jobDto.setEnabled(true); - jobDto.setStartDate(getJobStartDate()); - jobDto.setFrequency(JobExecutingType.ONCE); - jobDto.setRecurringTimes(0); - if (users != null) { - jobDto.setUserRoles(Utils.serialize(users)); - } - - try { - UserImportJob job = conversionService.convert(jobDto, UserImportJob.class); - UserImportJob created = userImportJobService.createJob(job); - return conversionService.convert(created, UserImportJobDTO.class); - } catch (JobAlreadyExistException e) { - throw new NotAcceptableException(String.format(JOB_IS_ALREADY_SCHEDULED, jobDto.getProviderType())); - } - } - - @POST - @Path("user/import/{type}/mapping") - @Consumes(MediaType.APPLICATION_JSON) - public Response saveMapping(@PathParam("type") String type, RoleGroupMapping mapping) { - LdapProviderType providerType = LdapProviderType.fromValue(type); - List mappingEntities = RoleGroupMappingConverter.convertRoleGroupMapping(mapping); - userImportService.saveRoleGroupMapping(providerType, mappingEntities); - return Response.ok().build(); - } - - @GET - @Path("user/import/{type}/mapping") - @Produces(MediaType.APPLICATION_JSON) - public RoleGroupMapping getMapping(@PathParam("type") String type) { - LdapProviderType providerType = LdapProviderType.fromValue(type); - List mappingEntities = userImportService.getRoleGroupMapping(providerType); - return RoleGroupMappingConverter.convertRoleGroupMapping(type, mappingEntities); - } - - private Date getJobStartDate() { - Calendar calendar = GregorianCalendar.getInstance(); - // Job will be started in five seconds after now - calendar.add(Calendar.SECOND, 5); - calendar.set(Calendar.MILLISECOND, 0); - - return calendar.getTime(); - } -} diff --git a/src/main/java/org/ohdsi/webapi/user/importer/UserImportJobController.java b/src/main/java/org/ohdsi/webapi/user/importer/UserImportJobController.java deleted file mode 100644 index 4c6c02f2a5..0000000000 --- a/src/main/java/org/ohdsi/webapi/user/importer/UserImportJobController.java +++ /dev/null @@ -1,163 +0,0 @@ -package org.ohdsi.webapi.user.importer; - -import org.ohdsi.webapi.arachne.scheduler.exception.JobNotFoundException; -import org.ohdsi.webapi.user.importer.dto.JobHistoryItemDTO; -import org.ohdsi.webapi.user.importer.dto.UserImportJobDTO; -import org.ohdsi.webapi.user.importer.exception.JobAlreadyExistException; -import org.ohdsi.webapi.user.importer.model.UserImportJob; -import org.ohdsi.webapi.user.importer.service.UserImportJobService; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.RestController; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.NotAcceptableException; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import java.util.List; -import java.util.stream.Collectors; - -import static org.ohdsi.webapi.Constants.JOB_IS_ALREADY_SCHEDULED; - -/** - * REST Services related to importing user information - * from an external source (i.e. Active Directory) - * - * @summary User Import - */ -@RestController -@Path("/user/import/job") -@Transactional -public class UserImportJobController { - private final UserImportJobService jobService; - private final GenericConversionService conversionService; - - public UserImportJobController(UserImportJobService jobService, @Qualifier("conversionService") GenericConversionService conversionService) { - - this.jobService = jobService; - this.conversionService = conversionService; - } - - /** - * Create a user import job - * - * @summary Create user import job - * @param jobDTO The user import information - * @return The job information - */ - @POST - @Path("/") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public UserImportJobDTO createJob(UserImportJobDTO jobDTO) { - - UserImportJob job = conversionService.convert(jobDTO, UserImportJob.class); - try { - UserImportJob created = jobService.createJob(job); - return conversionService.convert(created, UserImportJobDTO.class); - } catch(JobAlreadyExistException e) { - throw new NotAcceptableException(String.format(JOB_IS_ALREADY_SCHEDULED, job.getProviderType())); - } - } - - /** - * Update a user import job - * - * @summary Update user import job - * @param jobId The job ID - * @param jobDTO The user import information - * @return The job information - */ - @PUT - @Path("/{id}") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public UserImportJobDTO updateJob(@PathParam("id") Long jobId, UserImportJobDTO jobDTO) { - - UserImportJob job = conversionService.convert(jobDTO, UserImportJob.class); - try { - job.setId(jobId); - UserImportJob updated = jobService.updateJob(job); - return conversionService.convert(updated, UserImportJobDTO.class); - } catch (JobNotFoundException e) { - throw new NotFoundException(); - } - } - - /** - * Get the user import job list - * - * @summary Get user import jobs - * @return The list of user import jobs - */ - @GET - @Path("/") - @Produces(MediaType.APPLICATION_JSON) - @Transactional - public List listJobs() { - - return jobService.getJobs().stream() - .map(job -> conversionService.convert(job, UserImportJobDTO.class)) - .peek(job -> jobService.getLatestHistoryItem(job.getId()) - .ifPresent(item -> job.setLastExecuted(item.getEndTime()))) - .collect(Collectors.toList()); - } - - /** - * Get user import job by ID - * - * @summary Get user import job by ID - * @param id The job ID - * @return The user import job - */ - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - public UserImportJobDTO getJob(@PathParam("id") Long id) { - - return jobService.getJob(id).map(job -> conversionService.convert(job, UserImportJobDTO.class)) - .orElseThrow(NotFoundException::new); - } - - /** - * Delete user import job by ID - * - * @summary Delete user import job by ID - * @param id The job ID - * @return The user import job - */ - @DELETE - @Path("/{id}") - public Response deleteJob(@PathParam("id") Long id) { - UserImportJob job = jobService.getJob(id).orElseThrow(NotFoundException::new); - jobService.delete(job); - return Response.ok().build(); - } - - /** - * Get the user import job history - * - * @summary Get import history - * @param id The job ID - * @return The job history - */ - @GET - @Path("/{id}/history") - @Produces(MediaType.APPLICATION_JSON) - public List getImportHistory(@PathParam("id") Long id) { - - return jobService.getJobHistoryItems(id) - .map(item -> conversionService.convert(item, JobHistoryItemDTO.class)) - .collect(Collectors.toList()); - } - -} diff --git a/src/main/java/org/ohdsi/webapi/util/ExceptionUtils.java b/src/main/java/org/ohdsi/webapi/util/ExceptionUtils.java index e7f5fc374e..5b581d7f15 100644 --- a/src/main/java/org/ohdsi/webapi/util/ExceptionUtils.java +++ b/src/main/java/org/ohdsi/webapi/util/ExceptionUtils.java @@ -1,14 +1,14 @@ package org.ohdsi.webapi.util; import java.util.Objects; -import jakarta.ws.rs.NotFoundException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; public class ExceptionUtils { - public static void throwNotFoundExceptionIfNull(Object entity, String message) throws NotFoundException { - + public static void throwNotFoundExceptionIfNull(Object entity, String message) { if (Objects.isNull(entity)) { - throw new NotFoundException(message); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, message); } } } diff --git a/src/main/java/org/ohdsi/webapi/util/GenericExceptionMapper.java b/src/main/java/org/ohdsi/webapi/util/GenericExceptionMapper.java deleted file mode 100644 index fc42e68aa2..0000000000 --- a/src/main/java/org/ohdsi/webapi/util/GenericExceptionMapper.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2015 fdefalco. - * - * 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.util; - -import org.ohdsi.webapi.exception.BadRequestAtlasException; -import org.ohdsi.webapi.exception.ConceptNotExistException; -import org.ohdsi.webapi.exception.ConversionAtlasException; -import org.ohdsi.webapi.exception.UserException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.apache.shiro.authz.UnauthorizedException; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.messaging.support.ErrorMessage; - -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.ext.ExceptionMapper; -import jakarta.ws.rs.ext.Provider; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.UndeclaredThrowableException; -import java.util.Objects; -import org.ohdsi.webapi.vocabulary.ConceptRecommendedNotInstalledException; - -/** - * - * @author fdefalco - */ - -@Provider -public class GenericExceptionMapper implements ExceptionMapper { - private static final Logger LOGGER = LoggerFactory.getLogger(GenericExceptionMapper.class); - private final String DETAIL = "Detail: "; - - @Override - public Response toResponse(Throwable ex) { - StringWriter errorStackTrace = new StringWriter(); - ex.printStackTrace(new PrintWriter(errorStackTrace)); - LOGGER.error(errorStackTrace.toString()); - Status responseStatus; - if (ex instanceof DataIntegrityViolationException) { - responseStatus = Status.CONFLICT; - String cause = ex.getCause().getCause().getMessage(); - cause = cause.substring(cause.indexOf(DETAIL) + DETAIL.length()); - ex = new RuntimeException(cause); - } else if (ex instanceof UnauthorizedException || ex instanceof ForbiddenException) { - responseStatus = Status.FORBIDDEN; - } else if (ex instanceof NotFoundException) { - responseStatus = Status.NOT_FOUND; - } else if (ex instanceof BadRequestException) { - responseStatus = Status.BAD_REQUEST; - } else if (ex instanceof UndeclaredThrowableException) { - Throwable throwable = getThrowable((UndeclaredThrowableException)ex); - if (Objects.nonNull(throwable)) { - if (throwable instanceof UnauthorizedException || throwable instanceof ForbiddenException) { - responseStatus = Status.FORBIDDEN; - } else if (throwable instanceof BadRequestAtlasException || throwable instanceof ConceptNotExistException) { - responseStatus = Status.BAD_REQUEST; - ex = throwable; - } else if (throwable instanceof ConversionAtlasException) { - responseStatus = Status.BAD_REQUEST; - // New exception must be created or direct self-reference exception will be thrown - ex = new RuntimeException(throwable.getMessage()); - } else { - responseStatus = Status.INTERNAL_SERVER_ERROR; - ex = new RuntimeException("An exception occurred: " + ex.getClass().getName()); - } - } else { - responseStatus = Status.INTERNAL_SERVER_ERROR; - ex = new RuntimeException("An exception occurred: " + ex.getClass().getName()); - } - } else if (ex instanceof UserException) { - responseStatus = Status.INTERNAL_SERVER_ERROR; - // Create new message to prevent sending error information to client - ex = new RuntimeException(ex.getMessage()); - } else if (ex instanceof ConceptNotExistException) { - responseStatus = Status.BAD_REQUEST; - } else if (ex instanceof ConceptRecommendedNotInstalledException) { - responseStatus = Status.NOT_IMPLEMENTED; - } else { - responseStatus = Status.INTERNAL_SERVER_ERROR; - // Create new message to prevent sending error information to client - ex = new RuntimeException("An exception occurred: " + ex.getClass().getName()); - } - // Clean stacktrace, but keep message - ex.setStackTrace(new StackTraceElement[0]); - ErrorMessage errorMessage = new ErrorMessage(ex); - return Response.status(responseStatus) - .entity(errorMessage) - .type(MediaType.APPLICATION_JSON) - .build(); - } - - private Throwable getThrowable(UndeclaredThrowableException ex) { - if (Objects.nonNull(ex.getUndeclaredThrowable()) && ex.getUndeclaredThrowable() instanceof InvocationTargetException) { - InvocationTargetException ite = (InvocationTargetException) ex.getUndeclaredThrowable(); - return ite.getTargetException(); - } - return null; - } -} \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/util/HttpUtils.java b/src/main/java/org/ohdsi/webapi/util/HttpUtils.java index 28b4d7b8bd..d5147c7796 100644 --- a/src/main/java/org/ohdsi/webapi/util/HttpUtils.java +++ b/src/main/java/org/ohdsi/webapi/util/HttpUtils.java @@ -1,17 +1,19 @@ package org.ohdsi.webapi.util; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.HttpHeaders; import java.io.OutputStream; public class HttpUtils { - public static Response respondBinary(OutputStream stream, String filename) { + public static ResponseEntity respondBinary(OutputStream stream, String filename) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.set(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", filename)); - return Response - .ok(stream) - .type(MediaType.APPLICATION_OCTET_STREAM) - .header("Content-Disposition", String.format("attachment; filename=\"%s\"", filename)) - .build(); + return ResponseEntity.ok() + .headers(headers) + .body(stream); } } diff --git a/src/main/java/org/ohdsi/webapi/util/OutputStreamWriter.java b/src/main/java/org/ohdsi/webapi/util/OutputStreamWriter.java deleted file mode 100644 index 5b6a2fb4f2..0000000000 --- a/src/main/java/org/ohdsi/webapi/util/OutputStreamWriter.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.ohdsi.webapi.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.ext.MessageBodyWriter; -import jakarta.ws.rs.ext.Provider; - -/** - * http://stackoverflow.com/questions/29712554/how-to-download-a-file-using-a-java-rest-service-and-a-data-stream - * - */ - -@Provider -public class OutputStreamWriter implements MessageBodyWriter { - - @Override - public boolean isWriteable(Class type, Type genericType, - Annotation[] annotations, MediaType mediaType) { - return ByteArrayOutputStream.class == type; - } - - @Override - public long getSize(ByteArrayOutputStream t, Class type, Type genericType, - Annotation[] annotations, MediaType mediaType) { - return -1; - } - - @Override - public void writeTo(ByteArrayOutputStream t, Class type, Type genericType, - Annotation[] annotations, MediaType mediaType, - MultivaluedMap httpHeaders, OutputStream entityStream) - throws IOException, WebApplicationException { - t.writeTo(entityStream); - } -} \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/versioning/service/VersionService.java b/src/main/java/org/ohdsi/webapi/versioning/service/VersionService.java index c3f13818fb..54edd43b1d 100644 --- a/src/main/java/org/ohdsi/webapi/versioning/service/VersionService.java +++ b/src/main/java/org/ohdsi/webapi/versioning/service/VersionService.java @@ -22,7 +22,8 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceException; -import jakarta.ws.rs.NotFoundException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -97,7 +98,7 @@ public T create(VersionType type, T assetVersion) { public T update(VersionType type, VersionUpdateDTO updateDTO) { T currentVersion = getRepository(type).findById(updateDTO.getVersionPk()).orElse(null); if (Objects.isNull(currentVersion)) { - throw new NotFoundException("Version not found"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Version not found"); } currentVersion.setComment(updateDTO.getComment()); @@ -109,7 +110,7 @@ public void delete(VersionType type, long assetId, int version) { VersionPK pk = new VersionPK(assetId, version); T currentVersion = getRepository(type).getOne(pk); if (Objects.isNull(currentVersion)) { - throw new NotFoundException("Version not found"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Version not found"); } currentVersion.setArchived(true); } diff --git a/src/test/java/org/ohdsi/webapi/tagging/ReusableTaggingTest.java b/src/test/java/org/ohdsi/webapi/tagging/ReusableTaggingTest.java index e119c5cb1a..c300270939 100644 --- a/src/test/java/org/ohdsi/webapi/tagging/ReusableTaggingTest.java +++ b/src/test/java/org/ohdsi/webapi/tagging/ReusableTaggingTest.java @@ -1,6 +1,6 @@ package org.ohdsi.webapi.tagging; -import org.ohdsi.webapi.reusable.ReusableController; +import org.ohdsi.webapi.reusable.ReusableMvcController; import org.ohdsi.webapi.reusable.dto.ReusableDTO; import org.ohdsi.webapi.reusable.repository.ReusableRepository; import org.ohdsi.webapi.tag.domain.Tag; @@ -12,7 +12,7 @@ public class ReusableTaggingTest extends BaseTaggingTest { @Autowired - private ReusableController controller; + private ReusableMvcController controller; @Autowired private ReusableRepository repository; @@ -24,12 +24,12 @@ public void doCreateInitialData() throws IOException { dto.setName("test name"); dto.setDescription("test description"); - initialDTO = controller.create(dto); + initialDTO = controller.create(dto).getBody(); } @Override protected ReusableDTO doCopyData(ReusableDTO def) { - return controller.copy(def.getId()); + return controller.copy(def.getId()).getBody(); } @Override @@ -64,7 +64,7 @@ protected void unassignProtectedTag(Integer id, boolean isPermissionProtected) { @Override protected ReusableDTO getDTO(Integer id) { - return controller.get(id); + return controller.get(id).getBody(); } @Override @@ -83,6 +83,6 @@ protected void assignTags(Integer id, Tag...tags) { protected List getDTOsByTag(List tagNames) { TagNameListRequestDTO requestDTO = new TagNameListRequestDTO(); requestDTO.setNames(tagNames); - return controller.listByTags(requestDTO); + return controller.listByTags(requestDTO).getBody(); } } diff --git a/src/test/java/org/ohdsi/webapi/test/JobServiceIT.java b/src/test/java/org/ohdsi/webapi/test/JobServiceIT.java index 6114504ce5..ca0b07a5a0 100644 --- a/src/test/java/org/ohdsi/webapi/test/JobServiceIT.java +++ b/src/test/java/org/ohdsi/webapi/test/JobServiceIT.java @@ -11,7 +11,7 @@ import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; -import org.ohdsi.webapi.exampleapplication.ExampleApplicationWithJobService; +// import org.ohdsi.webapi.exampleapplication.ExampleApplicationWithJobService; // Removed - example service deleted import org.ohdsi.webapi.job.JobExecutionResource; import org.ohdsi.webapi.job.JobInstanceResource; import org.springframework.beans.factory.annotation.Value; @@ -98,7 +98,7 @@ public void createAndFindJob() { private void assertJobInstance(final JobInstanceResource instance) { Assert.assertNotNull(instance.getInstanceId()); - assertEquals(ExampleApplicationWithJobService.EXAMPLE_JOB_NAME, instance.getName()); + assertEquals("OhdsiExampleJob", instance.getName()); // Was ExampleApplicationWithJobService.EXAMPLE_JOB_NAME } private void assertOk(final ResponseEntity entity) { diff --git a/src/test/java/org/ohdsi/webapi/test/SecurityIT.java.disabled b/src/test/java/org/ohdsi/webapi/test/SecurityIT.java.disabled new file mode 100644 index 0000000000..fc1fec6b35 --- /dev/null +++ b/src/test/java/org/ohdsi/webapi/test/SecurityIT.java.disabled @@ -0,0 +1,219 @@ +package org.ohdsi.webapi.test; + + +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseOperation; +import com.github.springtestdbunit.annotation.DatabaseTearDown; +import com.github.springtestdbunit.annotation.DbUnitConfiguration; +import com.google.common.collect.ImmutableMap; +import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.server.model.ResourceMethod; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.rules.ErrorCollector; +import org.junit.runner.RunWith; +import org.ohdsi.webapi.JerseyConfig; +import org.ohdsi.webapi.WebApi; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.*; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.util.UriComponentsBuilder; + +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.net.URI; +import java.util.*; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {WebApi.class, WebApiIT.DbUnitConfiguration.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +@DbUnitConfiguration(databaseConnection = "dbUnitDatabaseConnection") +@TestExecutionListeners(value = {DbUnitTestExecutionListener.class}, mergeMode = MERGE_WITH_DEFAULTS) +@DatabaseTearDown(value = "/database/empty.xml", type = DatabaseOperation.DELETE_ALL) +@TestPropertySource(properties = {"security.provider=AtlasRegularSecurity"}) +public class SecurityIT { + + private final Map EXPECTED_RESPONSE_CODES = ImmutableMap.builder() + .put("/info/", HttpStatus.OK) + .put("/i18n/", HttpStatus.OK) + .put("/i18n/locales", HttpStatus.OK) + .put("/ddl/results", HttpStatus.OK) + .put("/ddl/cemresults", HttpStatus.OK) + .put("/ddl/achilles", HttpStatus.OK) + .put("/saml/saml-metadata", HttpStatus.OK) + .put("/saml/slo", HttpStatus.TEMPORARY_REDIRECT) + .build(); + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private JerseyConfig jerseyConfig; + + @Value("${baseUri}") + private String baseUri; + + @Rule + public ErrorCollector collector = new ErrorCollector(); + + private final Logger LOG = LoggerFactory.getLogger(SecurityIT.class); + + @BeforeClass + public static void before() throws IOException { + TomcatURLStreamHandlerFactory.disable(); + ITStarter.before(); + } + + @AfterClass + public static void after() { + ITStarter.tearDownSubject(); + } + + @Test + @Ignore("Skipping due to Java 21 LDAP module access issues with AtlasRegularSecurity") + public void testServiceSecurity() { + + Map> serviceMap = getServiceMap(); + for (String servicePrefix : serviceMap.keySet()) { + List serviceInfos = serviceMap.get(servicePrefix); + for (ServiceInfo serviceInfo : serviceInfos) { + if (!serviceInfo.pathPrefix.startsWith("/")) { + serviceInfo.pathPrefix = "/" + serviceInfo.pathPrefix; + } + serviceInfo.pathPrefix = serviceInfo.pathPrefix.replaceAll("//", "/"); + String rawUrl = baseUri + serviceInfo.pathPrefix; + URI uri = null; + try { + Map parametersMap = prepareParameters(serviceInfo.parameters); + HttpEntity entity = new HttpEntity<>(new HttpHeaders()); + uri = UriComponentsBuilder.fromUriString(rawUrl) + .buildAndExpand(parametersMap).encode().toUri(); + LOG.info("testing service {}:{}", serviceInfo.httpMethod, uri); + ResponseEntity response = this.restTemplate.exchange(uri, serviceInfo.httpMethod, entity, + getResponseType(serviceInfo)); + LOG.info("tested service {}:{} with code {}", serviceInfo.httpMethod, uri, response.getStatusCode()); + HttpStatus expectedStatus = EXPECTED_RESPONSE_CODES.getOrDefault(serviceInfo.pathPrefix, HttpStatus.UNAUTHORIZED); + assertThat(response.getStatusCode()).isEqualTo(expectedStatus); + } catch (Throwable t) { + LOG.info("failed service {}:{}", serviceInfo.httpMethod, uri); + collector.addError(new ThrowableEx(t, rawUrl)); + } + } + } + } + + private Class getResponseType(ServiceInfo serviceInfo) { + + if (serviceInfo.mediaTypes.contains(MediaType.TEXT_PLAIN_TYPE)) { + return String.class; + } else if (serviceInfo.pathPrefix.equalsIgnoreCase("/saml/saml-metadata")) { + return Void.class; + } + return Object.class; + } + + private Map prepareParameters(List parameters) { + + Map parametersMap = new HashMap<>(); + if (parameters != null && !parameters.isEmpty()) { + for (Parameter parameter : parameters) { + String value = "0"; + // if parameter has classloader then it is of object type, else it is primitive type + if (parameter.getRawType().getClassLoader() != null) { + value = null; + } + parametersMap.put(parameter.getSourceName(), value); + } + } + return parametersMap; + } + + /* + * Retrieve information about rest services (path prefixes, http methods, parameters) + */ + private Map> getServiceMap() { + // + Set> classes = this.jerseyConfig.getClasses(); + Map> serviceMap = new HashMap<>(); + for (Class clazz : classes) { + Map> map = scan(clazz); + if (map != null) { + serviceMap.putAll(map); + } + } + + return serviceMap; + } + + private Map> scan(Class baseClass) { + + Resource.Builder builder = Resource.builder(baseClass); + if (null == builder) + return null; + Resource resource = builder.build(); + String uriPrefix = ""; + Map> info = new TreeMap<>(); + return process(uriPrefix, resource, info); + } + + private Map> process(String uriPrefix, Resource resource, Map> info) { + + String pathPrefix = uriPrefix; + List resources = new ArrayList<>(resource.getChildResources()); + if (resource.getPath() != null) { + pathPrefix = pathPrefix + resource.getPath(); + } + for (ResourceMethod method : resource.getAllMethods()) { + List serviceInfos = info.computeIfAbsent(pathPrefix, k -> new ArrayList<>()); + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.pathPrefix = pathPrefix; + serviceInfo.httpMethod = HttpMethod.valueOf(method.getHttpMethod()); + serviceInfo.parameters = method.getInvocable().getParameters(); + serviceInfo.mediaTypes = method.getProducedTypes(); + serviceInfos.add(serviceInfo); + } + for (Resource childResource : resources) { + process(pathPrefix, childResource, info); + } + return info; + } + + private static class ServiceInfo { + public String pathPrefix; + public HttpMethod httpMethod; + public List parameters; + public List mediaTypes; + } + + private static class ThrowableEx extends Throwable { + private final String serviceName; + + public ThrowableEx(Throwable throwable, String serviceName) { + + super(throwable); + this.serviceName = serviceName; + } + + @Override + public String getMessage() { + + return serviceName + ": " + super.getMessage(); + } + } +} From aef7820426738eaea645f48b4ee00d9b7b54f213 Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 22 Nov 2025 18:06:29 +0800 Subject: [PATCH 03/18] cleanup p2 --- .../webapi/test/SecurityIT.java.disabled | 219 ------------------ 1 file changed, 219 deletions(-) delete mode 100644 src/test/java/org/ohdsi/webapi/test/SecurityIT.java.disabled diff --git a/src/test/java/org/ohdsi/webapi/test/SecurityIT.java.disabled b/src/test/java/org/ohdsi/webapi/test/SecurityIT.java.disabled deleted file mode 100644 index fc1fec6b35..0000000000 --- a/src/test/java/org/ohdsi/webapi/test/SecurityIT.java.disabled +++ /dev/null @@ -1,219 +0,0 @@ -package org.ohdsi.webapi.test; - - -import com.github.springtestdbunit.DbUnitTestExecutionListener; -import com.github.springtestdbunit.annotation.DatabaseOperation; -import com.github.springtestdbunit.annotation.DatabaseTearDown; -import com.github.springtestdbunit.annotation.DbUnitConfiguration; -import com.google.common.collect.ImmutableMap; -import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; -import org.glassfish.jersey.server.model.Parameter; -import org.glassfish.jersey.server.model.Resource; -import org.glassfish.jersey.server.model.ResourceMethod; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.rules.ErrorCollector; -import org.junit.runner.RunWith; -import org.ohdsi.webapi.JerseyConfig; -import org.ohdsi.webapi.WebApi; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.*; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.TestExecutionListeners; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.web.util.UriComponentsBuilder; - -import jakarta.ws.rs.core.MediaType; -import java.io.IOException; -import java.net.URI; -import java.util.*; - -import static org.assertj.core.api.Java6Assertions.assertThat; -import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS; - -@RunWith(SpringRunner.class) -@SpringBootTest(classes = {WebApi.class, WebApiIT.DbUnitConfiguration.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("test") -@DbUnitConfiguration(databaseConnection = "dbUnitDatabaseConnection") -@TestExecutionListeners(value = {DbUnitTestExecutionListener.class}, mergeMode = MERGE_WITH_DEFAULTS) -@DatabaseTearDown(value = "/database/empty.xml", type = DatabaseOperation.DELETE_ALL) -@TestPropertySource(properties = {"security.provider=AtlasRegularSecurity"}) -public class SecurityIT { - - private final Map EXPECTED_RESPONSE_CODES = ImmutableMap.builder() - .put("/info/", HttpStatus.OK) - .put("/i18n/", HttpStatus.OK) - .put("/i18n/locales", HttpStatus.OK) - .put("/ddl/results", HttpStatus.OK) - .put("/ddl/cemresults", HttpStatus.OK) - .put("/ddl/achilles", HttpStatus.OK) - .put("/saml/saml-metadata", HttpStatus.OK) - .put("/saml/slo", HttpStatus.TEMPORARY_REDIRECT) - .build(); - - @Autowired - private TestRestTemplate restTemplate; - - @Autowired - private JerseyConfig jerseyConfig; - - @Value("${baseUri}") - private String baseUri; - - @Rule - public ErrorCollector collector = new ErrorCollector(); - - private final Logger LOG = LoggerFactory.getLogger(SecurityIT.class); - - @BeforeClass - public static void before() throws IOException { - TomcatURLStreamHandlerFactory.disable(); - ITStarter.before(); - } - - @AfterClass - public static void after() { - ITStarter.tearDownSubject(); - } - - @Test - @Ignore("Skipping due to Java 21 LDAP module access issues with AtlasRegularSecurity") - public void testServiceSecurity() { - - Map> serviceMap = getServiceMap(); - for (String servicePrefix : serviceMap.keySet()) { - List serviceInfos = serviceMap.get(servicePrefix); - for (ServiceInfo serviceInfo : serviceInfos) { - if (!serviceInfo.pathPrefix.startsWith("/")) { - serviceInfo.pathPrefix = "/" + serviceInfo.pathPrefix; - } - serviceInfo.pathPrefix = serviceInfo.pathPrefix.replaceAll("//", "/"); - String rawUrl = baseUri + serviceInfo.pathPrefix; - URI uri = null; - try { - Map parametersMap = prepareParameters(serviceInfo.parameters); - HttpEntity entity = new HttpEntity<>(new HttpHeaders()); - uri = UriComponentsBuilder.fromUriString(rawUrl) - .buildAndExpand(parametersMap).encode().toUri(); - LOG.info("testing service {}:{}", serviceInfo.httpMethod, uri); - ResponseEntity response = this.restTemplate.exchange(uri, serviceInfo.httpMethod, entity, - getResponseType(serviceInfo)); - LOG.info("tested service {}:{} with code {}", serviceInfo.httpMethod, uri, response.getStatusCode()); - HttpStatus expectedStatus = EXPECTED_RESPONSE_CODES.getOrDefault(serviceInfo.pathPrefix, HttpStatus.UNAUTHORIZED); - assertThat(response.getStatusCode()).isEqualTo(expectedStatus); - } catch (Throwable t) { - LOG.info("failed service {}:{}", serviceInfo.httpMethod, uri); - collector.addError(new ThrowableEx(t, rawUrl)); - } - } - } - } - - private Class getResponseType(ServiceInfo serviceInfo) { - - if (serviceInfo.mediaTypes.contains(MediaType.TEXT_PLAIN_TYPE)) { - return String.class; - } else if (serviceInfo.pathPrefix.equalsIgnoreCase("/saml/saml-metadata")) { - return Void.class; - } - return Object.class; - } - - private Map prepareParameters(List parameters) { - - Map parametersMap = new HashMap<>(); - if (parameters != null && !parameters.isEmpty()) { - for (Parameter parameter : parameters) { - String value = "0"; - // if parameter has classloader then it is of object type, else it is primitive type - if (parameter.getRawType().getClassLoader() != null) { - value = null; - } - parametersMap.put(parameter.getSourceName(), value); - } - } - return parametersMap; - } - - /* - * Retrieve information about rest services (path prefixes, http methods, parameters) - */ - private Map> getServiceMap() { - // - Set> classes = this.jerseyConfig.getClasses(); - Map> serviceMap = new HashMap<>(); - for (Class clazz : classes) { - Map> map = scan(clazz); - if (map != null) { - serviceMap.putAll(map); - } - } - - return serviceMap; - } - - private Map> scan(Class baseClass) { - - Resource.Builder builder = Resource.builder(baseClass); - if (null == builder) - return null; - Resource resource = builder.build(); - String uriPrefix = ""; - Map> info = new TreeMap<>(); - return process(uriPrefix, resource, info); - } - - private Map> process(String uriPrefix, Resource resource, Map> info) { - - String pathPrefix = uriPrefix; - List resources = new ArrayList<>(resource.getChildResources()); - if (resource.getPath() != null) { - pathPrefix = pathPrefix + resource.getPath(); - } - for (ResourceMethod method : resource.getAllMethods()) { - List serviceInfos = info.computeIfAbsent(pathPrefix, k -> new ArrayList<>()); - ServiceInfo serviceInfo = new ServiceInfo(); - serviceInfo.pathPrefix = pathPrefix; - serviceInfo.httpMethod = HttpMethod.valueOf(method.getHttpMethod()); - serviceInfo.parameters = method.getInvocable().getParameters(); - serviceInfo.mediaTypes = method.getProducedTypes(); - serviceInfos.add(serviceInfo); - } - for (Resource childResource : resources) { - process(pathPrefix, childResource, info); - } - return info; - } - - private static class ServiceInfo { - public String pathPrefix; - public HttpMethod httpMethod; - public List parameters; - public List mediaTypes; - } - - private static class ThrowableEx extends Throwable { - private final String serviceName; - - public ThrowableEx(Throwable throwable, String serviceName) { - - super(throwable); - this.serviceName = serviceName; - } - - @Override - public String getMessage() { - - return serviceName + ": " + super.getMessage(); - } - } -} From a182b53d0e562ae8d25dc284cd424155792dac9b Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:35:34 +0800 Subject: [PATCH 04/18] migreate auth provider --- .../ohdsi/webapi/auth/AuthProviderService.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/ohdsi/webapi/auth/AuthProviderService.java b/src/main/java/org/ohdsi/webapi/auth/AuthProviderService.java index d82ce0c889..fafc3e7628 100644 --- a/src/main/java/org/ohdsi/webapi/auth/AuthProviderService.java +++ b/src/main/java/org/ohdsi/webapi/auth/AuthProviderService.java @@ -16,13 +16,11 @@ package org.ohdsi.webapi.auth; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; - import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Controller; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; @@ -30,8 +28,8 @@ /** * Service that exposes available authentication providers for Atlas frontend. */ -@Path("/auth") -@Controller +@RestController +@RequestMapping("/auth") public class AuthProviderService { @Value("${security.auth.jdbc.enabled}") @@ -74,9 +72,7 @@ public class AuthProviderService { * Get the list of enabled authentication providers. * This endpoint is publicly accessible (no auth required). */ - @GET - @Path("/providers") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(value = "/providers", produces = MediaType.APPLICATION_JSON_VALUE) public List getProviders() { List providers = new ArrayList<>(); From 124f0bee985979a1e3e69a97f408a9516d92072d Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:01:17 +0800 Subject: [PATCH 05/18] cleanup --- .../org/ohdsi/webapi/activity/Tracker.java | 31 +- .../ohdsi/webapi/i18n/I18nServiceImpl.java | 60 +- .../org/ohdsi/webapi/info/InfoService.java | 9 +- .../webapi/job/NotificationServiceImpl.java | 98 +- .../webapi/mvc/CohortSampleMvcController.java | 216 --- .../webapi/mvc/EvidenceMvcController.java | 1051 ------------- .../org/ohdsi/webapi/mvc/MigrationUtils.java | 56 - .../webapi/mvc/NotificationMvcController.java | 110 -- .../ohdsi/webapi/mvc/TagMvcController.java | 132 -- .../ohdsi/webapi/mvc/ToolMvcController.java | 44 - .../mvc/controller/ActivityMvcController.java | 38 - .../controller/CDMResultsMvcController.java | 268 ---- .../CohortAnalysisMvcController.java | 127 -- .../CohortDefinitionMvcController.java | 1134 -------------- .../mvc/controller/CohortMvcController.java | 93 -- .../CohortResultsMvcController.java | 1386 ----------------- .../controller/ConceptSetMvcController.java | 1062 ------------- .../mvc/controller/DDLMvcController.java | 215 --- .../controller/FeasibilityMvcController.java | 256 --- .../mvc/controller/I18nMvcController.java | 83 - .../mvc/controller/InfoMvcController.java | 44 - .../mvc/controller/JobMvcController.java | 156 -- .../controller/PermissionMvcController.java | 211 --- .../mvc/controller/SourceMvcController.java | 398 ----- .../controller/SqlRenderMvcController.java | 49 - .../mvc/controller/UserMvcController.java | 368 ----- .../controller/VocabularyMvcController.java | 715 --------- .../reusable/ReusableMvcController.java | 358 ----- .../webapi/reusable/ReusableService.java | 84 +- .../webapi/security/PermissionService.java | 160 +- .../webapi/service/CDMResultsService.java | 103 +- .../webapi/service/CohortAnalysisService.java | 81 +- .../service/CohortDefinitionService.java | 128 +- .../webapi/service/CohortResultsService.java | 699 +++++---- .../webapi/service/CohortSampleService.java | 73 +- .../ohdsi/webapi/service/CohortService.java | 23 +- .../webapi/service/ConceptSetService.java | 209 ++- .../org/ohdsi/webapi/service/DDLService.java | 35 +- .../ohdsi/webapi/service/EvidenceService.java | 112 +- .../webapi/service/FeasibilityService.java | 93 +- .../org/ohdsi/webapi/service/JobService.java | 62 +- .../SSOService.java} | 17 +- .../webapi/service/SqlRenderService.java | 14 +- .../org/ohdsi/webapi/service/UserService.java | 71 +- .../webapi/service/VocabularyService.java | 489 ++++-- .../ohdsi/webapi/source/SourceService.java | 465 +++++- .../controller/StatisticMvcController.java | 291 ---- .../statistic/service/StatisticService.java | 263 +++- .../java/org/ohdsi/webapi/tag/TagService.java | 97 +- .../ohdsi/webapi/tool/ToolServiceImpl.java | 28 +- .../importer/UserImportJobMvcController.java | 186 --- .../importer/UserImportMvcController.java | 249 --- .../service/UserImportJobServiceImpl.java | 124 +- .../service/UserImportServiceImpl.java | 177 ++- .../migration/DualRuntimeTestSupport.java | 94 -- .../test/migration/MigrationPhase1IT.java | 50 - .../test/migration/MigrationPhase2IT.java | 94 -- .../test/migration/MigrationPhase3IT.java | 165 -- 58 files changed, 2878 insertions(+), 10626 deletions(-) delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/CohortSampleMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/EvidenceMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/NotificationMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/TagMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/ToolMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/ActivityMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CDMResultsMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CohortAnalysisMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CohortDefinitionMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CohortMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/ConceptSetMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/DDLMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/FeasibilityMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/I18nMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/InfoMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/JobMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/PermissionMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/SourceMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/SqlRenderMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/UserMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/controller/VocabularyMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/reusable/ReusableMvcController.java rename src/main/java/org/ohdsi/webapi/{mvc/controller/SSOMvcController.java => service/SSOService.java} (80%) delete mode 100644 src/main/java/org/ohdsi/webapi/statistic/controller/StatisticMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/user/importer/UserImportJobMvcController.java delete mode 100644 src/main/java/org/ohdsi/webapi/user/importer/UserImportMvcController.java delete mode 100644 src/test/java/org/ohdsi/webapi/test/migration/DualRuntimeTestSupport.java delete mode 100644 src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase1IT.java delete mode 100644 src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase2IT.java delete mode 100644 src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase3IT.java diff --git a/src/main/java/org/ohdsi/webapi/activity/Tracker.java b/src/main/java/org/ohdsi/webapi/activity/Tracker.java index 01fce0ae68..6c2f1441e1 100644 --- a/src/main/java/org/ohdsi/webapi/activity/Tracker.java +++ b/src/main/java/org/ohdsi/webapi/activity/Tracker.java @@ -17,33 +17,54 @@ import java.util.Date; import java.util.concurrent.ConcurrentLinkedQueue; + import org.ohdsi.webapi.activity.Activity.ActivityType; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; /** + * Activity tracker service * * @author fdefalco + * @deprecated Example REST service - will be deprecated in a future release */ +@RestController +@RequestMapping("/activity") +@Deprecated public class Tracker { private static ConcurrentLinkedQueue activityLog; - + public static void trackActivity(ActivityType type, String caption) { if (activityLog == null) { activityLog = new ConcurrentLinkedQueue<>(); } - + Activity activity = new Activity(); activity.caption = caption; activity.timestamp = new Date(); activity.type = type; - + activityLog.add(activity); } - + public static Object[] getActivity() { if (activityLog == null) { activityLog = new ConcurrentLinkedQueue<>(); } - + return activityLog.toArray(); } + + /** + * Get latest activity + * + * @deprecated DO NOT USE - will be removed in future release + */ + @GetMapping(value = "/latest", produces = MediaType.APPLICATION_JSON_VALUE) + @Deprecated + public Object[] getLatestActivity() { + return getActivity(); + } } diff --git a/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java b/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java index cc9ce89690..75201fed57 100644 --- a/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java @@ -3,9 +3,14 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; import org.ohdsi.circe.helper.ResourceHelper; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.stereotype.Component; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import jakarta.annotation.PostConstruct; import org.springframework.web.server.ResponseStatusException; @@ -15,10 +20,25 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; -@Component +/** + * Spring MVC version of I18nController + * + * Migration Status: Replaces /i18n/I18nController.java (Jersey) + * Endpoints: 2 GET endpoints + * Complexity: Simple - i18n resource handling + */ +@RestController +@RequestMapping("/i18n") public class I18nServiceImpl implements I18nService { + @Value("${i18n.enabled}") + private boolean i18nEnabled = true; + + @Value("${i18n.defaultLocale}") + private String defaultLocale = "en"; + private List availableLocales; @PostConstruct @@ -69,4 +89,40 @@ public String getLocaleResource(Locale locale) { } return messages; } + + // REST Endpoints + + /** + * Get i18n resources for current locale + * + * Note: Locale is resolved by LocaleInterceptor and stored in LocaleContextHolder + */ + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public String getResources() { + // Get locale from LocaleContextHolder (set by LocaleInterceptor) + Locale locale = LocaleContextHolder.getLocale(); + + if (!this.i18nEnabled || locale == null || !isLocaleSupported(locale.getLanguage())) { + locale = Locale.forLanguageTag(defaultLocale); + } + + return getLocaleResource(locale); + } + + /** + * Get list of available locales + */ + @GetMapping(value = "/locales", produces = MediaType.APPLICATION_JSON_VALUE) + public List getAvailableLocalesEndpoint() { + if (this.i18nEnabled) { + return getAvailableLocales(); + } + + // if i18n is disabled, then return only default locale + return ImmutableList.of(new LocaleDTO(this.defaultLocale, null, true)); + } + + private boolean isLocaleSupported(String code) { + return getAvailableLocales().stream().anyMatch(l -> Objects.equals(code, l.getCode())); + } } diff --git a/src/main/java/org/ohdsi/webapi/info/InfoService.java b/src/main/java/org/ohdsi/webapi/info/InfoService.java index 01445aba3f..b14fc4070b 100644 --- a/src/main/java/org/ohdsi/webapi/info/InfoService.java +++ b/src/main/java/org/ohdsi/webapi/info/InfoService.java @@ -22,12 +22,16 @@ import org.apache.commons.lang3.StringUtils; import org.ohdsi.info.ConfigurationInfo; import org.springframework.boot.info.BuildProperties; -import org.springframework.stereotype.Controller; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.stream.Collectors; -@Controller +@RestController +@RequestMapping("/info") public class InfoService { private final Info info; @@ -46,6 +50,7 @@ public InfoService(BuildProperties buildProperties, BuildInfo buildInfo, List WHITE_LIST = new ArrayList<>(); @@ -32,14 +53,18 @@ public class NotificationServiceImpl implements NotificationService { private final SearchableJobExecutionDao jobExecutionDao; private final PermissionManager permissionManager; private final UserRepository userRepository; + private final GenericConversionService conversionService; @Value("#{!'${security.provider}'.equals('DisabledSecurity')}") private boolean securityEnabled; - public NotificationServiceImpl(SearchableJobExecutionDao jobExecutionDao, List whiteList, PermissionManager permissionManager, UserRepository userRepository) { + public NotificationServiceImpl(SearchableJobExecutionDao jobExecutionDao, List whiteList, + PermissionManager permissionManager, UserRepository userRepository, + @Qualifier("conversionService") GenericConversionService conversionService) { this.jobExecutionDao = jobExecutionDao; this.permissionManager = permissionManager; this.userRepository = userRepository; + this.conversionService = conversionService; whiteList.forEach(g -> { WHITE_LIST.add(g.getJobName()); FOLDING_KEYS.add(g.getExecutionFoldingKey()); @@ -48,6 +73,73 @@ public NotificationServiceImpl(SearchableJobExecutionDao jobExecutionDao, List list( + @RequestParam(value = "hide_statuses", required = false) String hideStatuses, + @RequestParam(value = "refreshJobs", defaultValue = "FALSE") Boolean refreshJobs) { + List statuses = new ArrayList<>(); + if (StringUtils.isNotEmpty(hideStatuses)) { + for (String status : hideStatuses.split(",")) { + try { + statuses.add(BatchStatus.valueOf(status)); + } catch (IllegalArgumentException e) { + log.warn("Invalid argument passed as batch status: {}", status); + } + } + } + List executionInfos; + if (refreshJobs) { + executionInfos = findRefreshCacheLastJobs(); + } else { + executionInfos = findLastJobs(statuses); + } + return executionInfos.stream().map(this::toDTO).collect(Collectors.toList()); + } + + /** + * Gets the date when notifications were last viewed + * + * @summary Get notification last viewed date + * @return The date when notifications were last viewed + */ + @GetMapping(value = "/viewed", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional(readOnly = true) + public Date getLastViewedTimeEndpoint() { + try { + return getLastViewedTime(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Sets the date when notifications were last viewed + * + * @summary Set notification last viewed date + * @param stamp The date to set + */ + @PostMapping(value = "/viewed", consumes = MediaType.APPLICATION_JSON_VALUE) + public void setLastViewedTimeEndpoint(@RequestBody Date stamp) { + try { + setLastViewedTime(stamp); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private JobExecutionResource toDTO(JobExecutionInfo entity) { + return conversionService.convert(entity, JobExecutionResource.class); + } + @Override public List findLastJobs(List hideStatuses) { return findJobs(hideStatuses, MAX_SIZE, false); diff --git a/src/main/java/org/ohdsi/webapi/mvc/CohortSampleMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/CohortSampleMvcController.java deleted file mode 100644 index d61373903e..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/CohortSampleMvcController.java +++ /dev/null @@ -1,216 +0,0 @@ -package org.ohdsi.webapi.mvc; - -import org.ohdsi.webapi.GenerationStatus; -import org.ohdsi.webapi.cohortdefinition.CohortDefinitionRepository; -import org.ohdsi.webapi.cohortdefinition.CohortGenerationInfo; -import org.ohdsi.webapi.cohortdefinition.CohortGenerationInfoId; -import org.ohdsi.webapi.cohortdefinition.CohortGenerationInfoRepository; -import org.ohdsi.webapi.cohortsample.CohortSamplingService; -import org.ohdsi.webapi.cohortsample.dto.CohortSampleDTO; -import org.ohdsi.webapi.cohortsample.dto.CohortSampleListDTO; -import org.ohdsi.webapi.cohortsample.dto.SampleParametersDTO; -import org.ohdsi.webapi.source.Source; -import org.ohdsi.webapi.source.SourceRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; - -import java.util.*; - -@RestController -@RequestMapping("/cohortsample") -public class CohortSampleMvcController extends AbstractMvcController { - private final CohortDefinitionRepository cohortDefinitionRepository; - private final CohortGenerationInfoRepository generationInfoRepository; - private final CohortSamplingService samplingService; - private final SourceRepository sourceRepository; - - @Autowired - public CohortSampleMvcController( - CohortSamplingService samplingService, - SourceRepository sourceRepository, - CohortDefinitionRepository cohortDefinitionRepository, - CohortGenerationInfoRepository generationInfoRepository - ) { - this.samplingService = samplingService; - this.sourceRepository = sourceRepository; - this.cohortDefinitionRepository = cohortDefinitionRepository; - this.generationInfoRepository = generationInfoRepository; - } - - /** - * Get information about cohort samples for a data source - * - * @param cohortDefinitionId The id for an existing cohort definition - * @param sourceKey - * @return JSON containing information about cohort samples - */ - @GetMapping("/{cohortDefinitionId}/{sourceKey}") - public ResponseEntity listCohortSamples( - @PathVariable("cohortDefinitionId") int cohortDefinitionId, - @PathVariable("sourceKey") String sourceKey - ) { - Source source = getSource(sourceKey); - CohortSampleListDTO result = new CohortSampleListDTO(); - - result.setCohortDefinitionId(cohortDefinitionId); - result.setSourceId(source.getId()); - - CohortGenerationInfo generationInfo = generationInfoRepository.findById( - new CohortGenerationInfoId(cohortDefinitionId, source.getId())).orElse(null); - result.setGenerationStatus(generationInfo != null ? generationInfo.getStatus() : null); - result.setIsValid(generationInfo != null && generationInfo.isIsValid()); - - result.setSamples(this.samplingService.listSamples(cohortDefinitionId, source.getId())); - - return ok(result); - } - - /** - * Get an existing cohort sample - * @param cohortDefinitionId - * @param sourceKey - * @param sampleId - * @param fields - * @return personId, gender, age of each person in the cohort sample - */ - @GetMapping("/{cohortDefinitionId}/{sourceKey}/{sampleId}") - public ResponseEntity getCohortSample( - @PathVariable("cohortDefinitionId") int cohortDefinitionId, - @PathVariable("sourceKey") String sourceKey, - @PathVariable("sampleId") Integer sampleId, - @RequestParam(defaultValue = "") String fields - ) { - List returnFields = Arrays.asList(fields.split(",")); - boolean withRecordCounts = returnFields.contains("recordCount"); - return ok(this.samplingService.getSample(sampleId, withRecordCounts)); - } - - /** - * @summary Refresh a cohort sample - * Refresh a cohort sample for a given source key. This will re-sample persons from the cohort. - * @param cohortDefinitionId - * @param sourceKey - * @param sampleId - * @param fields - * @return A sample of persons from a cohort - */ - @PostMapping("/{cohortDefinitionId}/{sourceKey}/{sampleId}/refresh") - public ResponseEntity refreshCohortSample( - @PathVariable("cohortDefinitionId") int cohortDefinitionId, - @PathVariable("sourceKey") String sourceKey, - @PathVariable("sampleId") Integer sampleId, - @RequestParam(defaultValue = "") String fields - ) { - List returnFields = Arrays.asList(fields.split(",")); - boolean withRecordCounts = returnFields.contains("recordCount"); - this.samplingService.refreshSample(sampleId); - return ok(this.samplingService.getSample(sampleId, withRecordCounts)); - } - - /** - * Does an existing cohort have samples? - * @param cohortDefinitionId - * @return true or false - */ - @GetMapping("/has-samples/{cohortDefinitionId}") - public ResponseEntity> hasSamples( - @PathVariable("cohortDefinitionId") int cohortDefinitionId - ) { - int nSamples = this.samplingService.countSamples(cohortDefinitionId); - return ok(Collections.singletonMap("hasSamples", nSamples > 0)); - } - - /** - * Does an existing cohort have samples from a particular source? - * @param sourceKey - * @param cohortDefinitionId - * @return true or false - */ - @GetMapping("/has-samples/{cohortDefinitionId}/{sourceKey}") - public ResponseEntity> hasSamplesForSource( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("cohortDefinitionId") int cohortDefinitionId - ) { - Source source = getSource(sourceKey); - int nSamples = this.samplingService.countSamples(cohortDefinitionId, source.getId()); - return ok(Collections.singletonMap("hasSamples", nSamples > 0)); - } - - /** - * Create a new cohort sample - * @param sourceKey - * @param cohortDefinitionId - * @param sampleParameters - * @return - */ - @PostMapping("/{cohortDefinitionId}/{sourceKey}") - public ResponseEntity createCohortSample( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("cohortDefinitionId") int cohortDefinitionId, - @RequestBody SampleParametersDTO sampleParameters - ) { - sampleParameters.validate(); - Source source = getSource(sourceKey); - if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort definition " + cohortDefinitionId + " does not exist."); - } - CohortGenerationInfo generationInfo = generationInfoRepository.findById( - new CohortGenerationInfoId(cohortDefinitionId, source.getId())).orElse(null); - if (generationInfo == null || generationInfo.getStatus() != GenerationStatus.COMPLETE) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cohort is not yet generated"); - } - return ok(samplingService.createSample(source, cohortDefinitionId, sampleParameters)); - } - - /** - * Delete a cohort sample - * @param sourceKey - * @param cohortDefinitionId - * @param sampleId - * @return - */ - @DeleteMapping("/{cohortDefinitionId}/{sourceKey}/{sampleId}") - public ResponseEntity deleteCohortSample( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("cohortDefinitionId") int cohortDefinitionId, - @PathVariable("sampleId") int sampleId - ) { - Source source = getSource(sourceKey); - if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort definition " + cohortDefinitionId + " does not exist."); - } - samplingService.deleteSample(cohortDefinitionId, source, sampleId); - return noContent(); - } - - /** - * Delete all samples for a cohort on a data source - * @param sourceKey - * @param cohortDefinitionId - * @return - */ - @DeleteMapping("/{cohortDefinitionId}/{sourceKey}") - public ResponseEntity deleteCohortSamples( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("cohortDefinitionId") int cohortDefinitionId - ) { - Source source = getSource(sourceKey); - if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort definition " + cohortDefinitionId + " does not exist."); - } - samplingService.launchDeleteSamplesTasklet(cohortDefinitionId, source.getId()); - return ResponseEntity.status(HttpStatus.ACCEPTED).build(); - } - - private Source getSource(String sourceKey) { - Source source = sourceRepository.findBySourceKey(sourceKey); - if (source == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Source " + sourceKey + " does not exist"); - } - return source; - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/EvidenceMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/EvidenceMvcController.java deleted file mode 100644 index d82b2bdd78..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/EvidenceMvcController.java +++ /dev/null @@ -1,1051 +0,0 @@ -package org.ohdsi.webapi.mvc; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Collection; -import java.util.ArrayList; -import java.util.List; -import java.io.IOException; -import java.math.BigDecimal; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.json.JSONException; -import org.json.JSONObject; -import org.ohdsi.circe.helper.ResourceHelper; -import org.ohdsi.circe.vocabulary.ConceptSetExpression; -import org.ohdsi.circe.vocabulary.ConceptSetExpressionQueryBuilder; -import org.ohdsi.sql.SqlRender; -import org.ohdsi.sql.SqlTranslate; -import org.ohdsi.webapi.conceptset.ConceptSetGenerationInfoRepository; -import org.ohdsi.webapi.evidence.CohortStudyMapping; -import org.ohdsi.webapi.evidence.CohortStudyMappingRepository; -import org.ohdsi.webapi.evidence.ConceptCohortMapping; -import org.ohdsi.webapi.evidence.ConceptCohortMappingRepository; -import org.ohdsi.webapi.evidence.ConceptOfInterestMapping; -import org.ohdsi.webapi.evidence.ConceptOfInterestMappingRepository; -import org.ohdsi.webapi.evidence.DrugEvidence; -import org.ohdsi.webapi.evidence.EvidenceDetails; -import org.ohdsi.webapi.evidence.EvidenceSummary; -import org.ohdsi.webapi.evidence.EvidenceUniverse; -import org.ohdsi.webapi.evidence.HoiEvidence; -import org.ohdsi.webapi.evidence.DrugHoiEvidence; -import org.ohdsi.webapi.evidence.DrugLabel; -import org.ohdsi.webapi.evidence.DrugLabelInfo; -import org.ohdsi.webapi.evidence.DrugLabelRepository; -import org.ohdsi.webapi.evidence.EvidenceInfo; -import org.ohdsi.webapi.evidence.DrugRollUpEvidence; -import org.ohdsi.webapi.evidence.Evidence; -import org.ohdsi.webapi.evidence.SpontaneousReport; -import org.ohdsi.webapi.evidence.EvidenceSearch; -import org.ohdsi.webapi.evidence.negativecontrols.NegativeControlDTO; -import org.ohdsi.webapi.evidence.negativecontrols.NegativeControlMapper; -import org.ohdsi.webapi.evidence.negativecontrols.NegativeControlTaskParameters; -import org.ohdsi.webapi.evidence.negativecontrols.NegativeControlTasklet; -import org.ohdsi.webapi.job.GeneratesNotification; -import org.ohdsi.webapi.job.JobExecutionResource; -import org.ohdsi.webapi.job.JobTemplate; -import org.ohdsi.webapi.service.EvidenceService; -import org.ohdsi.webapi.service.ConceptSetService; -import org.ohdsi.webapi.source.Source; -import org.ohdsi.webapi.source.SourceDaimon; -import org.ohdsi.webapi.util.PreparedSqlRender; -import org.ohdsi.webapi.util.PreparedStatementRenderer; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.web.bind.annotation.*; - -/** - * Provides REST services for querying the Common Evidence Model - * - * @summary REST services for querying the Common Evidence Model See - * https://github.com/OHDSI/CommonEvidenceModel - */ -@RestController -@RequestMapping("/evidence") -public class EvidenceMvcController extends AbstractMvcController implements GeneratesNotification { - - private static final String NAME = "negativeControlsAnalysisJob"; - - @Autowired - private JobTemplate jobTemplate; - - @Autowired - private DrugLabelRepository drugLabelRepository; - - @Autowired - private ConceptCohortMappingRepository mappingRepository; - - @Autowired - private ConceptOfInterestMappingRepository conceptOfInterestMappingRepository; - - @Autowired - private CohortStudyMappingRepository cohortStudyMappingRepository; - - @Autowired - private ConceptSetGenerationInfoRepository conceptSetGenerationInfoRepository; - - @Autowired - private ConceptSetService conceptSetService; - - @Autowired - private EvidenceService daoService; - - private final RowMapper drugLabelRowMapper = new RowMapper() { - @Override - public DrugLabelInfo mapRow(final ResultSet rs, final int arg1) throws SQLException { - final DrugLabelInfo returnVal = new DrugLabelInfo(); - returnVal.conceptId = rs.getString("CONCEPT_ID"); - returnVal.conceptName = rs.getString("CONCEPT_NAME"); - returnVal.usaProductLabelExists = rs.getInt("US_SPL_LABEL"); - return returnVal; - } - }; - - public static class DrugConditionSourceSearchParams { - - @JsonProperty("targetDomain") - public String targetDomain = "CONDITION"; - @JsonProperty("drugConceptIds") - public int[] drugConceptIds; - @JsonProperty("conditionConceptIds") - public int[] conditionConceptIds; - @JsonProperty("sourceIds") - public String[] sourceIds; - - public String getDrugConceptIds() { - return StringUtils.join(drugConceptIds, ','); - } - - public String getConditionConceptIds() { - return StringUtils.join(conditionConceptIds, ','); - } - - public String getSourceIds() { - if (sourceIds != null) { - List ids = Arrays.stream(sourceIds) - .map(sourceId -> sourceId.replaceAll("(\"|')", "")) - .collect(Collectors.toList()); - return "'" + StringUtils.join(ids, "','") + "'"; - } - return "''"; - } - } - - /** - * PENELOPE function: search - * the cohort_study table for the selected cohortId in the WebAPI DB - * - * @summary Find studies for a cohort - will be depreciated - * @deprecated - * @param cohortId The cohort Id - * @return A list of studies related to the cohort - */ - @GetMapping("/study/{cohortId}") - public ResponseEntity> getCohortStudyMapping(@PathVariable("cohortId") int cohortId) { - return ok(cohortStudyMappingRepository.findByCohortDefinitionId(cohortId)); - } - - /** - * PENELOPE function: search - * the COHORT_CONCEPT_MAP for the selected cohortId in the WebAPI DB - * - * @summary Find cohorts for a concept - will be depreciated - * @deprecated - * @param conceptId The concept Id of interest - * @return A list of cohorts for the specified conceptId - */ - @GetMapping("/mapping/{conceptId}") - public ResponseEntity> getConceptCohortMapping(@PathVariable("conceptId") int conceptId) { - return ok(mappingRepository.findByConceptId(conceptId)); - } - - /** - * PENELOPE function: - * reference to a manually curated table related concept_of_interest in - * WebAPI for use with PENELOPE. This will be depreciated in a future - * release. - * - * @summary Find a custom concept mapping - will be depreciated - * @deprecated - * @param conceptId The conceptId of interest - * @return A list of concepts based on the conceptId of interest - */ - @GetMapping("/conceptofinterest/{conceptId}") - public ResponseEntity> getConceptOfInterest(@PathVariable("conceptId") int conceptId) { - return ok(conceptOfInterestMappingRepository.findAllByConceptId(conceptId)); - } - - /** - * PENELOPE function: - * reference to the list of product labels in the WebAPI DRUG_LABELS table - * that associates a product label SET_ID to the RxNorm ingredient. This - * will be depreciated in a future release as this can be found using the - * OMOP vocabulary - * - * @summary Find a drug label - will be depreciated - * @deprecated - * @param setid The drug label setId - * @return The set of drug labels that match the setId specified. - */ - @GetMapping("/label/{setid}") - public ResponseEntity> getDrugLabel(@PathVariable("setid") String setid) { - return ok(drugLabelRepository.findAllBySetid(setid)); - } - - /** - * PENELOPE function: search - * the DRUG_LABELS.search_name for the searchTerm - * - * @summary Search for a drug label - will be depreciated - * @deprecated - * @param searchTerm The search term - * @return A list of drug labels matching the search term - */ - @GetMapping("/labelsearch/{searchTerm}") - public ResponseEntity> searchDrugLabels(@PathVariable("searchTerm") String searchTerm) { - return ok(drugLabelRepository.searchNameContainsTerm(searchTerm)); - } - - /** - * Provides a high level description of the information found in the Common - * Evidence Model (CEM). - * - * @summary Get summary of the Common Evidence Model (CEM) contents - * @param sourceKey The source key containing the CEM daimon - * @return A collection of evidence information stored in CEM - */ - @GetMapping("/{sourceKey}/info") - public ResponseEntity> getInfo(@PathVariable("sourceKey") String sourceKey) { - Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); - String sqlPath = "/resources/evidence/sql/getInfo.sql"; - String tqName = "cem_schema"; - String tqValue = source.getTableQualifier(SourceDaimon.DaimonType.CEM); - PreparedStatementRenderer psr = new PreparedStatementRenderer(source, sqlPath, tqName, tqValue); - return ok(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { - - EvidenceInfo info = new EvidenceInfo(); - info.title = rs.getString("TITLE"); - info.description = rs.getString("DESCRIPTION"); - info.provenance = rs.getString("PROVENANCE"); - info.contributor = rs.getString("CONTRIBUTOR"); - info.contactName = rs.getString("CONTACT_NAME"); - info.creationDate = rs.getDate("CREATION_DATE"); - info.coverageStartDate = rs.getDate("COVERAGE_START_DATE"); - info.coverageEndDate = rs.getDate("COVERAGE_END_DATE"); - info.versionIdentifier = rs.getString("VERSION_IDENTIFIER"); - return info; - })); - } - - /** - * Searches the evidence base for evidence related to one ore more drug and - * condition combinations for the source(s) specified - * - * @param sourceKey The source key containing the CEM daimon - * @param searchParams - * @return - */ - @PostMapping("/{sourceKey}/drugconditionpairs") - public ResponseEntity> getDrugConditionPairs(@PathVariable("sourceKey") String sourceKey, @RequestBody DrugConditionSourceSearchParams searchParams) { - Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); - String sql = getDrugHoiEvidenceSQL(source, searchParams); - return ok(daoService.getSourceJdbcTemplate(source).query(sql, (rs, rowNum) -> { - String evidenceSource = rs.getString("SOURCE_ID"); - String mappingType = rs.getString("MAPPING_TYPE"); - String drugConceptId = rs.getString("DRUG_CONCEPT_ID"); - String drugConceptName = rs.getString("DRUG_CONCEPT_NAME"); - String conditionConceptId = rs.getString("CONDITION_CONCEPT_ID"); - String conditionConceptName = rs.getString("CONDITION_CONCEPT_NAME"); - String uniqueIdentifier = rs.getString("UNIQUE_IDENTIFIER"); - - DrugHoiEvidence evidence = new DrugHoiEvidence(); - evidence.evidenceSource = evidenceSource; - evidence.mappingType = mappingType; - evidence.drugConceptId = drugConceptId; - evidence.drugConceptName = drugConceptName; - evidence.hoiConceptId = conditionConceptId; - evidence.hoiConceptName = conditionConceptName; - evidence.uniqueIdentifier = uniqueIdentifier; - - return evidence; - })); - } - - /** - * Retrieves a list of evidence for the specified drug conceptId - * - * @summary Get Evidence For Drug - * @param sourceKey The source key containing the CEM daimon - * @param id - An RxNorm Drug Concept Id - * @return A list of evidence - */ - @GetMapping("/{sourceKey}/drug/{id}") - public ResponseEntity> getDrugEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id) { - Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); - PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); - return ok(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { - String evidenceSource = rs.getString("SOURCE_ID"); - String hoi = rs.getString("CONCEPT_ID_2"); - String hoiName = rs.getString("CONCEPT_ID_2_NAME"); - String statType = rs.getString("STATISTIC_VALUE_TYPE"); - BigDecimal statVal = rs.getBigDecimal("STATISTIC_VALUE"); - String relationshipType = rs.getString("RELATIONSHIP_ID"); - String uniqueIdentifier = rs.getString("UNIQUE_IDENTIFIER"); - String uniqueIdentifierType = rs.getString("UNIQUE_IDENTIFIER_TYPE"); - - DrugEvidence evidence = new DrugEvidence(); - evidence.evidenceSource = evidenceSource; - evidence.hoiConceptId = hoi; - evidence.hoiConceptName = hoiName; - evidence.relationshipType = relationshipType; - evidence.statisticType = statType; - evidence.statisticValue = statVal; - evidence.uniqueIdentifier = uniqueIdentifier; - evidence.uniqueIdentifierType = uniqueIdentifierType; - - return evidence; - })); - } - - /** - * Retrieves a list of evidence for the specified health outcome of interest - * (hoi) conceptId - * - * @summary Get Evidence For Health Outcome - * @param sourceKey The source key containing the CEM daimon - * @param id The conceptId for the health outcome of interest - * @return A list of evidence - */ - @GetMapping("/{sourceKey}/hoi/{id}") - public ResponseEntity> getHoiEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id) { - Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); - PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); - return ok(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { - String evidenceSource = rs.getString("SOURCE_ID"); - String drug = rs.getString("CONCEPT_ID_1"); - String drugName = rs.getString("CONCEPT_ID_1_NAME"); - String statType = rs.getString("STATISTIC_VALUE_TYPE"); - BigDecimal statVal = rs.getBigDecimal("STATISTIC_VALUE"); - String relationshipType = rs.getString("RELATIONSHIP_ID"); - String uniqueIdentifier = rs.getString("UNIQUE_IDENTIFIER"); - String uniqueIdentifierType = rs.getString("UNIQUE_IDENTIFIER_TYPE"); - - HoiEvidence evidence = new HoiEvidence(); - evidence.evidenceSource = evidenceSource; - evidence.drugConceptId = drug; - evidence.drugConceptName = drugName; - evidence.relationshipType = relationshipType; - evidence.statisticType = statType; - evidence.statisticValue = statVal; - evidence.uniqueIdentifier = uniqueIdentifier; - evidence.uniqueIdentifierType = uniqueIdentifierType; - - return evidence; - })); - } - - /** - * Retrieves a list of RxNorm ingredients from the concept set and - * determines if we have label evidence for them. - * - * @summary Get Drug Labels For RxNorm Ingredients - * @param sourceKey The source key of the CEM daimon - * @param identifiers The list of RxNorm Ingredients concepts or ancestors - * @return A list of evidence for the drug and HOI - */ - @PostMapping("/{sourceKey}/druglabel") - public ResponseEntity> getDrugIngredientLabel(@PathVariable("sourceKey") String sourceKey, @RequestBody long[] identifiers) { - Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); - return ok(executeGetDrugLabels(identifiers, source)); - } - - /** - * Retrieves a list of evidence for the specified health outcome of interest - * and drug as defined in the key parameter. - * - * @summary Get Evidence For Drug & Health Outcome - * @param sourceKey The source key of the CEM daimon - * @param key The key must be structured as {drugConceptId}-{hoiConceptId} - * @return A list of evidence for the drug and HOI - */ - @GetMapping("/{sourceKey}/drughoi/{key}") - public ResponseEntity> getDrugHoiEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("key") final String key) { - Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); - PreparedStatementRenderer psr = prepareGetDrugHoiEvidence(key, source); - return ok(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { - String evidenceSource = rs.getString("SOURCE_ID"); - String drug = rs.getString("CONCEPT_ID_1"); - String drugName = rs.getString("CONCEPT_ID_1_NAME"); - String hoi = rs.getString("CONCEPT_ID_2"); - String hoiName = rs.getString("CONCEPT_ID_2_NAME"); - String statType = rs.getString("STATISTIC_VALUE_TYPE"); - BigDecimal statVal = rs.getBigDecimal("STATISTIC_VALUE"); - String relationshipType = rs.getString("RELATIONSHIP_ID"); - String uniqueIdentifier = rs.getString("UNIQUE_IDENTIFIER"); - String uniqueIdentifierType = rs.getString("UNIQUE_IDENTIFIER_TYPE"); - - DrugHoiEvidence evidence = new DrugHoiEvidence(); - evidence.evidenceSource = evidenceSource; - evidence.drugConceptId = drug; - evidence.drugConceptName = drugName; - evidence.hoiConceptId = hoi; - evidence.hoiConceptName = hoiName; - evidence.relationshipType = relationshipType; - evidence.statisticType = statType; - evidence.statisticValue = statVal; - evidence.uniqueIdentifier = uniqueIdentifier; - evidence.uniqueIdentifierType = uniqueIdentifierType; - - return evidence; - })); - } - - /** - * Originally provided a roll up of evidence from LAERTES - * - * @summary Depreciated - * @deprecated - * @param sourceKey The source key of the CEM daimon - * @param id The RxNorm drug conceptId - * @param filter Specified the type of rollup level (ingredient, clinical - * drug, branded drug) - * @return A list of evidence rolled up - */ - @GetMapping("/{sourceKey}/drugrollup/{filter}/{id}") - public ResponseEntity> getDrugRollupIngredientEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id, @PathVariable("filter") final String filter) { - String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; - ArrayList evidence = new ArrayList<>(); - HttpHeaders headers = new HttpHeaders(); - headers.add("Warning", "299 - " + warningMessage); - return ResponseEntity.ok().headers(headers).body(evidence); - } - - /** - * Retrieve all evidence from Common Evidence Model (CEM) for a given - * conceptId - * - * @summary Get evidence for a concept - * @param sourceKey The source key of the CEM daimon - * @param id The conceptId of interest - * @return A list of evidence matching the conceptId of interest - */ - @GetMapping("/{sourceKey}/{id}") - public ResponseEntity> getEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id) { - Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); - PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); - return ok(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { - String evidenceSource = rs.getString("SOURCE_ID"); - String drug = rs.getString("CONCEPT_ID_1"); - String drugName = rs.getString("CONCEPT_ID_1_NAME"); - String hoi = rs.getString("CONCEPT_ID_2"); - String hoiName = rs.getString("CONCEPT_ID_2_NAME"); - String statType = rs.getString("STATISTIC_VALUE_TYPE"); - BigDecimal statVal = rs.getBigDecimal("STATISTIC_VALUE"); - String relationshipType = rs.getString("RELATIONSHIP_ID"); - String uniqueIdentifier = rs.getString("UNIQUE_IDENTIFIER"); - String uniqueIdentifierType = rs.getString("UNIQUE_IDENTIFIER_TYPE"); - - Evidence evidence = new Evidence(); - evidence.evidenceSource = evidenceSource; - evidence.drugConceptId = drug; - evidence.drugConceptName = drugName; - evidence.hoiConceptId = hoi; - evidence.hoiConceptName = hoiName; - evidence.relationshipType = relationshipType; - evidence.statisticType = statType; - evidence.statisticValue = statVal; - evidence.uniqueIdentifier = uniqueIdentifier; - evidence.uniqueIdentifierType = uniqueIdentifierType; - - return evidence; - })); - } - - /** - * Originally provided an evidence summary from LAERTES - * - * @summary Depreciated - * @deprecated - * @param sourceKey The source key of the CEM daimon - * @param conditionID The condition conceptId - * @param drugID The drug conceptId - * @param evidenceGroup The evidence group - * @return A summary of evidence - */ - @GetMapping("/{sourceKey}/evidencesummary") - public ResponseEntity> getEvidenceSummaryBySource(@PathVariable("sourceKey") String sourceKey, @RequestParam(required = false) String conditionID, @RequestParam(required = false) String drugID, @RequestParam(required = false) String evidenceGroup) { - String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; - ArrayList evidenceSummary = new ArrayList<>(); - HttpHeaders headers = new HttpHeaders(); - headers.add("Warning", "299 - " + warningMessage); - return ResponseEntity.ok().headers(headers).body(evidenceSummary); - } - - /** - * Originally provided an evidence details from LAERTES - * - * @summary Depreciated - * @deprecated - * @param sourceKey The source key of the CEM daimon - * @param conditionID The condition conceptId - * @param drugID The drug conceptId - * @param evidenceType The evidence type - * @return A list of evidence details - * @throws org.codehaus.jettison.json.JSONException - * @throws java.io.IOException - */ - @GetMapping("/{sourceKey}/evidencedetails") - public ResponseEntity> getEvidenceDetails(@PathVariable("sourceKey") String sourceKey, - @RequestParam(required = false) String conditionID, - @RequestParam(required = false) String drugID, - @RequestParam(required = false) String evidenceType) - throws JSONException, IOException { - String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; - ArrayList evidenceDetails = new ArrayList<>(); - HttpHeaders headers = new HttpHeaders(); - headers.add("Warning", "299 - " + warningMessage); - return ResponseEntity.ok().headers(headers).body(evidenceDetails); - } - - /** - * Originally provided an summary from spontaneous reports from LAERTES - * - * @summary Depreciated - * @deprecated - * @param sourceKey The source key of the CEM daimon - * @param search The search term - * @return A list of spontaneous report summaries - * @throws JSONException - * @throws IOException - */ - @PostMapping("/{sourceKey}/spontaneousreports") - public ResponseEntity> getSpontaneousReports(@PathVariable("sourceKey") String sourceKey, @RequestBody EvidenceSearch search) throws JSONException, IOException { - String warningMessage = "This method will be deprecated in the next release."; - ArrayList returnVal = new ArrayList<>(); - HttpHeaders headers = new HttpHeaders(); - headers.add("Warning", "299 - " + warningMessage); - return ResponseEntity.ok().headers(headers).body(returnVal); - } - - /** - * Originally provided an evidence search from LAERTES - * - * @summary Depreciated - * @deprecated - * @param sourceKey The source key of the CEM daimon - * @param search The search term - * @return A list of evidence - * @throws JSONException - * @throws IOException - */ - @PostMapping("/{sourceKey}/evidencesearch") - public ResponseEntity> evidenceSearch(@PathVariable("sourceKey") String sourceKey, @RequestBody EvidenceSearch search) throws JSONException, IOException { - String warningMessage = "This method will be deprecated in the next release."; - ArrayList returnVal = new ArrayList<>(); - HttpHeaders headers = new HttpHeaders(); - headers.add("Warning", "299 - " + warningMessage); - return ResponseEntity.ok().headers(headers).body(returnVal); - } - - /** - * Originally provided a label evidence search from LAERTES - * - * @summary Depreciated - * @deprecated - * @param sourceKey The source key of the CEM daimon - * @param search The search term - * @return A list of evidence - * @throws JSONException - * @throws IOException - */ - @PostMapping("/{sourceKey}/labelevidence") - public ResponseEntity> labelEvidence(@PathVariable("sourceKey") String sourceKey, @RequestBody EvidenceSearch search) throws JSONException, IOException { - String warningMessage = "This method will be deprecated in the next release."; - ArrayList returnVal = new ArrayList<>(); - HttpHeaders headers = new HttpHeaders(); - headers.add("Warning", "299 - " + warningMessage); - return ResponseEntity.ok().headers(headers).body(returnVal); - } - - /** - * Queues up a negative control generation task to compute negative controls - * using Common Evidence Model (CEM) - * - * @summary Generate negative controls - * @param sourceKey The source key of the CEM daimon - * @param task - The negative control task with parameters - * @return information about the negative control job - * @throws Exception - */ - @PostMapping("/{sourceKey}/negativecontrols") - public ResponseEntity queueNegativeControlsJob(@PathVariable("sourceKey") String sourceKey, @RequestBody NegativeControlTaskParameters task) throws Exception { - if (task == null) { - return ok(null); - } - JobParametersBuilder builder = new JobParametersBuilder(); - - // Get a JDBC template for the OHDSI source repository - // and the source dialect for use when we write the results - // back to the OHDSI repository - JdbcTemplate jdbcTemplate = daoService.getJdbcTemplate(); - task.setJdbcTemplate(jdbcTemplate); - String ohdsiDatasourceSourceDialect = daoService.getSourceDialect(); - task.setSourceDialect(ohdsiDatasourceSourceDialect); - task.setOhdsiSchema(daoService.getOhdsiSchema()); - - // source key comes from the client, we look it up here and hand it off to the tasklet - Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); - // Verify the source has both the evidence & results daimon configured - // and throw an exception if either is missing - String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); - String cemResultsSchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.CEMResults); - if (cemSchema == null) { - throw new RuntimeException("Evidence daimon not configured for source."); - } - if (cemResultsSchema == null) { - throw new RuntimeException("Results daimon not configured for source."); - } - - task.setSource(source); - - if (!StringUtils.isEmpty(task.getJobName())) { - builder.addString("jobName", limitJobParams(task.getJobName())); - } - builder.addString("concept_set_id", ("" + task.getConceptSetId())); - builder.addString("concept_set_name", task.getConceptSetName()); - builder.addString("concept_domain_id", task.getConceptDomainId()); - builder.addString("source_id", ("" + source.getSourceId())); - - // Create a set of parameters to store with the generation info - JSONObject params = new JSONObject(); - params.put("csToInclude", task.getCsToInclude()); - params.put("csToExclude", task.getCsToExclude()); - builder.addString("params", params.toString()); - - // Resolve the concept set expressions for the included and excluded - // concept sets if specified - ConceptSetExpressionQueryBuilder csBuilder = new ConceptSetExpressionQueryBuilder(); - ConceptSetExpression csExpression; - String csSQL = ""; - if (task.getCsToInclude() > 0) { - try { - csExpression = conceptSetService.getConceptSetExpression(task.getCsToInclude()); - csSQL = csBuilder.buildExpressionQuery(csExpression); - } catch (Exception e) { - // log warning would go here if logger was available - } - } - task.setCsToIncludeSQL(csSQL); - csSQL = ""; - if (task.getCsToExclude() > 0) { - try { - csExpression = conceptSetService.getConceptSetExpression(task.getCsToExclude()); - csSQL = csBuilder.buildExpressionQuery(csExpression); - } catch (Exception e) { - // log warning would go here if logger was available - } - } - task.setCsToExcludeSQL(csSQL); - - final JobParameters jobParameters = builder.toJobParameters(); - - NegativeControlTasklet tasklet = new NegativeControlTasklet(task, daoService.getSourceJdbcTemplate(task.getSource()), task.getJdbcTemplate(), - daoService.getTransactionTemplate(), this.conceptSetGenerationInfoRepository, daoService.getSourceDialect()); - - return ok(this.jobTemplate.launchTasklet(NAME, "negativeControlsAnalysisStep", tasklet, jobParameters)); - } - - /** - * Retrieves the negative controls for a concept set - * - * @summary Retrieve negative controls - * @param sourceKey The source key of the CEM daimon - * @param conceptSetId The concept set id - * @return The list of negative controls - */ - @GetMapping("/{sourceKey}/negativecontrols/{conceptsetid}") - public ResponseEntity> getNegativeControls(@PathVariable("sourceKey") String sourceKey, @PathVariable("conceptsetid") int conceptSetId) throws Exception { - Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); - PreparedStatementRenderer psr = this.prepareGetNegativeControls(source, conceptSetId); - final List recs = daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), new NegativeControlMapper()); - return ok(recs); - } - - /** - * Retrieves parameterized SQL used to generate negative controls - * - * @summary Retrieves parameterized SQL used to generate negative controls - * @param sourceKey The source key of the CEM daimon - * @return The list of negative controls - */ - @GetMapping(value = "/{sourceKey}/negativecontrols/sql", produces = MediaType.TEXT_PLAIN_VALUE) - public ResponseEntity getNegativeControlsSqlStatement(@PathVariable("sourceKey") String sourceKey, - @RequestParam(defaultValue = "CONDITION") String conceptDomain, - @RequestParam(defaultValue = "DRUG") String targetDomain, - @RequestParam(defaultValue = "192671") String conceptOfInterest) { - NegativeControlTaskParameters task = new NegativeControlTaskParameters(); - Source source = daoService.getSourceRepository().findBySourceKey(sourceKey); - task.setSource(source); - task.setCsToIncludeSQL(""); - task.setCsToExcludeSQL(""); - task.setConceptDomainId(conceptDomain); - task.setOutcomeOfInterest(targetDomain); - CharSequence csCommaDelimited = ","; - if (conceptOfInterest.contains(csCommaDelimited)) { - task.setConceptsOfInterest(conceptOfInterest.split(",")); - } else { - task.setConceptsOfInterest(new String[]{conceptOfInterest}); - } - return ok(getNegativeControlSql(task)); - } - - @Override - public String getJobName() { - return NAME; - } - - @Override - public String getExecutionFoldingKey() { - return "concept_set_id"; - } - - /** - * Retrieve the SQL used to generate negative controls - * - * @summary Get negative control SQL - * @param task The task containing the parameters for generating negative - * controls - * @return The SQL script for generating negative controls - */ - public static String getNegativeControlSql(NegativeControlTaskParameters task) { - StringBuilder sb = new StringBuilder(); - String resourceRoot = "/resources/evidence/sql/negativecontrols/"; - Source source = task.getSource(); - String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); - String cemResultsSchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.CEMResults); - String vocabularySchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.Vocabulary); - if (vocabularySchema == null) { - vocabularySchema = cemSchema; - } - String translatedSchema = task.getTranslatedSchema(); - if (translatedSchema == null) { - translatedSchema = cemSchema; - } - - String csToExcludeSQL = SqlRender.renderSql(task.getCsToExcludeSQL(), - new String[]{"vocabulary_database_schema"}, - new String[]{vocabularySchema} - ); - String csToIncludeSQL = SqlRender.renderSql(task.getCsToIncludeSQL(), - new String[]{"vocabulary_database_schema"}, - new String[]{vocabularySchema} - ); - - String outcomeOfInterest = task.getOutcomeOfInterest().toLowerCase(); - String conceptsOfInterest = JoinArray(task.getConceptsOfInterest()); - String csToInclude = String.valueOf(task.getCsToInclude()); - String csToExclude = String.valueOf(task.getCsToExclude()); - String medlineWinnenburgTable = translatedSchema + ".MEDLINE_WINNENBURG"; - String splicerTable = translatedSchema + ".SPLICER"; - String aeolusTable = translatedSchema + ".AEOLUS"; - String conceptsToExcludeData = "#NC_EXCLUDED_CONCEPTS"; - String conceptsToIncludeData = "#NC_INCLUDED_CONCEPTS"; - String broadConceptsData = cemSchema + ".NC_LU_BROAD_CONCEPTS"; - String drugInducedConditionsData = cemSchema + ".NC_LU_DRUG_INDUCED_CONDITIONS"; - String pregnancyConditionData = cemSchema + ".NC_LU_PREGNANCY_CONDITIONS"; - - String[] params = new String[]{"outcomeOfInterest", "conceptsOfInterest", "vocabulary", "cem_schema", "cem_results_schema", "translatedSchema"}; - String[] values = new String[]{outcomeOfInterest, conceptsOfInterest, vocabularySchema, cemSchema, cemResultsSchema, translatedSchema}; - - String sqlFile = "findConceptUniverse.sql"; - sb.append("-- ").append(sqlFile).append("\n\n"); - String sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, params, values); - sb.append(sql + "\n\n"); - - sqlFile = "findDrugIndications.sql"; - sb.append("-- ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, params, values); - sb.append(sql + "\n\n"); - - sqlFile = "findConcepts.sql"; - sb.append("-- User excluded - ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, - ArrayUtils.addAll(params, new String[]{"storeData", "conceptSetId", "conceptSetExpression"}), - ArrayUtils.addAll(values, new String[]{conceptsToExcludeData, csToExclude, csToExcludeSQL}) - ); - sb.append(sql + "\n\n"); - - sqlFile = "findConcepts.sql"; - sb.append("-- User included - ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, - ArrayUtils.addAll(params, new String[]{"storeData", "conceptSetId", "conceptSetExpression"}), - ArrayUtils.addAll(values, new String[]{conceptsToIncludeData, csToInclude, csToIncludeSQL}) - ); - sb.append(sql + "\n\n"); - - sqlFile = "pullEvidencePrep.sql"; - sb.append("-- ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, params, values); - sb.append(sql + "\n\n"); - - sqlFile = "pullEvidence.sql"; - sb.append("-- MEDLINE_WINNENBURG -- ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, - ArrayUtils.addAll(params, new String[]{"adeType", "adeData"}), - ArrayUtils.addAll(values, new String[]{"MEDLINE_WINNENBURG", medlineWinnenburgTable}) - ); - sb.append(sql + "\n\n"); - - sqlFile = "pullEvidence.sql"; - sb.append("-- SPLICER -- ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, - ArrayUtils.addAll(params, new String[]{"adeType", "adeData"}), - ArrayUtils.addAll(values, new String[]{"SPLICER", splicerTable}) - ); - sb.append(sql + "\n\n"); - - sqlFile = "pullEvidence.sql"; - sb.append("-- AEOLUS -- ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, - ArrayUtils.addAll(params, new String[]{"adeType", "adeData"}), - ArrayUtils.addAll(values, new String[]{"AEOLUS", aeolusTable}) - ); - sb.append(sql + "\n\n"); - - sqlFile = "pullEvidencePost.sql"; - sb.append("-- ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, params, values); - sb.append(sql + "\n\n"); - - sqlFile = "summarizeEvidence.sql"; - sb.append("-- ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, - ArrayUtils.addAll(params, new String[]{"broadConceptsData", "drugInducedConditionsData", "pregnancyConditionData", "conceptsToExclude", "conceptsToInclude"}), - ArrayUtils.addAll(values, new String[]{broadConceptsData, drugInducedConditionsData, pregnancyConditionData, conceptsToExcludeData, conceptsToIncludeData}) - ); - sb.append(sql + "\n\n"); - - sqlFile = "optimizeEvidence.sql"; - sb.append("-- ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, params, values); - sb.append(sql + "\n\n"); - - sqlFile = "deleteJobResults.sql"; - sb.append("-- ").append(sqlFile).append("\n\n"); - sql = getJobResultsDeleteStatementSql(cemResultsSchema, task.getConceptSetId()); - sb.append(sql + "\n\n"); - - sqlFile = "exportNegativeControls.sql"; - sb.append("-- ").append(sqlFile).append("\n\n"); - sql = ResourceHelper.GetResourceAsString(resourceRoot + sqlFile); - sql = SqlRender.renderSql(sql, - ArrayUtils.addAll(params, new String[]{"conceptSetId"}), - ArrayUtils.addAll(values, new String[]{Integer.toString(task.getConceptSetId())}) - ); - sb.append(sql + "\n\n"); - - sql = SqlTranslate.translateSql(sb.toString(), source.getSourceDialect()); - - return sql; - } - - /** - * SQL to delete negative controls job results - * - * @summary SQL to delete negative controls job results - * @param cemResultsSchema The CEM results schema - * @param conceptSetId The concept set ID - * @return The SQL statement - */ - public static String getJobResultsDeleteStatementSql(String cemResultsSchema, int conceptSetId) { - String sql = ResourceHelper.GetResourceAsString("/resources/evidence/sql/negativecontrols/deleteJobResults.sql"); - sql = SqlRender.renderSql(sql, - (new String[]{"cem_results_schema", "conceptSetId"}), - (new String[]{cemResultsSchema, Integer.toString(conceptSetId)}) - ); - return sql; - } - - /** - * SQL to insert negative controls - * - * @summary SQL to insert negative controls - * @param task The negative control task and parameters - * @return The SQL statement - */ - public static String getNegativeControlInsertStatementSql(NegativeControlTaskParameters task) { - String sql = ResourceHelper.GetResourceAsString("/resources/evidence/sql/negativecontrols/insertNegativeControls.sql"); - sql = SqlRender.renderSql(sql, new String[]{"ohdsiSchema"}, new String[]{task.getOhdsiSchema()}); - sql = SqlTranslate.translateSql(sql, task.getSourceDialect()); - - return sql; - } - - protected PreparedStatementRenderer prepareExecuteGetDrugLabels(long[] identifiers, Source source) { - String sqlPath = "/resources/evidence/sql/getDrugLabelForIngredients.sql"; - String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); - String vocabularySchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.Vocabulary); - if (vocabularySchema == null) { - vocabularySchema = cemSchema; - } - String[] tableQualifierNames = new String[]{"cem_schema", "vocabularySchema"}; - String[] tableQualifierValues = new String[]{cemSchema, vocabularySchema}; - return new PreparedStatementRenderer(source, sqlPath, tableQualifierNames, tableQualifierValues, "conceptIds", identifiers); - } - - /** - * Get the SQL for obtaining product label evidence from a set of RxNorm - * Ingredients - * - * @summary SQL for obtaining product label evidence - * @param identifiers The list of RxNorm Ingredient conceptIds - * @param source The source that contains the CEM daimon - * @return A prepared SQL statement - */ - protected Collection executeGetDrugLabels(long[] identifiers, Source source) { - Collection info = new ArrayList<>(); - if (identifiers.length == 0) { - return info; - } else { - int parameterLimit = PreparedSqlRender.getParameterLimit(source); - if (parameterLimit > 0 && identifiers.length > parameterLimit) { - info = executeGetDrugLabels(Arrays.copyOfRange(identifiers, parameterLimit, identifiers.length), source); - identifiers = Arrays.copyOfRange(identifiers, 0, parameterLimit); - } - PreparedStatementRenderer psr = prepareExecuteGetDrugLabels(identifiers, source); - info.addAll(daoService.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), this.drugLabelRowMapper)); - return info; - } - } - - /** - * Get the SQL for obtaining evidence for a drug/condition pair for source - * ids - * - * @summary SQL for obtaining evidence for a drug/hoi pair by source - * @param source The source that contains the CEM daimon - * @return A prepared SQL statement - */ - protected String getDrugHoiEvidenceSQL(Source source, DrugConditionSourceSearchParams searchParams) { - String sqlPath = "/resources/evidence/sql/getDrugConditionPairBySourceId.sql"; - String sql = ResourceHelper.GetResourceAsString(sqlPath); - String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); - String vocabularySchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.Vocabulary); - if (vocabularySchema == null) { - vocabularySchema = cemSchema; - } - String[] params = new String[]{"cem_schema", "vocabularySchema", "targetDomain", "sourceIdList", "drugList", "conditionList"}; - String[] values = new String[]{cemSchema, vocabularySchema, searchParams.targetDomain.toUpperCase(), searchParams.getSourceIds(), searchParams.getDrugConceptIds(), searchParams.getConditionConceptIds()}; - sql = SqlRender.renderSql(sql, params, values); - sql = SqlTranslate.translateSql(sql, source.getSourceDialect()); - return sql; - } - - /** - * Get the SQL for obtaining evidence for a drug/hoi combination - * - * @summary SQL for obtaining evidence for a drug/hoi combination - * @param key The drug-hoi conceptId pair - * @param source The source that contains the CEM daimon - * @return A prepared SQL statement - */ - protected PreparedStatementRenderer prepareGetDrugHoiEvidence(final String key, Source source) { - String[] par = key.split("-"); - String drug_id = par[0]; - String hoi_id = par[1]; - String sqlPath = "/resources/evidence/sql/getDrugHoiEvidence.sql"; - String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); - String vocabularySchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.Vocabulary); - if (vocabularySchema == null) { - vocabularySchema = cemSchema; - } - String[] tableQualifierNames = new String[]{"cem_schema", "vocabularySchema"}; - String[] tableQualifierValues = new String[]{cemSchema, vocabularySchema}; - String[] names = new String[]{"drug_id", "hoi_id"}; - Object[] values = new Integer[]{Integer.parseInt(drug_id), Integer.parseInt(hoi_id)}; - return new PreparedStatementRenderer(source, sqlPath, tableQualifierNames, tableQualifierValues, names, values); - } - - /** - * Get the SQL for obtaining evidence for a concept - * - * @summary SQL for obtaining evidence for a concept - * @param source The source that contains the CEM daimon - * @param conceptId The conceptId of interest - * @return A prepared SQL statement - */ - protected PreparedStatementRenderer prepareGetEvidenceForConcept(Source source, Long conceptId) { - String sqlPath = "/resources/evidence/sql/getEvidenceForConcept.sql"; - String cemSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEM); - String vocabularySchema = source.getTableQualifierOrNull(SourceDaimon.DaimonType.Vocabulary); - if (vocabularySchema == null) { - vocabularySchema = cemSchema; - } - String[] tableQualifierNames = new String[]{"cem_schema", "vocabularySchema"}; - String[] tableQualifierValues = new String[]{cemSchema, vocabularySchema}; - String[] names = new String[]{"id"}; - Object[] values = new Long[]{conceptId}; - return new PreparedStatementRenderer(source, sqlPath, tableQualifierNames, tableQualifierValues, names, values); - } - - /** - * Get the SQL for obtaining negative controls for the concept set specified - * - * @summary SQL for obtaining negative controls - * @param source The source that contains the CEM daimon - * @param conceptSetId The conceptSetId associated to the negative controls - * @return A prepared SQL statement - */ - protected PreparedStatementRenderer prepareGetNegativeControls(Source source, int conceptSetId) { - String sqlPath = "/resources/evidence/sql/negativecontrols/getNegativeControls.sql"; - String cemResultsSchema = source.getTableQualifier(SourceDaimon.DaimonType.CEMResults); - String[] tableQualifierNames = new String[]{"cem_results_schema"}; - String[] tableQualifierValues = new String[]{cemResultsSchema}; - String[] names = new String[]{"conceptSetId"}; - Object[] values = new Object[]{conceptSetId}; - return new PreparedStatementRenderer(source, sqlPath, tableQualifierNames, tableQualifierValues, names, values); - } - - private static String JoinArray(final String[] array) { - String result = ""; - - for (int i = 0; i < array.length; i++) { - if (i > 0) { - result += ","; - } - - result += "'" + array[i] + "'"; - } - - return result; - } - - private static String limitJobParams(String param) { - if (param.length() >= 250) { - return param.substring(0, 245) + "..."; - } - return param; - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java b/src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java deleted file mode 100644 index 674d2ceba4..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/MigrationUtils.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.ohdsi.webapi.mvc; - -import org.springframework.http.MediaType; - -/** - * Utility methods to assist with Jersey to Spring MVC migration. - */ -public class MigrationUtils { - - /** - * Convert JAX-RS MediaType constant to Spring media type string - */ - public static String toSpringMediaType(String jaxrsMediaType) { - // JAX-RS uses constants like MediaType.APPLICATION_JSON_VALUE - // Spring uses constants like MediaType.APPLICATION_JSON_VALUE - // This is mainly for documentation/reference - return switch (jaxrsMediaType) { - case "application/json" -> org.springframework.http.MediaType.APPLICATION_JSON_VALUE; - case "application/xml" -> org.springframework.http.MediaType.APPLICATION_XML_VALUE; - case "text/plain" -> org.springframework.http.MediaType.TEXT_PLAIN_VALUE; - case "text/html" -> org.springframework.http.MediaType.TEXT_HTML_VALUE; - case "multipart/form-data" -> org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; - case "application/x-www-form-urlencoded" -> org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; - default -> jaxrsMediaType; - }; - } - - /** - * Annotation mapping reference for migration - */ - public static class AnnotationMapping { - /* - * JAX-RS to Spring MVC Annotation Mapping Guide: - * - * @Path("/api") → @RequestMapping("/WebAPI/api") - * @GET → @GetMapping - * @POST → @PostMapping - * @PUT → @PutMapping - * @DELETE → @DeleteMapping - * @PathParam("id") → @PathVariable("id") - * @QueryParam("name") → @RequestParam(value="name") - * @FormParam("field") → @RequestParam("field") - * @FormDataParam("file") → @RequestPart("file") // for MultipartFile - * @Produces(APPLICATION_JSON) → produces = APPLICATION_JSON_VALUE - * @Consumes(APPLICATION_JSON) → consumes = APPLICATION_JSON_VALUE - * Response → ResponseEntity - * Response.ok(entity) → ResponseEntity.ok(entity) - * Response.status(404) → ResponseEntity.status(404) or ResponseEntity.notFound() - * - * Provider Classes: - * @Provider + ExceptionMapper → @ControllerAdvice + @ExceptionHandler - * @Provider + ContainerRequestFilter → HandlerInterceptor - * @Provider + MessageBodyWriter → HttpMessageConverter - */ - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/NotificationMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/NotificationMvcController.java deleted file mode 100644 index 1abfa0e132..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/NotificationMvcController.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.ohdsi.webapi.mvc; - -import org.apache.commons.lang3.StringUtils; -import org.ohdsi.webapi.job.JobExecutionInfo; -import org.ohdsi.webapi.job.JobExecutionResource; -import org.ohdsi.webapi.job.NotificationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.stream.Collectors; - -/** - * REST Services related to working with the system notifications - * - * @summary Notifications - */ -@RestController -@RequestMapping("/notifications") -@Transactional -public class NotificationMvcController extends AbstractMvcController { - private static final Logger log = LoggerFactory.getLogger(NotificationMvcController.class); - - private final NotificationService service; - private final GenericConversionService conversionService; - - public NotificationMvcController(final NotificationService service, @Qualifier("conversionService") GenericConversionService conversionService) { - this.service = service; - this.conversionService = conversionService; - } - - /** - * Get the list of notifications - * - * @summary Get all notifications - * @param hideStatuses Used to filter statuses - passes as a comma-delimited - * list - * @param refreshJobs Boolean - when true, it will refresh the cache - * of notifications - * @return - */ - @GetMapping("/") - @Transactional(readOnly = true) - public ResponseEntity> list( - @RequestParam(value = "hide_statuses", required = false) String hideStatuses, - @RequestParam(value = "refreshJobs", defaultValue = "FALSE") Boolean refreshJobs) { - List statuses = new ArrayList<>(); - if (StringUtils.isNotEmpty(hideStatuses)) { - for (String status : hideStatuses.split(",")) { - try { - statuses.add(BatchStatus.valueOf(status)); - } catch (IllegalArgumentException e) { - log.warn("Invalid argument passed as batch status: {}", status); - } - } - } - List executionInfos; - if (refreshJobs) { - executionInfos = service.findRefreshCacheLastJobs(); - } else { - executionInfos = service.findLastJobs(statuses); - } - return ok(executionInfos.stream().map(this::toDTO).collect(Collectors.toList())); - } - - /** - * Gets the date when notifications were last viewed - * - * @summary Get notification last viewed date - * @return The date when notifications were last viewed - */ - @GetMapping("/viewed") - @Transactional(readOnly = true) - public ResponseEntity getLastViewedTime() { - try { - return ok(service.getLastViewedTime()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Sets the date when notifications were last viewed - * - * @summary Set notification last viewed date - * @param stamp - */ - @PostMapping("/viewed") - public ResponseEntity setLastViewedTime(@RequestBody Date stamp) { - try { - service.setLastViewedTime(stamp); - return ok(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private JobExecutionResource toDTO(JobExecutionInfo entity) { - return conversionService.convert(entity, JobExecutionResource.class); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/TagMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/TagMvcController.java deleted file mode 100644 index 5b78e419a3..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/TagMvcController.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.ohdsi.webapi.mvc; - -import org.apache.commons.lang3.StringUtils; -import org.ohdsi.webapi.tag.TagGroupService; -import org.ohdsi.webapi.tag.TagService; -import org.ohdsi.webapi.tag.dto.AssignmentPermissionsDTO; -import org.ohdsi.webapi.tag.dto.TagDTO; -import org.ohdsi.webapi.tag.dto.TagGroupSubscriptionDTO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Collections; -import java.util.List; - -@RestController -@RequestMapping("/tag") -public class TagMvcController extends AbstractMvcController { - private final TagService tagService; - private final TagGroupService tagGroupService; - - @Autowired - public TagMvcController(TagService tagService, - TagGroupService tagGroupService) { - this.tagService = tagService; - this.tagGroupService = tagGroupService; - } - - /** - * Creates a tag. - * - * @param dto - * @return - */ - @PostMapping("/") - public ResponseEntity create(@RequestBody final TagDTO dto) { - return ok(tagService.create(dto)); - } - - /** - * Returns list of tags, which names contain a provided substring. - * - * @summary Search tags by name part - * @param namePart - * @return - */ - @GetMapping("/search") - public ResponseEntity> search(@RequestParam("namePart") String namePart) { - if (StringUtils.isBlank(namePart)) { - return ok(Collections.emptyList()); - } - return ok(tagService.listInfoDTO(namePart)); - } - - /** - * Returns list of all tags. - * - * @return - */ - @GetMapping("/") - public ResponseEntity> list() { - return ok(tagService.listInfoDTO()); - } - - /** - * Updates tag with ID={id}. - * - * @param id - * @param dto - * @return - */ - @PutMapping("/{id}") - public ResponseEntity update(@PathVariable("id") final Integer id, @RequestBody final TagDTO dto) { - return ok(tagService.update(id, dto)); - } - - /** - * Return tag by ID. - * - * @param id - * @return - */ - @GetMapping("/{id}") - public ResponseEntity get(@PathVariable("id") final Integer id) { - return ok(tagService.getDTOById(id)); - } - - /** - * Deletes tag with ID={id}. - * - * @param id - */ - @DeleteMapping("/{id}") - public ResponseEntity delete(@PathVariable("id") final Integer id) { - tagService.delete(id); - return ok(); - } - - /** - * Assignes group of tags to groups of assets. - * - * @param dto - * @return - */ - @PostMapping("/multiAssign") - public ResponseEntity assignGroup(@RequestBody final TagGroupSubscriptionDTO dto) { - tagGroupService.assignGroup(dto); - return ok(); - } - - /** - * Unassignes group of tags from groups of assets. - * - * @param dto - * @return - */ - @PostMapping("/multiUnassign") - public ResponseEntity unassignGroup(@RequestBody final TagGroupSubscriptionDTO dto) { - tagGroupService.unassignGroup(dto); - return ok(); - } - - /** - * Tags assignment permissions for current user - * - * @return - */ - @GetMapping("/assignmentPermissions") - public ResponseEntity assignmentPermissions() { - return ok(tagService.getAssignmentPermissions()); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/ToolMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/ToolMvcController.java deleted file mode 100644 index 6ed7ebfac9..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/ToolMvcController.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.ohdsi.webapi.mvc; - -import org.ohdsi.webapi.tool.ToolServiceImpl; -import org.ohdsi.webapi.tool.dto.ToolDTO; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/tool") -public class ToolMvcController extends AbstractMvcController { - private final ToolServiceImpl service; - - public ToolMvcController(ToolServiceImpl service) { - this.service = service; - } - - @GetMapping("") - public ResponseEntity> getTools() { - return ok(service.getTools()); - } - - @GetMapping("/{id}") - public ResponseEntity getToolById(@PathVariable("id") Integer id) { - return ok(service.getById(id)); - } - - @PostMapping("") - public ResponseEntity createTool(@RequestBody ToolDTO dto) { - return ok(service.saveTool(dto)); - } - - @DeleteMapping("/{id}") - public ResponseEntity delete(@PathVariable("id") Integer id) { - service.delete(id); - return ok(); - } - - @PutMapping("") - public ResponseEntity updateTool(@RequestBody ToolDTO toolDTO) { - return ok(service.saveTool(toolDTO)); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/ActivityMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/ActivityMvcController.java deleted file mode 100644 index 32973a899a..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/ActivityMvcController.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import org.ohdsi.webapi.activity.Tracker; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * Spring MVC version of ActivityService - * - * Migration Status: Replaces /service/ActivityService.java (Jersey) - * Endpoints: 1 GET endpoint - * Complexity: Simple - deprecated, read-only - * - * @deprecated Example REST service - will be deprecated in a future release - */ -@RestController -@RequestMapping("/activity") -@Deprecated -public class ActivityMvcController extends AbstractMvcController { - - /** - * Get latest activity - * - * Jersey: GET /WebAPI/activity/latest - * Spring MVC: GET /WebAPI/v2/activity/latest - * - * @deprecated DO NOT USE - will be removed in future release - */ - @GetMapping(value = "/latest", produces = MediaType.APPLICATION_JSON_VALUE) - @Deprecated - public ResponseEntity getLatestActivity() { - return ok(Tracker.getActivity()); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CDMResultsMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CDMResultsMvcController.java deleted file mode 100644 index bd0dd86100..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/CDMResultsMvcController.java +++ /dev/null @@ -1,268 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import org.ohdsi.webapi.job.JobExecutionResource; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.report.CDMDashboard; -import org.ohdsi.webapi.report.CDMDataDensity; -import org.ohdsi.webapi.report.CDMDeath; -import org.ohdsi.webapi.report.CDMObservationPeriod; -import org.ohdsi.webapi.report.CDMPersonSummary; -import org.ohdsi.webapi.service.CDMResultsService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.AbstractMap.SimpleEntry; -import java.util.List; - -/** - * Spring MVC version of CDMResultsService - * - * Migration Status: Replaces /service/CDMResultsService.java (Jersey) - * Endpoints: 11 endpoints (9 GET, 2 POST) - * Complexity: Medium - delegates to existing Jersey service for business logic - * - * This controller delegates to the existing Jersey CDMResultsService to preserve - * all business logic, caching, and job execution functionality. The Jersey service - * remains as a @Component for dependency injection but endpoints are now exposed - * via this Spring MVC controller. - */ -@RestController -@RequestMapping("/cdmresults") -public class CDMResultsMvcController extends AbstractMvcController { - - @Autowired - private CDMResultsService cdmResultsService; - - /** - * Get the record count and descendant record count for one or more concepts in a single CDM database - * - *

- * This POST request accepts a json array containing one or more concept IDs. (e.g. [201826, 437827]) - *

- * - * @param sourceKey The unique identifier for a CDM source (e.g. SYNPUF5PCT) - * @param identifiers List of concept IDs - * @return A list of concept IDs with their record counts and descendant record counts - * - *

- * [ - * { - * "201826": [ - * 612861, - * 653173 - * ] - * }, - * { - * "437827": [ - * 224421, - * 224421 - * ] - * } - * ] - *

- * - * Jersey: POST /WebAPI/cdmresults/{sourceKey}/conceptRecordCount - * Spring MVC: POST /WebAPI/v2/cdmresults/{sourceKey}/conceptRecordCount - */ - @PostMapping(value = "/{sourceKey}/conceptRecordCount", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity>>> getConceptRecordCount( - @PathVariable("sourceKey") String sourceKey, - @RequestBody List identifiers) { - List>> result = cdmResultsService.getConceptRecordCount(sourceKey, identifiers); - return ok(result); - } - - /** - * Queries for dashboard report for the sourceKey - * - * Jersey: GET /WebAPI/cdmresults/{sourceKey}/dashboard - * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/dashboard - * - * @param sourceKey The source key - * @return CDMDashboard - */ - @GetMapping(value = "/{sourceKey}/dashboard", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getDashboard(@PathVariable("sourceKey") String sourceKey) { - CDMDashboard dashboard = cdmResultsService.getDashboard(sourceKey); - return ok(dashboard); - } - - /** - * Queries for person report for the sourceKey - * - * Jersey: GET /WebAPI/cdmresults/{sourceKey}/person - * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/person - * - * @param sourceKey The source key - * @return CDMPersonSummary - */ - @GetMapping(value = "/{sourceKey}/person", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getPerson(@PathVariable("sourceKey") String sourceKey) { - CDMPersonSummary person = cdmResultsService.getPerson(sourceKey); - return ok(person); - } - - /** - * Warm the results cache for a selected source - * - * Jersey: GET /WebAPI/cdmresults/{sourceKey}/warmCache - * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/warmCache - * - * @summary Warm cache for source key - * @param sourceKey The source key - * @return The job execution information - */ - @GetMapping(value = "/{sourceKey}/warmCache", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity warmCache(@PathVariable("sourceKey") String sourceKey) { - JobExecutionResource jobExecution = cdmResultsService.warmCache(sourceKey); - return ok(jobExecution); - } - - /** - * Refresh the results cache for a selected source - * - * Jersey: GET /WebAPI/cdmresults/{sourceKey}/refreshCache - * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/refreshCache - * - * @summary Refresh results cache - * @param sourceKey The source key - * @return The job execution resource - */ - @GetMapping(value = "/{sourceKey}/refreshCache", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity refreshCache(@PathVariable("sourceKey") String sourceKey) { - JobExecutionResource jobExecution = cdmResultsService.refreshCache(sourceKey); - return ok(jobExecution); - } - - /** - * Clear the cdm_cache and achilles_cache for a specific source - * - * Jersey: POST /WebAPI/cdmresults/{sourceKey}/clearCache - * Spring MVC: POST /WebAPI/v2/cdmresults/{sourceKey}/clearCache - * - * @summary Clear the cdm_cache and achilles_cache for a source - * @param sourceKey The source key - * @return void - */ - @PostMapping(value = "/{sourceKey}/clearCache") - public ResponseEntity clearCacheForSource(@PathVariable("sourceKey") String sourceKey) { - if (!isSecured() || !isAdmin()) { - return forbidden(); - } - cdmResultsService.clearCacheForSource(sourceKey); - return ok(); - } - - /** - * Clear the cdm_cache and achilles_cache for all sources - * - * Jersey: POST /WebAPI/cdmresults/clearCache - * Spring MVC: POST /WebAPI/v2/cdmresults/clearCache - * - * @summary Clear the cdm_cache and achilles_cache for all sources - * @return void - */ - @PostMapping(value = "/clearCache") - public ResponseEntity clearCache() { - if (!isSecured() || !isAdmin()) { - return forbidden(); - } - cdmResultsService.clearCache(); - return ok(); - } - - /** - * Queries for data density report for the given sourceKey - * - * Jersey: GET /WebAPI/cdmresults/{sourceKey}/datadensity - * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/datadensity - * - * @param sourceKey The source key - * @return CDMDataDensity - */ - @GetMapping(value = "/{sourceKey}/datadensity", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getDataDensity(@PathVariable("sourceKey") String sourceKey) { - CDMDataDensity dataDensity = cdmResultsService.getDataDensity(sourceKey); - return ok(dataDensity); - } - - /** - * Queries for death report for the given sourceKey - * - * Jersey: GET /WebAPI/cdmresults/{sourceKey}/death - * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/death - * - * @param sourceKey The source key - * @return CDMDeath - */ - @GetMapping(value = "/{sourceKey}/death", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getDeath(@PathVariable("sourceKey") String sourceKey) { - CDMDeath death = cdmResultsService.getDeath(sourceKey); - return ok(death); - } - - /** - * Queries for observation period report for the given sourceKey - * - * Jersey: GET /WebAPI/cdmresults/{sourceKey}/observationPeriod - * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/observationPeriod - * - * @param sourceKey The source key - * @return CDMObservationPeriod - */ - @GetMapping(value = "/{sourceKey}/observationPeriod", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getObservationPeriod(@PathVariable("sourceKey") String sourceKey) { - CDMObservationPeriod observationPeriod = cdmResultsService.getObservationPeriod(sourceKey); - return ok(observationPeriod); - } - - /** - * Queries for domain treemap results - * - * Jersey: GET /WebAPI/cdmresults/{sourceKey}/{domain}/ - * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/{domain}/ - * - * @param sourceKey The source key - * @param domain The domain - * @return List - */ - @GetMapping(value = "/{sourceKey}/{domain}/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getTreemap( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("domain") String domain) { - ArrayNode treemap = cdmResultsService.getTreemap(domain, sourceKey); - return ok(treemap); - } - - /** - * Queries for drilldown results - * - * Jersey: GET /WebAPI/cdmresults/{sourceKey}/{domain}/{conceptId} - * Spring MVC: GET /WebAPI/v2/cdmresults/{sourceKey}/{domain}/{conceptId} - * - * @param sourceKey The source key - * @param domain The domain for the drilldown - * @param conceptId The concept ID - * @return The JSON results - */ - @GetMapping(value = "/{sourceKey}/{domain}/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getDrilldown( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("domain") String domain, - @PathVariable("conceptId") int conceptId) { - JsonNode drilldown = cdmResultsService.getDrilldown(domain, conceptId, sourceKey); - return ok(drilldown); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortAnalysisMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortAnalysisMvcController.java deleted file mode 100644 index 0aad050678..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortAnalysisMvcController.java +++ /dev/null @@ -1,127 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import org.ohdsi.webapi.cohortanalysis.*; -import org.ohdsi.webapi.job.JobExecutionResource; -import org.ohdsi.webapi.model.results.Analysis; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -/** - * Spring MVC version of CohortAnalysisService - * - * Migration Status: Replaces /service/CohortAnalysisService.java (Jersey) - * Endpoints: 5 endpoints (3 GET, 2 POST) - * Complexity: Medium - business logic delegated to original service - */ -@RestController -@RequestMapping("/cohortanalysis") -public class CohortAnalysisMvcController extends AbstractMvcController { - - private final org.ohdsi.webapi.service.CohortAnalysisService cohortAnalysisService; - - public CohortAnalysisMvcController(org.ohdsi.webapi.service.CohortAnalysisService cohortAnalysisService) { - this.cohortAnalysisService = cohortAnalysisService; - } - - /** - * Returns all cohort analyses in the WebAPI database - * - * Jersey: GET /WebAPI/cohortanalysis/ - * Spring MVC: GET /WebAPI/v2/cohortanalysis - * - * @summary Get all cohort analyses - * @return List of all cohort analyses - */ - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCohortAnalyses() { - List analyses = cohortAnalysisService.getCohortAnalyses(); - return ok(analyses); - } - - /** - * Returns all cohort analyses in the WebAPI database - * for the given cohort_definition_id - * - * Jersey: GET /WebAPI/cohortanalysis/{id} - * Spring MVC: GET /WebAPI/v2/cohortanalysis/{id} - * - * @summary Get cohort analyses by cohort ID - * @param id The cohort definition identifier - * @return List of all cohort analyses and their statuses - * for the given cohort_definition_id - */ - @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCohortAnalysesForCohortDefinition(@PathVariable("id") int id) { - List analyses = cohortAnalysisService.getCohortAnalysesForCohortDefinition(id); - return ok(analyses); - } - - /** - * Returns the summary for the cohort - * - * Jersey: GET /WebAPI/cohortanalysis/{id}/summary - * Spring MVC: GET /WebAPI/v2/cohortanalysis/{id}/summary - * - * @summary Cohort analysis summary - * @param id - the cohort_definition id - * @return Summary which includes the base cohort_definition, the cohort analyses list and their - * statuses for this cohort, and a base set of common cohort results that may or may not - * yet have been ran - */ - @GetMapping(value = "/{id}/summary", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortSummary(@PathVariable("id") int id) { - CohortSummary summary = cohortAnalysisService.getCohortSummary(id); - return ok(summary); - } - - /** - * Generates a preview of the cohort analysis SQL used to run - * the Cohort Analysis Job - * - * Jersey: POST /WebAPI/cohortanalysis/preview - * Spring MVC: POST /WebAPI/v2/cohortanalysis/preview - * - * @summary Cohort analysis SQL preview - * @param task - the CohortAnalysisTask, be sure to have a least one - * analysis_id and one cohort_definition id - * @return - SQL for the given CohortAnalysisTask translated and rendered to - * the current dialect - */ - @PostMapping( - value = "/preview", - produces = MediaType.TEXT_PLAIN_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity getRunCohortAnalysisSql(@RequestBody CohortAnalysisTask task) { - String sql = cohortAnalysisService.getRunCohortAnalysisSql(task); - return ok(sql); - } - - /** - * Queues up a cohort analysis task, that generates and translates SQL for the - * given cohort definitions, analysis ids and concept ids - * - * Jersey: POST /WebAPI/cohortanalysis/ - * Spring MVC: POST /WebAPI/v2/cohortanalysis - * - * @summary Queue cohort analysis job - * @param task The cohort analysis task to be ran - * @return information about the Cohort Analysis Job - * @throws Exception - */ - @PostMapping( - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity queueCohortAnalysisJob(@RequestBody CohortAnalysisTask task) throws Exception { - JobExecutionResource jobExecution = cohortAnalysisService.queueCohortAnalysisJob(task); - if (jobExecution == null) { - return notFound(); - } - return ok(jobExecution); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortDefinitionMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortDefinitionMvcController.java deleted file mode 100644 index 7078a5def7..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortDefinitionMvcController.java +++ /dev/null @@ -1,1134 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.lang3.StringUtils; -import org.commonmark.Extension; -import org.commonmark.ext.gfm.tables.TablesExtension; -import org.commonmark.node.Node; -import org.commonmark.parser.Parser; -import org.commonmark.renderer.html.HtmlRenderer; -import org.hibernate.Hibernate; -import org.ohdsi.analysis.Utils; -import org.ohdsi.circe.check.Checker; -import org.ohdsi.circe.cohortdefinition.CohortExpression; -import org.ohdsi.circe.cohortdefinition.CohortExpressionQueryBuilder; -import org.ohdsi.circe.cohortdefinition.ConceptSet; -import org.ohdsi.circe.cohortdefinition.printfriendly.MarkdownRender; -import org.ohdsi.sql.SqlRender; -import org.ohdsi.webapi.Constants; -import org.ohdsi.webapi.check.CheckResult; -import org.ohdsi.webapi.check.checker.cohort.CohortChecker; -import org.ohdsi.webapi.check.warning.Warning; -import org.ohdsi.webapi.check.warning.WarningUtils; -import org.ohdsi.webapi.cohortdefinition.*; -import org.ohdsi.webapi.cohortdefinition.dto.*; -import org.ohdsi.webapi.cohortdefinition.event.CohortDefinitionChangedEvent; -import org.ohdsi.webapi.common.SourceMapKey; -import org.ohdsi.webapi.common.generation.GenerateSqlResult; -import org.ohdsi.webapi.common.sensitiveinfo.CohortGenerationSensitiveInfoService; -import org.ohdsi.webapi.conceptset.ConceptSetExport; -import org.ohdsi.webapi.job.JobExecutionResource; -import org.ohdsi.webapi.job.JobTemplate; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.security.PermissionService; -import org.ohdsi.webapi.service.*; -import org.ohdsi.webapi.service.dto.CheckResultDTO; -import org.ohdsi.webapi.shiro.Entities.UserEntity; -import org.ohdsi.webapi.shiro.Entities.UserRepository; -import org.ohdsi.webapi.shiro.management.datasource.SourceIdAccessor; -import org.ohdsi.webapi.source.Source; -import org.ohdsi.webapi.source.SourceDaimon; -import org.ohdsi.webapi.source.SourceInfo; -import org.ohdsi.webapi.source.SourceRepository; -import org.ohdsi.webapi.source.SourceService; -import org.ohdsi.webapi.tag.TagService; -import org.ohdsi.webapi.tag.dto.TagNameListRequestDTO; -import org.ohdsi.webapi.util.*; -import org.ohdsi.webapi.util.CancelableJdbcTemplate; -import org.ohdsi.webapi.versioning.domain.CohortVersion; -import org.ohdsi.webapi.versioning.domain.Version; -import org.ohdsi.webapi.versioning.domain.VersionBase; -import org.ohdsi.webapi.versioning.domain.VersionType; -import org.ohdsi.webapi.versioning.dto.VersionDTO; -import org.ohdsi.webapi.versioning.dto.VersionUpdateDTO; -import org.ohdsi.webapi.versioning.service.VersionService; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.job.builder.SimpleJobBuilder; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionTemplate; -import org.springframework.web.bind.annotation.*; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import java.io.ByteArrayOutputStream; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; - -import static org.ohdsi.webapi.Constants.Params.COHORT_DEFINITION_ID; -import static org.ohdsi.webapi.Constants.Params.JOB_NAME; -import static org.ohdsi.webapi.Constants.Params.SOURCE_ID; -import static org.ohdsi.webapi.util.SecurityUtils.whitelist; - -/** - * Spring MVC version of CohortDefinitionService - * - * Migration Status: Replaces /service/CohortDefinitionService.java (Jersey) - * Endpoints: 25+ endpoints for cohort definition management - * Complexity: High - comprehensive CRUD, versioning, generation, tags, validation - */ -@RestController -@RequestMapping("/cohortdefinition") -public class CohortDefinitionMvcController extends AbstractMvcController { - - private static final CohortExpressionQueryBuilder queryBuilder = new CohortExpressionQueryBuilder(); - - @Autowired - private CohortDefinitionRepository cohortDefinitionRepository; - - @Autowired - private JobRepository jobRepository; - - @Autowired - private PlatformTransactionManager transactionManager; - - private TransactionTemplate transactionTemplate; - - @Autowired - public void setTransactionManager(PlatformTransactionManager transactionManager) { - this.transactionManager = transactionManager; - this.transactionTemplate = new TransactionTemplate(transactionManager); - } - - @Autowired - private TransactionTemplate transactionTemplateRequiresNew; - - @Autowired - private TransactionTemplate transactionTemplateNoTransaction; - - @Autowired - private JobTemplate jobTemplate; - - @Autowired - private CohortGenerationService cohortGenerationService; - - @Autowired - private JobService jobService; - - @Autowired - private CohortGenerationSensitiveInfoService sensitiveInfoService; - - @Autowired - private SourceIdAccessor sourceIdAccessor; - - @Autowired - private ConversionService conversionService; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private ApplicationEventPublisher eventPublisher; - - @Autowired - private SourceService sourceService; - - @Autowired - private VocabularyService vocabularyService; - - @Autowired - private PermissionService permissionService; - - @Autowired - private UserRepository userRepository; - - @Autowired - private SourceRepository sourceRepository; - - @PersistenceContext - protected EntityManager entityManager; - - @Autowired - private CohortChecker cohortChecker; - - @Autowired - private VersionService versionService; - - @Value("${security.defaultGlobalReadPermissions}") - private boolean defaultGlobalReadPermissions; - - private final MarkdownRender markdownPF = new MarkdownRender(); - private final List extensions = Arrays.asList(TablesExtension.create()); - - private final RowMapper summaryMapper = (rs, rowNum) -> { - InclusionRuleReport.Summary summary = new InclusionRuleReport.Summary(); - summary.baseCount = rs.getLong("base_count"); - summary.finalCount = rs.getLong("final_count"); - summary.lostCount = rs.getLong("lost_count"); - - double matchRatio = (summary.baseCount > 0) ? ((double) summary.finalCount / (double) summary.baseCount) : 0.0; - summary.percentMatched = new BigDecimal(matchRatio * 100.0).setScale(2, RoundingMode.HALF_UP).toPlainString() + "%"; - return summary; - }; - - private final RowMapper inclusionRuleStatisticMapper = (rs, rowNum) -> { - InclusionRuleReport.InclusionRuleStatistic statistic = new InclusionRuleReport.InclusionRuleStatistic(); - statistic.id = rs.getInt("rule_sequence"); - statistic.name = rs.getString("name"); - statistic.countSatisfying = rs.getLong("person_count"); - long personTotal = rs.getLong("person_total"); - - long gainCount = rs.getLong("gain_count"); - double excludeRatio = personTotal > 0 ? (double) gainCount / (double) personTotal : 0.0; - String percentExcluded = new BigDecimal(excludeRatio * 100.0).setScale(2, RoundingMode.HALF_UP).toPlainString(); - statistic.percentExcluded = percentExcluded + "%"; - - long satisfyCount = rs.getLong("person_count"); - double satisfyRatio = personTotal > 0 ? (double) satisfyCount / (double) personTotal : 0.0; - String percentSatisfying = new BigDecimal(satisfyRatio * 100.0).setScale(2, RoundingMode.HALF_UP).toPlainString(); - statistic.percentSatisfying = percentSatisfying + "%"; - return statistic; - }; - - private final RowMapper inclusionRuleResultItemMapper = (rs, rowNum) -> { - Long[] resultItem = new Long[2]; - resultItem[0] = rs.getLong("inclusion_rule_mask"); - resultItem[1] = rs.getLong("person_count"); - return resultItem; - }; - - public static class GenerateSqlRequest { - @JsonProperty("expression") - public CohortExpression expression; - - @JsonProperty("options") - public CohortExpressionQueryBuilder.BuildExpressionQueryOptions options; - } - - /** - * Returns OHDSI template SQL for a given cohort definition - * - * Jersey: POST /WebAPI/cohortdefinition/sql - * Spring MVC: POST /WebAPI/v2/cohortdefinition/sql - * - * @summary Generate Sql - */ - @PostMapping(value = "/sql", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity generateSql(@RequestBody GenerateSqlRequest request) { - CohortExpressionQueryBuilder.BuildExpressionQueryOptions options = request.options; - GenerateSqlResult result = new GenerateSqlResult(); - if (options == null) { - options = new CohortExpressionQueryBuilder.BuildExpressionQueryOptions(); - } - String expressionSql = queryBuilder.buildExpressionQuery(request.expression, options); - result.templateSql = SqlRender.renderSql(expressionSql, null, null); - - return ok(result); - } - - /** - * Returns metadata about all cohort definitions in the WebAPI database - * - * Jersey: GET /WebAPI/cohortdefinition/ - * Spring MVC: GET /WebAPI/v2/cohortdefinition/ - * - * @summary List Cohort Definitions - */ - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - @Cacheable(cacheNames = "cohortDefinitionList", key = "@permissionService.getSubjectCacheKey()") - public ResponseEntity> getCohortDefinitionList() { - List definitions = cohortDefinitionRepository.list(); - List result = definitions.stream() - .filter(!defaultGlobalReadPermissions ? entity -> permissionService.hasReadAccess(entity) : entity -> true) - .map(def -> { - CohortMetadataDTO dto = conversionService.convert(def, CohortMetadataImplDTO.class); - permissionService.fillWriteAccess(def, dto); - permissionService.fillReadAccess(def, dto); - return dto; - }) - .collect(Collectors.toList()); - return ok(result); - } - - /** - * Creates a cohort definition in the WebAPI database - * - * Jersey: POST /WebAPI/cohortdefinition/ - * Spring MVC: POST /WebAPI/v2/cohortdefinition/ - * - * @summary Create Cohort Definition - */ - @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @Transactional - @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) - public ResponseEntity createCohortDefinition(@RequestBody CohortDTO dto) { - Date currentTime = Calendar.getInstance().getTime(); - - UserEntity user = userRepository.findByLogin(security.getSubject()); - CohortDefinition newDef = new CohortDefinition(); - newDef.setName(StringUtils.trim(dto.getName())) - .setDescription(dto.getDescription()) - .setExpressionType(dto.getExpressionType()); - newDef.setCreatedBy(user); - newDef.setCreatedDate(currentTime); - - newDef = this.cohortDefinitionRepository.save(newDef); - - CohortDefinitionDetails details = new CohortDefinitionDetails(); - details.setCohortDefinition(newDef) - .setExpression(Utils.serialize(dto.getExpression())); - - newDef.setDetails(details); - - CohortDefinition createdDefinition = this.cohortDefinitionRepository.save(newDef); - return ok(conversionService.convert(createdDefinition, CohortDTO.class)); - } - - /** - * Returns the 'raw' cohort definition for the given id - * - * Jersey: GET /WebAPI/cohortdefinition/{id} - * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id} - * - * @summary Get Raw Cohort Definition - */ - @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortDefinitionRaw(@PathVariable("id") final int id) { - CohortRawDTO result = transactionTemplate.execute(transactionStatus -> { - CohortDefinition d = this.cohortDefinitionRepository.findOneWithDetail(id); - ExceptionUtils.throwNotFoundExceptionIfNull(d, String.format("There is no cohort definition with id = %d.", id)); - return conversionService.convert(d, CohortRawDTO.class); - }); - return ok(result); - } - - /** - * Check that a cohort exists - * - * Jersey: GET /WebAPI/cohortdefinition/{id}/exists - * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/exists - * - * @summary Check Cohort Definition Name - */ - @GetMapping(value = "/{id}/exists", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCountCDefWithSameName( - @PathVariable("id") final int id, - @RequestParam(value = "name", required = false) String name) { - return ok(cohortDefinitionRepository.getCountCDefWithSameName(id, name)); - } - - /** - * Saves the cohort definition for the given id - * - * Jersey: PUT /WebAPI/cohortdefinition/{id} - * Spring MVC: PUT /WebAPI/v2/cohortdefinition/{id} - * - * @summary Save Cohort Definition - */ - @PutMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @Transactional - @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) - public ResponseEntity saveCohortDefinition(@PathVariable("id") final int id, @RequestBody CohortDTO def) { - Date currentTime = Calendar.getInstance().getTime(); - - saveVersion(id); - - CohortDefinition currentDefinition = this.cohortDefinitionRepository.findOneWithDetail(id); - UserEntity modifier = userRepository.findByLogin(security.getSubject()); - - currentDefinition.setName(def.getName()) - .setDescription(def.getDescription()) - .setExpressionType(def.getExpressionType()) - .getDetails().setExpression(Utils.serialize(def.getExpression())); - currentDefinition.setModifiedBy(modifier); - currentDefinition.setModifiedDate(currentTime); - - currentDefinition = this.cohortDefinitionRepository.save(currentDefinition); - eventPublisher.publishEvent(new CohortDefinitionChangedEvent(currentDefinition)); - - CohortDTO result = getCohortDefinition(id); - return ok(result); - } - - /** - * Queues up a generate cohort task for the specified cohort definition id - * - * Jersey: GET /WebAPI/cohortdefinition/{id}/generate/{sourceKey} - * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/generate/{sourceKey} - * - * @summary Generate Cohort - */ - @GetMapping(value = "/{id}/generate/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity generateCohort( - @PathVariable("id") final int id, - @PathVariable("sourceKey") final String sourceKey, - @RequestParam(value = "demographic", defaultValue = "false") boolean demographicStat) { - - Source source = transactionTemplate.execute(status -> { - Source s = sourceRepository.findBySourceKey(sourceKey); - if (s != null) { - Hibernate.initialize(s); - } - return s; - }); - - CohortDefinition currentDefinition = transactionTemplate.execute(status -> { - CohortDefinition cd = this.cohortDefinitionRepository.findOneWithDetail(id); - if (cd != null) { - if (cd.getDetails() != null) { - cd.getDetails().getExpression(); - } - Hibernate.initialize(cd.getGenerationInfoList()); - } - return cd; - }); - - UserEntity user = transactionTemplate.execute(status -> { - UserEntity u = userRepository.findByLogin(security.getSubject()); - if (u != null) { - Hibernate.initialize(u); - } - return u; - }); - - JobExecutionResource result = cohortGenerationService.generateCohortViaJob(user, currentDefinition, source, demographicStat); - return ok(result); - } - - /** - * Cancel a cohort generation task - * - * Jersey: GET /WebAPI/cohortdefinition/{id}/cancel/{sourceKey} - * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/cancel/{sourceKey} - * - * @summary Cancel Cohort Generation - */ - @GetMapping(value = "/{id}/cancel/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity cancelGenerateCohort( - @PathVariable("id") final int id, - @PathVariable("sourceKey") final String sourceKey) { - - final Source source = Optional.ofNullable(sourceRepository.findBySourceKey(sourceKey)) - .orElseThrow(() -> new RuntimeException("Source not found")); - - transactionTemplateRequiresNew.execute(status -> { - CohortDefinition currentDefinition = cohortDefinitionRepository.findById(id).orElse(null); - if (Objects.nonNull(currentDefinition)) { - CohortGenerationInfo info = findBySourceId(currentDefinition.getGenerationInfoList(), source.getSourceId()); - if (Objects.nonNull(info)) { - invalidateExecution(info); - cohortDefinitionRepository.save(currentDefinition); - } - } - return null; - }); - - jobService.cancelJobExecution(e -> { - JobParameters parameters = e.getJobParameters(); - String jobName = e.getJobInstance().getJobName(); - return Objects.equals(parameters.getString(COHORT_DEFINITION_ID), Integer.toString(id)) - && Objects.equals(parameters.getString(SOURCE_ID), Integer.toString(source.getSourceId())) - && Objects.equals(Constants.GENERATE_COHORT, jobName); - }); - - return ok(); - } - - /** - * Returns a list of cohort generation info objects - * - * Jersey: GET /WebAPI/cohortdefinition/{id}/info - * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/info - * - * @summary Get cohort generation info - */ - @GetMapping(value = "/{id}/info", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity> getInfo(@PathVariable("id") final int id) { - CohortDefinition def = this.cohortDefinitionRepository.findById(id).orElse(null); - ExceptionUtils.throwNotFoundExceptionIfNull(def, String.format("There is no cohort definition with id = %d.", id)); - - Set infoList = def.getGenerationInfoList(); - List result = infoList.stream() - .filter(genInfo -> sourceIdAccessor.hasAccess(genInfo.getId().getSourceId())) - .collect(Collectors.toList()); - - Map sourceMap = sourceService.getSourcesMap(SourceMapKey.BY_SOURCE_ID); - List filteredResult = sensitiveInfoService.filterSensitiveInfo(result, - gi -> Collections.singletonMap(Constants.Variables.SOURCE, sourceMap.get(gi.getId().getSourceId()))); - - List dtos = filteredResult.stream() - .map(t -> conversionService.convert(t, CohortGenerationInfoDTO.class)) - .collect(Collectors.toList()); - - return ok(dtos); - } - - /** - * Copies the specified cohort definition - * - * Jersey: GET /WebAPI/cohortdefinition/{id}/copy - * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/copy - * - * @summary Copy Cohort Definition - */ - @GetMapping(value = "/{id}/copy", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) - public ResponseEntity copy(@PathVariable("id") final int id) { - CohortDTO sourceDef = getCohortDefinition(id); - sourceDef.setId(null); - sourceDef.setTags(null); - sourceDef.setName(NameUtils.getNameForCopy(sourceDef.getName(), this::getNamesLike, - cohortDefinitionRepository.findByName(sourceDef.getName()))); - - CohortDTO copyDef = createCohortDefinition(sourceDef).getBody(); - return ok(copyDef); - } - - /** - * Deletes the specified cohort definition - * - * Jersey: DELETE /WebAPI/cohortdefinition/{id} - * Spring MVC: DELETE /WebAPI/v2/cohortdefinition/{id} - * - * @summary Delete Cohort Definition - */ - @DeleteMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) - public ResponseEntity delete(@PathVariable("id") final int id) { - transactionTemplateRequiresNew.execute(new TransactionCallbackWithoutResult() { - @Override - public void doInTransactionWithoutResult(final TransactionStatus status) { - CohortDefinition def = cohortDefinitionRepository.findById(id).orElse(null); - if (!Objects.isNull(def)) { - def.getGenerationInfoList().forEach(cohortGenerationInfo -> { - Integer sourceId = cohortGenerationInfo.getId().getSourceId(); - - jobService.cancelJobExecution(e -> { - JobParameters parameters = e.getJobParameters(); - String jobName = e.getJobInstance().getJobName(); - return Objects.equals(parameters.getString(COHORT_DEFINITION_ID), Integer.toString(id)) - && Objects.equals(parameters.getString(SOURCE_ID), Integer.toString(sourceId)) - && Objects.equals(Constants.GENERATE_COHORT, jobName); - }); - }); - cohortDefinitionRepository.delete(def); - } - } - }); - - JobParametersBuilder builder = new JobParametersBuilder(); - builder.addString(JOB_NAME, String.format("Cleanup cohort %d.", id)); - builder.addString(COHORT_DEFINITION_ID, ("" + id)); - - final JobParameters jobParameters = builder.toJobParameters(); - - CleanupCohortTasklet cleanupTasklet = new CleanupCohortTasklet(transactionTemplateNoTransaction, sourceRepository); - - Step cleanupStep = new StepBuilder("cohortDefinition.cleanupCohort", jobRepository) - .tasklet(cleanupTasklet, transactionManager) - .build(); - - SimpleJobBuilder cleanupJobBuilder = new JobBuilder("cleanupCohort", jobRepository) - .start(cleanupStep); - - Job cleanupCohortJob = cleanupJobBuilder.build(); - - jobTemplate.launch(cleanupCohortJob, jobParameters); - - return ok(); - } - - /** - * Return concept sets used in a cohort definition as a zip file - * - * Jersey: GET /WebAPI/cohortdefinition/{id}/export/conceptset - * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/export/conceptset - * - * @summary Export Concept Sets as ZIP - */ - @GetMapping(value = "/{id}/export/conceptset", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) - public ResponseEntity exportConceptSets(@PathVariable("id") final int id) { - Source source = sourceService.getPriorityVocabularySource(); - if (Objects.isNull(source)) { - return forbidden(); - } - - CohortDefinition def = this.cohortDefinitionRepository.findOneWithDetail(id); - if (Objects.isNull(def)) { - return notFound(); - } - - List exports = getConceptSetExports(def, new SourceInfo(source)); - ByteArrayOutputStream exportStream = ExportUtil.writeConceptSetExportToCSVAndZip(exports); - - ByteArrayResource resource = new ByteArrayResource(exportStream.toByteArray()); - - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"cohortdefinition_" + def.getId() + "_export.zip\"") - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .body(resource); - } - - /** - * Get the Inclusion Rule report for the specified source and mode - * - * Jersey: GET /WebAPI/cohortdefinition/{id}/report/{sourceKey} - * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/report/{sourceKey} - * - * @summary Get Inclusion Rule Report - */ - @GetMapping(value = "/{id}/report/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity getInclusionRuleReport( - @PathVariable("id") final int id, - @PathVariable("sourceKey") final String sourceKey, - @RequestParam(value = "mode", defaultValue = "0") int modeId, - @RequestParam(value = "ccGenerateId", required = false) String ccGenerateId) { - - Source source = this.sourceRepository.findBySourceKey(sourceKey); - - InclusionRuleReport.Summary summary = getInclusionRuleReportSummary(whitelist(id), source, modeId); - List inclusionRuleStats = getInclusionRuleStatistics(whitelist(id), source, modeId); - String treemapData = getInclusionRuleTreemapData(whitelist(id), inclusionRuleStats.size(), source, modeId); - - InclusionRuleReport report = new InclusionRuleReport(); - report.summary = summary; - report.inclusionRuleStats = inclusionRuleStats; - report.treemapData = treemapData; - - return ok(report); - } - - /** - * Checks the cohort definition for logic issues - * - * Jersey: POST /WebAPI/cohortdefinition/check - * Spring MVC: POST /WebAPI/v2/cohortdefinition/check - * - * @summary Check Cohort Definition - */ - @PostMapping(value = "/check", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity runDiagnostics(@RequestBody CohortExpression expression) { - Checker checker = new Checker(); - return ok(new CheckResultDTO(checker.check(expression))); - } - - /** - * Checks the cohort definition for logic issues (V2 with tags) - * - * Jersey: POST /WebAPI/cohortdefinition/checkV2 - * Spring MVC: POST /WebAPI/v2/cohortdefinition/checkV2 - * - * @summary Check Cohort Definition V2 - */ - @PostMapping(value = "/checkV2", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity runDiagnosticsWithTags(@RequestBody CohortDTO cohortDTO) { - Checker checker = new Checker(); - CheckResultDTO checkResultDTO = new CheckResultDTO(checker.check(cohortDTO.getExpression())); - List circeWarnings = checkResultDTO.getWarnings().stream() - .map(WarningUtils::convertCirceWarning) - .collect(Collectors.toList()); - CheckResult checkResult = new CheckResult(cohortChecker.check(cohortDTO)); - checkResult.getWarnings().addAll(circeWarnings); - return ok(checkResult); - } - - /** - * Render a cohort expression in html or markdown form - * - * Jersey: POST /WebAPI/cohortdefinition/printfriendly/cohort - * Spring MVC: POST /WebAPI/v2/cohortdefinition/printfriendly/cohort - * - * @summary Cohort Print Friendly - */ - @PostMapping(value = "/printfriendly/cohort", consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity cohortPrintFriendly( - @RequestBody CohortExpression expression, - @RequestParam(value = "format", defaultValue = "html") String format) { - String markdown = convertCohortExpressionToMarkdown(expression); - return printFriendly(markdown, format); - } - - /** - * Render a list of concept sets in html or markdown form - * - * Jersey: POST /WebAPI/cohortdefinition/printfriendly/conceptsets - * Spring MVC: POST /WebAPI/v2/cohortdefinition/printfriendly/conceptsets - * - * @summary Concept Set Print Friendly - */ - @PostMapping(value = "/printfriendly/conceptsets", consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity conceptSetListPrintFriendly( - @RequestBody List conceptSetList, - @RequestParam(value = "format", defaultValue = "html") String format) { - String markdown = markdownPF.renderConceptSetList(conceptSetList.toArray(new ConceptSet[0])); - return printFriendly(markdown, format); - } - - /** - * Assign tag to Cohort Definition - * - * Jersey: POST /WebAPI/cohortdefinition/{id}/tag/ - * Spring MVC: POST /WebAPI/v2/cohortdefinition/{id}/tag/ - * - * @summary Assign Tag - */ - @PostMapping(value = "/{id}/tag", produces = MediaType.APPLICATION_JSON_VALUE) - @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) - @Transactional - public ResponseEntity assignTag(@PathVariable("id") final Integer id, @RequestBody int tagId) { - CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); - assignTag(entity, tagId); - return ok(); - } - - /** - * Unassign tag from Cohort Definition - * - * Jersey: DELETE /WebAPI/cohortdefinition/{id}/tag/{tagId} - * Spring MVC: DELETE /WebAPI/v2/cohortdefinition/{id}/tag/{tagId} - * - * @summary Unassign Tag - */ - @DeleteMapping(value = "/{id}/tag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) - @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) - @Transactional - public ResponseEntity unassignTag(@PathVariable("id") final Integer id, @PathVariable("tagId") final int tagId) { - CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); - unassignTag(entity, tagId); - return ok(); - } - - /** - * Assign protected tag to Cohort Definition - * - * Jersey: POST /WebAPI/cohortdefinition/{id}/protectedtag/ - * Spring MVC: POST /WebAPI/v2/cohortdefinition/{id}/protectedtag/ - * - * @summary Assign Protected Tag - */ - @PostMapping(value = "/{id}/protectedtag", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity assignPermissionProtectedTag(@PathVariable("id") final int id, @RequestBody int tagId) { - return assignTag(id, tagId); - } - - /** - * Unassign protected tag from Cohort Definition - * - * Jersey: DELETE /WebAPI/cohortdefinition/{id}/protectedtag/{tagId} - * Spring MVC: DELETE /WebAPI/v2/cohortdefinition/{id}/protectedtag/{tagId} - * - * @summary Unassign Protected Tag - */ - @DeleteMapping(value = "/{id}/protectedtag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity unassignPermissionProtectedTag(@PathVariable("id") final int id, @PathVariable("tagId") final int tagId) { - return unassignTag(id, tagId); - } - - /** - * Get list of versions of Cohort Definition - * - * Jersey: GET /WebAPI/cohortdefinition/{id}/version/ - * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/version/ - * - * @summary Get Cohort Definition Versions - */ - @GetMapping(value = "/{id}/version", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity> getVersions(@PathVariable("id") final long id) { - List versions = versionService.getVersions(VersionType.COHORT, id); - List dtos = versions.stream() - .map(v -> conversionService.convert(v, VersionDTO.class)) - .collect(Collectors.toList()); - return ok(dtos); - } - - /** - * Get version of Cohort Definition - * - * Jersey: GET /WebAPI/cohortdefinition/{id}/version/{version} - * Spring MVC: GET /WebAPI/v2/cohortdefinition/{id}/version/{version} - * - * @summary Get Cohort Definition Version - */ - @GetMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity getVersion( - @PathVariable("id") final int id, - @PathVariable("version") final int version) { - checkVersion(id, version, false); - CohortVersion cohortVersion = versionService.getById(VersionType.COHORT, id, version); - return ok(conversionService.convert(cohortVersion, CohortVersionFullDTO.class)); - } - - /** - * Updates version of Cohort Definition - * - * Jersey: PUT /WebAPI/cohortdefinition/{id}/version/{version} - * Spring MVC: PUT /WebAPI/v2/cohortdefinition/{id}/version/{version} - * - * @summary Update Version - */ - @PutMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity updateVersion( - @PathVariable("id") final int id, - @PathVariable("version") final int version, - @RequestBody VersionUpdateDTO updateDTO) { - checkVersion(id, version); - updateDTO.setAssetId(id); - updateDTO.setVersion(version); - CohortVersion updated = versionService.update(VersionType.COHORT, updateDTO); - return ok(conversionService.convert(updated, VersionDTO.class)); - } - - /** - * Delete version of Cohort Definition - * - * Jersey: DELETE /WebAPI/cohortdefinition/{id}/version/{version} - * Spring MVC: DELETE /WebAPI/v2/cohortdefinition/{id}/version/{version} - * - * @summary Delete Cohort Definition Version - */ - @DeleteMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity deleteVersion(@PathVariable("id") final int id, @PathVariable("version") final int version) { - checkVersion(id, version); - versionService.delete(VersionType.COHORT, id, version); - return ok(); - } - - /** - * Create a new asset from version of Cohort Definition - * - * Jersey: PUT /WebAPI/cohortdefinition/{id}/version/{version}/createAsset - * Spring MVC: PUT /WebAPI/v2/cohortdefinition/{id}/version/{version}/createAsset - * - * @summary Create Cohort from Version - */ - @PutMapping(value = "/{id}/version/{version}/createAsset", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - @CacheEvict(cacheNames = "cohortDefinitionList", allEntries = true) - public ResponseEntity copyAssetFromVersion( - @PathVariable("id") final int id, - @PathVariable("version") final int version) { - checkVersion(id, version, false); - CohortVersion cohortVersion = versionService.getById(VersionType.COHORT, id, version); - CohortVersionFullDTO fullDTO = conversionService.convert(cohortVersion, CohortVersionFullDTO.class); - CohortDTO dto = conversionService.convert(fullDTO.getEntityDTO(), CohortDTO.class); - dto.setId(null); - dto.setTags(null); - dto.setName(NameUtils.getNameForCopy(dto.getName(), this::getNamesLike, - cohortDefinitionRepository.findByName(dto.getName()))); - CohortDTO created = createCohortDefinition(dto).getBody(); - return ok(created); - } - - /** - * Get list of cohort definitions with assigned tags - * - * Jersey: POST /WebAPI/cohortdefinition/byTags - * Spring MVC: POST /WebAPI/v2/cohortdefinition/byTags - * - * @summary List Cohorts By Tag - */ - @PostMapping(value = "/byTags", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @Transactional - public ResponseEntity> listByTags(@RequestBody TagNameListRequestDTO requestDTO) { - if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { - return ok(Collections.emptyList()); - } - List names = requestDTO.getNames().stream() - .map(name -> name.toLowerCase(Locale.ROOT)) - .collect(Collectors.toList()); - List entities = cohortDefinitionRepository.findByTags(names); - List result = listByTags(entities, names, CohortDTO.class); - return ok(result); - } - - // Helper methods - - private CohortGenerationInfo findBySourceId(Set infoList, Integer sourceId) { - for (CohortGenerationInfo info : infoList) { - if (info.getId().getSourceId().equals(sourceId)) { - return info; - } - } - return null; - } - - private InclusionRuleReport.Summary getInclusionRuleReportSummary(int id, Source source, int modeId) { - String sql = "select cs.base_count, cs.final_count, cc.lost_count from @tableQualifier.cohort_summary_stats cs left join @tableQualifier.cohort_censor_stats cc " - + "on cc.cohort_definition_id = cs.cohort_definition_id where cs.cohort_definition_id = @id and cs.mode_id = @modeId"; - String tqName = "tableQualifier"; - String tqValue = source.getTableQualifier(SourceDaimon.DaimonType.Results); - String[] varNames = {"id", "modeId"}; - Object[] varValues = {whitelist(id), whitelist(modeId)}; - PreparedStatementRenderer psr = new PreparedStatementRenderer(source, sql, tqName, tqValue, varNames, varValues, SessionUtils.sessionId()); - List result = getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), summaryMapper); - return result.isEmpty() ? new InclusionRuleReport.Summary() : result.get(0); - } - - private List getInclusionRuleStatistics(int id, Source source, int modeId) { - String sql = "select i.rule_sequence, i.name, s.person_count, s.gain_count, s.person_total" - + " from @tableQualifier.cohort_inclusion i join @tableQualifier.cohort_inclusion_stats s on i.cohort_definition_id = s.cohort_definition_id" - + " and i.rule_sequence = s.rule_sequence" - + " where i.cohort_definition_id = @id and mode_id = @modeId ORDER BY i.rule_sequence"; - String tqName = "tableQualifier"; - String tqValue = source.getTableQualifier(SourceDaimon.DaimonType.Results); - String[] varNames = {"id", "modeId"}; - Object[] varValues = {whitelist(id), whitelist(modeId)}; - PreparedStatementRenderer psr = new PreparedStatementRenderer(source, sql, tqName, tqValue, varNames, varValues, SessionUtils.sessionId()); - return getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), inclusionRuleStatisticMapper); - } - - private int countSetBits(long n) { - int count = 0; - while (n > 0) { - n &= (n - 1); - count++; - } - return count; - } - - private String formatBitMask(Long n, int size) { - return StringUtils.reverse(StringUtils.leftPad(Long.toBinaryString(n), size, "0")); - } - - private String getInclusionRuleTreemapData(int id, int inclusionRuleCount, Source source, int modeId) { - String sql = "select inclusion_rule_mask, person_count from @tableQualifier.cohort_inclusion_result where cohort_definition_id = @id and mode_id = @modeId"; - String tqName = "tableQualifier"; - String tqValue = source.getTableQualifier(SourceDaimon.DaimonType.Results); - String[] varNames = {"id", "modeId"}; - Object[] varValues = {whitelist(id), whitelist(modeId)}; - PreparedStatementRenderer psr = new PreparedStatementRenderer(source, sql, tqName, tqValue, varNames, varValues, SessionUtils.sessionId()); - - List items = this.getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), inclusionRuleResultItemMapper); - Map> groups = new HashMap<>(); - for (Long[] item : items) { - int bitsSet = countSetBits(item[0]); - if (!groups.containsKey(bitsSet)) { - groups.put(bitsSet, new ArrayList()); - } - groups.get(bitsSet).add(item); - } - - StringBuilder treemapData = new StringBuilder("{\"name\" : \"Everyone\", \"children\" : ["); - - List groupKeys = new ArrayList<>(groups.keySet()); - Collections.sort(groupKeys); - Collections.reverse(groupKeys); - - int groupCount = 0; - for (Integer groupKey : groupKeys) { - if (groupCount > 0) { - treemapData.append(","); - } - - treemapData.append(String.format("{\"name\" : \"Group %d\", \"children\" : [", groupKey)); - - int groupItemCount = 0; - for (Long[] groupItem : groups.get(groupKey)) { - if (groupItemCount > 0) { - treemapData.append(","); - } - - treemapData.append(String.format("{\"name\": \"%s\", \"size\": %d}", formatBitMask(groupItem[0], inclusionRuleCount), groupItem[1])); - groupItemCount++; - } - groupCount++; - } - - treemapData.append(StringUtils.repeat("]}", groupCount + 1)); - - return treemapData.toString(); - } - - private List getConceptSetExports(CohortDefinition def, SourceInfo vocabSource) throws RuntimeException { - CohortExpression expression; - try { - expression = objectMapper.readValue(def.getDetails().getExpression(), CohortExpression.class); - } catch (Exception e) { - throw new RuntimeException(e); - } - - return Arrays.stream(expression.conceptSets) - .map(cs -> vocabularyService.exportConceptSet(cs, vocabSource)) - .collect(Collectors.toList()); - } - - public String convertCohortExpressionToMarkdown(CohortExpression expression) { - return markdownPF.renderCohort(expression); - } - - public String convertMarkdownToHTML(String markdown) { - Parser parser = Parser.builder().extensions(extensions).build(); - Node document = parser.parse(markdown); - HtmlRenderer renderer = HtmlRenderer.builder().extensions(extensions).build(); - return renderer.render(document); - } - - private ResponseEntity printFriendly(String markdown, String format) { - if ("html".equalsIgnoreCase(format)) { - String html = convertMarkdownToHTML(markdown); - return ResponseEntity.ok() - .contentType(MediaType.TEXT_HTML) - .body(html); - } else if ("markdown".equals(format)) { - return ResponseEntity.ok() - .contentType(MediaType.TEXT_PLAIN) - .body(markdown); - } else { - return ResponseEntity.status(415).build(); // Unsupported Media Type - } - } - - private void checkVersion(int id, int version) { - checkVersion(id, version, true); - } - - private void checkVersion(int id, int version, boolean checkOwnerShip) { - Version cohortVersion = versionService.getById(VersionType.COHORT, id, version); - ExceptionUtils.throwNotFoundExceptionIfNull(cohortVersion, - String.format("There is no cohort version with id = %d.", version)); - - CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); - if (checkOwnerShip) { - checkOwnerOrAdminOrGranted(entity); - } - } - - private CohortVersion saveVersion(int id) { - CohortDefinition def = this.cohortDefinitionRepository.findOneWithDetail(id); - CohortVersion version = conversionService.convert(def, CohortVersion.class); - - UserEntity user = Objects.nonNull(def.getModifiedBy()) ? def.getModifiedBy() : def.getCreatedBy(); - Date versionDate = Objects.nonNull(def.getModifiedDate()) ? def.getModifiedDate() : def.getCreatedDate(); - version.setCreatedBy(user); - version.setCreatedDate(versionDate); - return versionService.create(VersionType.COHORT, version); - } - - public CohortDTO getCohortDefinition(final int id) { - return transactionTemplate.execute(transactionStatus -> { - CohortDefinition d = this.cohortDefinitionRepository.findOneWithDetail(id); - ExceptionUtils.throwNotFoundExceptionIfNull(d, String.format("There is no cohort definition with id = %d.", id)); - return conversionService.convert(d, CohortDTO.class); - }); - } - - public List getNamesLike(String copyName) { - return cohortDefinitionRepository.findAllByNameStartsWith(copyName).stream() - .map(CohortDefinition::getName) - .collect(Collectors.toList()); - } - - // Helper service reference for DAO operations - @Autowired - private CohortDefinitionService cohortDefinitionService; - - @Autowired - private TagService tagService; - - @Value("${jdbc.suppressInvalidApiException}") - protected boolean suppressApiException; - - // Delegate methods to existing service for complex DAO operations - private CancelableJdbcTemplate getSourceJdbcTemplate(Source source) { - // Delegate to the existing service implementation - return cohortDefinitionService.getSourceJdbcTemplate(source); - } - - private void checkOwnerOrAdminOrGranted(CohortDefinition entity) { - if (!isSecured()) { - return; - } - - UserEntity user = userRepository.findByLogin(security.getSubject()); - Long ownerId = Objects.nonNull(entity.getCreatedBy()) ? entity.getCreatedBy().getId() : null; - - if (!(user.getId().equals(ownerId) || isAdmin() || permissionService.hasWriteAccess(entity))) { - throw new RuntimeException("Forbidden"); - } - } - - private CohortGenerationInfo invalidateExecution(CohortGenerationInfo info) { - info.setIsValid(false); - info.setStatus(org.ohdsi.webapi.GenerationStatus.COMPLETE); - info.setMessage("Invalidated by system"); - return info; - } - - private List listByTags(List entities, List names, Class clazz) { - return entities.stream() - .filter(e -> e.getTags().stream() - .map(tag -> tag.getName().toLowerCase(Locale.ROOT)) - .collect(Collectors.toList()) - .containsAll(names)) - .map(entity -> { - T dto = conversionService.convert(entity, clazz); - if (dto instanceof org.ohdsi.webapi.service.dto.CommonEntityDTO) { - permissionService.fillWriteAccess(entity, (org.ohdsi.webapi.service.dto.CommonEntityDTO) dto); - } - return dto; - }) - .collect(Collectors.toList()); - } - - private void assignTag(CohortDefinition entity, int tagId) { - checkOwnerOrAdminOrGranted(entity); - if (Objects.nonNull(entity)) { - org.ohdsi.webapi.tag.domain.Tag tag = tagService.getById(tagId); - if (Objects.nonNull(tag)) { - entity.getTags().add(tag); - } - } - } - - private void unassignTag(CohortDefinition entity, int tagId) { - checkOwnerOrAdminOrGranted(entity); - if (Objects.nonNull(entity)) { - org.ohdsi.webapi.tag.domain.Tag tag = tagService.getById(tagId); - if (Objects.nonNull(tag)) { - Set tags = entity.getTags().stream() - .filter(t -> t.getId() != tagId) - .collect(Collectors.toSet()); - entity.setTags(tags); - } - } - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortMvcController.java deleted file mode 100644 index 13f14d0979..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortMvcController.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import java.util.List; - -import jakarta.persistence.EntityManager; - -import org.ohdsi.webapi.cohort.CohortEntity; -import org.ohdsi.webapi.cohort.CohortRepository; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * Spring MVC version of CohortService - * - * Migration Status: Replaces /service/CohortService.java (Jersey) - * Endpoints: 2 endpoints (1 GET, 1 POST) - * Complexity: Simple - read and batch import operations - * - * Service to read/write to the Cohort table - */ -@RestController -@RequestMapping("/cohort") -public class CohortMvcController extends AbstractMvcController { - - @Autowired - private CohortRepository cohortRepository; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Autowired - private EntityManager em; - - /** - * Retrieves all cohort entities for the given cohort definition id - * from the COHORT table - * - * Jersey: GET /WebAPI/cohort/{id} - * Spring MVC: GET /WebAPI/v2/cohort/{id} - * - * @param id Cohort Definition id - * @return List of CohortEntity - */ - @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCohortListById(@PathVariable("id") final long id) { - List cohorts = this.cohortRepository.getAllCohortsForId(id); - return ok(cohorts); - } - - /** - * Imports a List of CohortEntity into the COHORT table - * - * Jersey: POST /WebAPI/cohort/import - * Spring MVC: POST /WebAPI/v2/cohort/import - * - * @param cohort List of CohortEntity - * @return status message - */ - @PostMapping(value = "/import", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.TEXT_PLAIN_VALUE) - public ResponseEntity saveCohortListToCDM(@RequestBody final List cohort) { - this.transactionTemplate.execute(new TransactionCallback() { - @Override - public Void doInTransaction(TransactionStatus status) { - int i = 0; - for (CohortEntity cohortEntity : cohort) { - em.persist(cohortEntity); - if (i % 5 == 0) { //5, same as the JDBC batch size - //flush a batch of inserts and release memory: - em.flush(); - em.clear(); - } - i++; - } - return null; - } - }); - - return ok("ok"); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java deleted file mode 100644 index 324d8b67db..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/CohortResultsMvcController.java +++ /dev/null @@ -1,1386 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import static org.ohdsi.webapi.util.SecurityUtils.whitelist; - -import java.io.ByteArrayOutputStream; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.util.*; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import org.ohdsi.webapi.cohortanalysis.CohortAnalysis; -import org.ohdsi.webapi.cohortanalysis.CohortAnalysisTask; -import org.ohdsi.webapi.cohortanalysis.CohortSummary; -import org.ohdsi.webapi.cohortdefinition.CohortDefinitionRepository; -import org.ohdsi.webapi.cohortdefinition.dto.CohortDTO; -import org.ohdsi.webapi.cohortresults.*; -import org.ohdsi.webapi.model.results.AnalysisResults; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.service.CohortDefinitionService; -import org.ohdsi.webapi.service.CohortResultsService; -import org.ohdsi.webapi.source.Source; -import org.ohdsi.webapi.source.SourceDaimon; -import org.ohdsi.webapi.util.PreparedStatementRenderer; -import org.ohdsi.webapi.util.SessionUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.web.bind.annotation.*; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Spring MVC version of CohortResultsService - * - * Migration Status: Replaces /service/CohortResultsService.java (Jersey) - * Endpoints: 40+ endpoints for cohort analysis results (Heracles Results) - * Complexity: High - extensive analysis reporting, caching, multiple data types - * - * REST Services related to retrieving cohort analysis (a.k.a Heracles Results) analyses results. - * More information on the Heracles project can be found at - * {@link https://www.ohdsi.org/web/wiki/doku.php?id=documentation:software:heracles}. - * The implementation found in WebAPI represents a migration of the functionality - * from the stand-alone HERACLES application to integrate it into WebAPI and ATLAS. - * - * @summary Cohort Analysis Results (a.k.a Heracles Results) - */ -@RestController -@RequestMapping("/cohortresults") -public class CohortResultsMvcController extends AbstractMvcController { - - @Autowired - private CohortResultsService cohortResultsService; - - @Autowired - private CohortDefinitionService cohortDefinitionService; - - @Autowired - private CohortDefinitionRepository cohortDefinitionRepository; - - @Autowired - private ObjectMapper mapper; - - /** - * Queries for cohort analysis results for the given cohort definition id - * - * @summary Get results for analysis group - * @param id cohort_definition id - * @param analysisGroup Name of the analysisGrouping under the /resources/cohortresults/sql/ directory - * @param analysisName Name of the analysis, currently the same name as the sql file under analysisGroup - * @param sourceKey the source to retrieve results - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @return List of key, value pairs - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/raw/{analysis_group}/{analysis_name} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/raw/{analysis_group}/{analysis_name} - */ - @GetMapping(value = "/{sourceKey}/{id}/raw/{analysisGroup}/{analysisName}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity>> getCohortResultsRaw( - @PathVariable("id") int id, - @PathVariable("analysisGroup") String analysisGroup, - @PathVariable("analysisName") String analysisName, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam) { - - List> results = cohortResultsService.getCohortResultsRaw( - id, analysisGroup, analysisName, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey); - return ok(results); - } - - /** - * Export the cohort analysis results to a ZIP file - * - * @summary Export cohort analysis results - * @param id The cohort ID - * @param sourceKey The source Key - * @return A response containing the .ZIP file of results - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/export.zip - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/export.zip - */ - @GetMapping(value = "/{sourceKey}/{id}/export.zip", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) - public ResponseEntity exportCohortResults( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - - ResponseEntity jerseyResponse = cohortResultsService.exportCohortResults(id, sourceKey); - ByteArrayOutputStream baos = (ByteArrayOutputStream) jerseyResponse.getBody(); - - ByteArrayResource resource = new ByteArrayResource(baos.toByteArray()); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentDispositionFormData("attachment", "cohort_" + id + "_export.zip"); - headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); - - return ResponseEntity.ok() - .headers(headers) - .contentLength(resource.contentLength()) - .body(resource); - } - - /** - * Provides a warmup mechanism for the data visualization cache. This - * endpoint does not appear to be used and may be a hold over from the - * original HERACLES implementation - * - * @summary Warmup data visualizations - * @param task The cohort analysis task - * @return The number of report visualizations warmed - * - * Jersey: POST /WebAPI/cohortresults/warmup - * Spring MVC: POST /WebAPI/v2/cohortresults/warmup - */ - @PostMapping(value = "/warmup", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity warmUpVisualizationData(@RequestBody CohortAnalysisTask task) { - int result = cohortResultsService.warmUpVisualizationData(task); - return ok(result); - } - - /** - * Provides a list of cohort analysis visualizations that are completed - * - * @summary Get completed cohort analysis visualizations - * @param id The cohort ID - * @param sourceKey The source key - * @return A list of visualization keys that are complete - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/completed - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/completed - */ - @GetMapping(value = "/{sourceKey}/{id}/completed", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCompletedVisualization( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - - Collection result = cohortResultsService.getCompletedVisualiztion(id, sourceKey); - return ok(result); - } - - /** - * Retrieves the tornado plot - * - * @summary Get the tornado plot - * @param sourceKey The source key - * @param cohortDefinitionId The cohort definition id - * @return The tornado plot data - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/tornado - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/tornado - */ - @GetMapping(value = "/{sourceKey}/{id}/tornado", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getTornadoReport( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") int cohortDefinitionId) { - - TornadoReport result = cohortResultsService.getTornadoReport(sourceKey, cohortDefinitionId); - return ok(result); - } - - /** - * Queries for cohort analysis dashboard for the given cohort definition id - * - * @summary Get the dashboard - * @param id The cohort definition id - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param demographicsOnly only render gender and age - * @param refresh Boolean - refresh visualization data - * @return CohortDashboard - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/dashboard - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/dashboard - */ - @GetMapping(value = "/{sourceKey}/{id}/dashboard", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getDashboard( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "demographics_only", defaultValue = "false") boolean demographicsOnly, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortDashboard result = cohortResultsService.getDashboard( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, demographicsOnly, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis condition treemap results for the given cohort definition id - * - * @summary Get condition treemap - * @param sourceKey The source key - * @param id The cohort ID - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/condition/ - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/condition/ - */ - @GetMapping(value = "/{sourceKey}/{id}/condition/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getConditionTreemap( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") int id, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getConditionTreemap( - sourceKey, id, minCovariatePersonCountParam, minIntervalPersonCountParam, refresh); - return ok(result); - } - - /** - * Get the distinct person count for a cohort - * - * @summary Get distinct person count - * @param sourceKey The source key - * @param id The cohort ID - * @param refresh Boolean - refresh visualization data - * @return Distinct person count as integer - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/distinctPersonCount/ - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/distinctPersonCount/ - */ - @GetMapping(value = "/{sourceKey}/{id}/distinctPersonCount/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getRawDistinctPersonCount( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") String id, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - Integer result = cohortResultsService.getRawDistinctPersonCount(sourceKey, id, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis condition drilldown results for the given cohort definition id and condition id - * - * @summary Get condition drilldown report - * @param sourceKey The source key - * @param id The cohort ID - * @param conditionId The condition concept ID - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return The CohortConditionDrilldown detail object - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/condition/{conditionId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/condition/{conditionId} - */ - @GetMapping(value = "/{sourceKey}/{id}/condition/{conditionId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getConditionResults( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") int id, - @PathVariable("conditionId") int conditionId, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortConditionDrilldown result = cohortResultsService.getConditionResults( - sourceKey, id, conditionId, minCovariatePersonCountParam, minIntervalPersonCountParam, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis condition era treemap results for the given cohort definition id - * - * @summary Get condition era treemap - * @param sourceKey The source key - * @param id The cohort ID - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/conditionera/ - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/conditionera/ - */ - @GetMapping(value = "/{sourceKey}/{id}/conditionera/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getConditionEraTreemap( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") int id, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getConditionEraTreemap( - sourceKey, id, minCovariatePersonCountParam, minIntervalPersonCountParam, refresh); - return ok(result); - } - - /** - * Get the completed analyses IDs for the selected cohort and source key - * - * @summary Get completed analyses IDs - * @param sourceKey The source key - * @param id The cohort ID - * @return A list of completed analysis IDs - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/analyses - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/analyses - */ - @GetMapping(value = "/{sourceKey}/{id}/analyses", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCompletedAnalyses( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") String id) { - - List result = cohortResultsService.getCompletedAnalyses(sourceKey, id); - return ok(result); - } - - /** - * Get the analysis generation progress - * - * @summary Get analysis progress - * @param sourceKey The source key - * @param id The cohort ID - * @return The generation progress information - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/info - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/info - */ - @GetMapping(value = "/{sourceKey}/{id}/info", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getAnalysisProgress( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") Integer id) { - - Object result = cohortResultsService.getAnalysisProgress(sourceKey, id); - return ok(result); - } - - /** - * Queries for cohort analysis condition era drilldown results for the given cohort definition id and condition id - * - * @summary Get condition era drilldown report - * @param id The cohort ID - * @param conditionId The condition ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return The CohortConditionEraDrilldown object - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/conditionera/{conditionId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/conditionera/{conditionId} - */ - @GetMapping(value = "/{sourceKey}/{id}/conditionera/{conditionId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getConditionEraDrilldown( - @PathVariable("id") int id, - @PathVariable("conditionId") int conditionId, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortConditionEraDrilldown result = cohortResultsService.getConditionEraDrilldown( - id, conditionId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for drug analysis treemap results for the given cohort definition id - * - * @summary Get drug treemap - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/drug/ - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/drug/ - */ - @GetMapping(value = "/{sourceKey}/{id}/drug/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDrugTreemap( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getDrugTreemap( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis drug drilldown results for the given cohort definition id and drug id - * - * @summary Get drug drilldown report - * @param id The cohort ID - * @param drugId The drug concept ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortDrugDrilldown - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/drug/{drugId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/drug/{drugId} - */ - @GetMapping(value = "/{sourceKey}/{id}/drug/{drugId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getDrugResults( - @PathVariable("id") int id, - @PathVariable("drugId") int drugId, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortDrugDrilldown result = cohortResultsService.getDrugResults( - id, drugId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis drug era treemap results for the given cohort definition id - * - * @summary Get drug era treemap report - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/drugera/ - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/drugera/ - */ - @GetMapping(value = "/{sourceKey}/{id}/drugera/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDrugEraTreemap( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getDrugEraTreemap( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis drug era drilldown results for the given cohort definition id and drug id - * - * @summary Get drug era drilldown report - * @param id The cohort ID - * @param drugId The drug concept ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortDrugEraDrilldown - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/drugera/{drugId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/drugera/{drugId} - */ - @GetMapping(value = "/{sourceKey}/{id}/drugera/{drugId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getDrugEraResults( - @PathVariable("id") int id, - @PathVariable("drugId") int drugId, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortDrugEraDrilldown result = cohortResultsService.getDrugEraResults( - id, drugId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis person results for the given cohort definition id - * - * @summary Get the person report - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortPersonSummary - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/person - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/person - */ - @GetMapping(value = "/{sourceKey}/{id}/person", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getPersonResults( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortPersonSummary result = cohortResultsService.getPersonResults( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis cohort specific results for the given cohort definition id - * - * @summary Get cohort specific results - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortSpecificSummary - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/cohortspecific - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/cohortspecific - */ - @GetMapping(value = "/{sourceKey}/{id}/cohortspecific", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortSpecificResults( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortSpecificSummary result = cohortResultsService.getCohortSpecificResults( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis cohort specific treemap results for the given cohort definition id - * - * @summary Get cohort specific treemap - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortSpecificTreemap - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/cohortspecifictreemap - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/cohortspecifictreemap - */ - @GetMapping(value = "/{sourceKey}/{id}/cohortspecifictreemap", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortSpecificTreemapResults( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortSpecificTreemap result = cohortResultsService.getCohortSpecificTreemapResults( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis procedure drilldown results for the given cohort definition id and concept id - * - * @summary Get procedure drilldown report - * @param id The cohort ID - * @param conceptId The procedure concept ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/cohortspecificprocedure/{conceptId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/cohortspecificprocedure/{conceptId} - */ - @GetMapping(value = "/{sourceKey}/{id}/cohortspecificprocedure/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCohortProcedureDrilldown( - @PathVariable("id") int id, - @PathVariable("conceptId") int conceptId, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getCohortProcedureDrilldown( - id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis drug drilldown results for the given cohort definition id and concept id - * - * @summary Get drug drilldown report for specific concept - * @param id The cohort ID - * @param conceptId The drug concept ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/cohortspecificdrug/{conceptId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/cohortspecificdrug/{conceptId} - */ - @GetMapping(value = "/{sourceKey}/{id}/cohortspecificdrug/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCohortDrugDrilldown( - @PathVariable("id") int id, - @PathVariable("conceptId") int conceptId, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getCohortDrugDrilldown( - id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis condition drilldown results for the given cohort definition id and concept id - * - * @summary Get condition drilldown report by concept ID - * @param id The cohort ID - * @param conceptId The condition concept ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/cohortspecificcondition/{conceptId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/cohortspecificcondition/{conceptId} - */ - @GetMapping(value = "/{sourceKey}/{id}/cohortspecificcondition/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCohortConditionDrilldown( - @PathVariable("id") int id, - @PathVariable("conceptId") int conceptId, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getCohortConditionDrilldown( - id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis for observation treemap - * - * @summary Get observation treemap report - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/observation - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/observation - */ - @GetMapping(value = "/{sourceKey}/{id}/observation", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCohortObservationResults( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getCohortObservationResults( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis observation drilldown results for the given cohort definition id and observation concept id - * - * @summary Get observation drilldown report for a concept ID - * @param id The cohort ID - * @param conceptId The observation concept ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortObservationDrilldown - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/observation/{conceptId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/observation/{conceptId} - */ - @GetMapping(value = "/{sourceKey}/{id}/observation/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortObservationResultsDrilldown( - @PathVariable("id") int id, - @PathVariable("conceptId") int conceptId, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortObservationDrilldown result = cohortResultsService.getCohortObservationResultsDrilldown( - id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis for measurement treemap - * - * @summary Get measurement treemap report - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/measurement - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/measurement - */ - @GetMapping(value = "/{sourceKey}/{id}/measurement", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCohortMeasurementResults( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getCohortMeasurementResults( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis measurement drilldown results for the given cohort definition id and measurement concept id - * - * @summary Get measurement drilldown report for concept ID - * @param id The cohort ID - * @param conceptId The measurement concept ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortMeasurementDrilldown - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/measurement/{conceptId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/measurement/{conceptId} - */ - @GetMapping(value = "/{sourceKey}/{id}/measurement/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortMeasurementResultsDrilldown( - @PathVariable("id") int id, - @PathVariable("conceptId") int conceptId, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortMeasurementDrilldown result = cohortResultsService.getCohortMeasurementResultsDrilldown( - id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis observation period for the given cohort definition id - * - * @summary Get observation period report - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortObservationPeriod - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/observationperiod - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/observationperiod - */ - @GetMapping(value = "/{sourceKey}/{id}/observationperiod", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortObservationPeriod( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortObservationPeriod result = cohortResultsService.getCohortObservationPeriod( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis data density for the given cohort definition id - * - * @summary Get data density report - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortDataDensity - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/datadensity - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/datadensity - */ - @GetMapping(value = "/{sourceKey}/{id}/datadensity", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortDataDensity( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortDataDensity result = cohortResultsService.getCohortDataDensity( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis procedure treemap results for the given cohort definition id - * - * @summary Get procedure treemap report - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/procedure/ - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/procedure/ - */ - @GetMapping(value = "/{sourceKey}/{id}/procedure/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getProcedureTreemap( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getProcedureTreemap( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis procedures for the given cohort definition id and concept id - * - * @summary Get procedure drilldown report by concept ID - * @param id The cohort ID - * @param conceptId The procedure concept ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortProceduresDrillDown - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/procedure/{conceptId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/procedure/{conceptId} - */ - @GetMapping(value = "/{sourceKey}/{id}/procedure/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortProceduresDrilldown( - @PathVariable("id") int id, - @PathVariable("conceptId") int conceptId, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortProceduresDrillDown result = cohortResultsService.getCohortProceduresDrilldown( - id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis visit treemap results for the given cohort definition id - * - * @summary Get visit treemap report - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/visit/ - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/visit/ - */ - @GetMapping(value = "/{sourceKey}/{id}/visit/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getVisitTreemap( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getVisitTreemap( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Queries for cohort analysis visits for the given cohort definition id and concept id - * - * @summary Get visit drilldown for a visit concept ID - * @param id The cohort ID - * @param conceptId The visit concept ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortVisitsDrilldown - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/visit/{conceptId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/visit/{conceptId} - */ - @GetMapping(value = "/{sourceKey}/{id}/visit/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortVisitsDrilldown( - @PathVariable("id") int id, - @PathVariable("conceptId") int conceptId, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortVisitsDrilldown result = cohortResultsService.getCohortVisitsDrilldown( - id, conceptId, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Returns the summary for the cohort - * - * @summary Get cohort summary - * @param id The cohort ID - * @param sourceKey The source key - * @return CohortSummary - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/summarydata - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/summarydata - */ - @GetMapping(value = "/{sourceKey}/{id}/summarydata", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortSummaryData( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - - CohortSummary result = cohortResultsService.getCohortSummaryData(id, sourceKey); - return ok(result); - } - - /** - * Queries for cohort analysis death data for the given cohort definition id - * - * @summary Get death report - * @param id The cohort ID - * @param sourceKey The source key - * @param minCovariatePersonCountParam The minimum number of covariates per person - * @param minIntervalPersonCountParam The minimum interval person count - * @param refresh Boolean - refresh visualization data - * @return CohortDeathData - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/death - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/death - */ - @GetMapping(value = "/{sourceKey}/{id}/death", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortDeathData( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "min_covariate_person_count", required = false) Integer minCovariatePersonCountParam, - @RequestParam(value = "min_interval_person_count", required = false) Integer minIntervalPersonCountParam, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - CohortDeathData result = cohortResultsService.getCohortDeathData( - id, minCovariatePersonCountParam, minIntervalPersonCountParam, sourceKey, refresh); - return ok(result); - } - - /** - * Returns the summary for the cohort - * - * @summary Get cohort summary analyses - * @param id The cohort ID - * @param sourceKey The source key - * @return CohortSummary - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/summaryanalyses - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/summaryanalyses - */ - @GetMapping(value = "/{sourceKey}/{id}/summaryanalyses", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortSummaryAnalyses( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - - CohortSummary result = cohortResultsService.getCohortSummaryAnalyses(id, sourceKey); - return ok(result); - } - - /** - * Returns breakdown with counts about people in cohort - * - * @summary Get cohort breakdown report - * @param id The cohort ID - * @param sourceKey The source key - * @return Collection - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/breakdown - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/breakdown - */ - @GetMapping(value = "/{sourceKey}/{id}/breakdown", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCohortBreakdown( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - - Collection result = cohortResultsService.getCohortBreakdown(id, sourceKey); - return ok(result); - } - - /** - * Returns the count of all members of a generated cohort definition identifier - * - * @summary Get cohort member count - * @param id The cohort ID - * @param sourceKey The source key - * @return The cohort count - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/members/count - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/members/count - */ - @GetMapping(value = "/{sourceKey}/{id}/members/count", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCohortMemberCount( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - - Long result = cohortResultsService.getCohortMemberCount(id, sourceKey); - return ok(result); - } - - /** - * Returns all cohort analyses in the results/OHDSI schema for the given cohort_definition_id - * - * @summary Get the cohort analysis list for a cohort - * @param id The cohort ID - * @param sourceKey The source key - * @param retrieveFullDetail Boolean - when TRUE, the full analysis details are returned - * @return List of all cohort analyses and their statuses for the given cohort_definition_id - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id} - */ - @GetMapping(value = "/{sourceKey}/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCohortAnalysesForCohortDefinition( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "fullDetail", defaultValue = "true") boolean retrieveFullDetail) { - - List result = cohortResultsService.getCohortAnalysesForCohortDefinition(id, sourceKey, retrieveFullDetail); - return ok(result); - } - - /** - * Get the exposure cohort incidence rates. This function is not using a - * proper incidence rate so this should be viewed as informational only - * and not as a report - * - * @summary DO NOT USE - * @deprecated - * @param sourceKey The source key - * @param search The exposure cohort search - * @return List - * - * Jersey: POST /WebAPI/cohortresults/{sourceKey}/exposurecohortrates - * Spring MVC: POST /WebAPI/v2/cohortresults/{sourceKey}/exposurecohortrates - */ - @PostMapping(value = "/{sourceKey}/exposurecohortrates", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @Deprecated - public ResponseEntity> getExposureOutcomeCohortRates( - @PathVariable("sourceKey") String sourceKey, - @RequestBody ExposureCohortSearch search) { - - List result = cohortResultsService.getExposureOutcomeCohortRates(sourceKey, search); - return ok(result); - } - - /** - * Provides a time to event calculation but it is unclear how this works. - * - * @summary DO NOT USE - * @deprecated - * @param sourceKey The source key - * @param search The exposure cohort search - * @return List - * - * Jersey: POST /WebAPI/cohortresults/{sourceKey}/timetoevent - * Spring MVC: POST /WebAPI/v2/cohortresults/{sourceKey}/timetoevent - */ - @PostMapping(value = "/{sourceKey}/timetoevent", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @Deprecated - public ResponseEntity> getTimeToEventDrilldown( - @PathVariable("sourceKey") String sourceKey, - @RequestBody ExposureCohortSearch search) { - - List result = cohortResultsService.getTimeToEventDrilldown(sourceKey, search); - return ok(result); - } - - /** - * Provides a predictor calculation but it is unclear how this works. - * - * @summary DO NOT USE - * @deprecated - * @param sourceKey The source key - * @param search The exposure cohort search - * @return List - * - * Jersey: POST /WebAPI/cohortresults/{sourceKey}/predictors - * Spring MVC: POST /WebAPI/v2/cohortresults/{sourceKey}/predictors - */ - @PostMapping(value = "/{sourceKey}/predictors", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @Deprecated - public ResponseEntity> getExposureOutcomeCohortPredictors( - @PathVariable("sourceKey") String sourceKey, - @RequestBody ExposureCohortSearch search) { - - List result = cohortResultsService.getExposureOutcomeCohortPredictors(sourceKey, search); - return ok(result); - } - - /** - * Returns heracles heel results (data quality issues) for the given cohort definition id - * - * @summary Get HERACLES heel report - * @param id The cohort ID - * @param sourceKey The source key - * @param refresh Boolean - refresh visualization data - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/heraclesheel - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/heraclesheel - */ - @GetMapping(value = "/{sourceKey}/{id}/heraclesheel", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getHeraclesHeel( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { - - List result = cohortResultsService.getHeraclesHeel(id, sourceKey, refresh); - return ok(result); - } - - /** - * Provides a data completeness report for a cohort - * - * @summary Get data completeness report - * @param id The cohort ID - * @param sourceKey The source key - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/datacompleteness - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/datacompleteness - */ - @GetMapping(value = "/{sourceKey}/{id}/datacompleteness", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDataCompleteness( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - - List result = cohortResultsService.getDataCompleteness(id, sourceKey); - return ok(result); - } - - /** - * Provide an entropy report for a cohort - * - * @summary Get entropy report - * @param id The cohort ID - * @param sourceKey The source key - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/entropy - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/entropy - */ - @GetMapping(value = "/{sourceKey}/{id}/entropy", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getEntropy( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - - List result = cohortResultsService.getEntropy(id, sourceKey); - return ok(result); - } - - /** - * Provide a full entropy report for a cohort - * - * @summary Get full entropy report - * @param id The cohort ID - * @param sourceKey The source key - * @return List - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/allentropy - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/allentropy - */ - @GetMapping(value = "/{sourceKey}/{id}/allentropy", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getAllEntropy( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - - List result = cohortResultsService.getAllEntropy(id, sourceKey); - return ok(result); - } - - /** - * Get the healthcare utilization exposure report for a specific window - * - * @summary Get healthcare utilization report for selected time window - * @param id The cohort ID - * @param sourceKey The source key - * @param window The time window - * @param periodType The period type - * @return HealthcareExposureReport - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/exposure/{window} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/exposure/{window} - */ - @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/exposure/{window}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getHealthcareUtilizationExposureReport( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @PathVariable("window") WindowType window, - @RequestParam(value = "periodType", defaultValue = "ww") PeriodType periodType) { - - HealthcareExposureReport result = cohortResultsService.getHealthcareUtilizationExposureReport(id, sourceKey, window, periodType); - return ok(result); - } - - /** - * Get the healthcare utilization periods - * - * @summary Get healthcare utilization periods - * @param id The cohort ID - * @param sourceKey The source key - * @param window The time window - * @return A list of the periods - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/periods/{window} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/periods/{window} - */ - @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/periods/{window}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getHealthcareUtilizationPeriods( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @PathVariable("window") WindowType window) { - - List result = cohortResultsService.getHealthcareUtilizationPeriods(id, sourceKey, window); - return ok(result); - } - - /** - * Get the healthcare utilization report by window, visit status, period type, visit concept, visit type concept and cost type concept. - * - * @summary Get healthcare utilization visit report - * @param id The cohort ID - * @param sourceKey The source key - * @param window The time window - * @param visitStat The visit status - * @param periodType The period type - * @param visitConcept The visit concept ID - * @param visitTypeConcept The visit type concept ID - * @param costTypeConcept The cost type concept ID - * @return HealthcareVisitUtilizationReport - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/visit/{window}/{visitStat} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/visit/{window}/{visitStat} - */ - @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/visit/{window}/{visitStat}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getHealthcareUtilizationVisitReport( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @PathVariable("window") WindowType window, - @PathVariable("visitStat") VisitStatType visitStat, - @RequestParam(value = "periodType", defaultValue = "ww") PeriodType periodType, - @RequestParam(value = "visitConcept", required = false) Long visitConcept, - @RequestParam(value = "visitTypeConcept", required = false) Long visitTypeConcept, - @RequestParam(value = "costTypeConcept", defaultValue = "31968") Long costTypeConcept) { - - HealthcareVisitUtilizationReport result = cohortResultsService.getHealthcareUtilizationVisitReport( - id, sourceKey, window, visitStat, periodType, visitConcept, visitTypeConcept, costTypeConcept); - return ok(result); - } - - /** - * Get the healthcare utilization summary report by drug and cost type concept - * - * @summary Get healthcare utilization drug summary report - * @param id The cohort ID - * @param sourceKey The source key - * @param window The time window - * @param drugTypeConceptId The drug type concept ID - * @param costTypeConceptId The cost type concept ID - * @return HealthcareDrugUtilizationSummary - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/drug/{window} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/drug/{window} - */ - @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/drug/{window}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getHealthcareUtilizationDrugSummaryReport( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @PathVariable("window") WindowType window, - @RequestParam(value = "drugType", required = false) Long drugTypeConceptId, - @RequestParam(value = "costType", defaultValue = "31968") Long costTypeConceptId) { - - HealthcareDrugUtilizationSummary result = cohortResultsService.getHealthcareUtilizationDrugSummaryReport( - id, sourceKey, window, drugTypeConceptId, costTypeConceptId); - return ok(result); - } - - /** - * Get the healthcare utilization detail report by drug and cost type concept - * - * @summary Get healthcare utilization drug detail report - * @param id The cohort ID - * @param sourceKey The source key - * @param window The time window - * @param drugConceptId The drug concept ID - * @param periodType The period type - * @param drugTypeConceptId The drug type concept ID - * @param costTypeConceptId The cost type concept ID - * @return HealthcareDrugUtilizationDetail - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/drug/{window}/{drugConceptId} - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/drug/{window}/{drugConceptId} - */ - @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/drug/{window}/{drugConceptId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getHealthcareUtilizationDrugDetailReport( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @PathVariable("window") WindowType window, - @PathVariable("drugConceptId") Long drugConceptId, - @RequestParam(value = "periodType", defaultValue = "ww") PeriodType periodType, - @RequestParam(value = "drugType", required = false) Long drugTypeConceptId, - @RequestParam(value = "costType", defaultValue = "31968") Long costTypeConceptId) { - - HealthcareDrugUtilizationDetail result = cohortResultsService.getHealthcareUtilizationDrugDetailReport( - id, sourceKey, window, drugConceptId, periodType, drugTypeConceptId, costTypeConceptId); - return ok(result); - } - - /** - * Get the drug type concepts for the selected drug concept ID - * - * @summary Get drug types for healthcare utilization report - * @param id The cohort ID - * @param sourceKey The source key - * @param drugConceptId The drug concept ID - * @return A list of concepts of drug types - * - * Jersey: GET /WebAPI/cohortresults/{sourceKey}/{id}/healthcareutilization/drugtypes - * Spring MVC: GET /WebAPI/v2/cohortresults/{sourceKey}/{id}/healthcareutilization/drugtypes - */ - @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/drugtypes", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDrugTypes( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey, - @RequestParam(value = "drugConceptId", required = false) Long drugConceptId) { - - List result = cohortResultsService.getDrugTypes(id, sourceKey, drugConceptId); - return ok(result); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/ConceptSetMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/ConceptSetMvcController.java deleted file mode 100644 index a2dbac17dc..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/ConceptSetMvcController.java +++ /dev/null @@ -1,1062 +0,0 @@ -/* - * Copyright 2015 fdefalco. - * - * 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.mvc.controller; - -import java.io.ByteArrayOutputStream; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.shiro.authz.UnauthorizedException; -import org.ohdsi.circe.vocabulary.ConceptSetExpression; -import org.ohdsi.vocabulary.Concept; -import org.ohdsi.webapi.check.CheckResult; -import org.ohdsi.webapi.check.checker.conceptset.ConceptSetChecker; -import org.ohdsi.webapi.conceptset.ConceptSet; -import org.ohdsi.webapi.conceptset.ConceptSetExport; -import org.ohdsi.webapi.conceptset.ConceptSetGenerationInfo; -import org.ohdsi.webapi.conceptset.ConceptSetGenerationInfoRepository; -import org.ohdsi.webapi.conceptset.ConceptSetItem; -import org.ohdsi.webapi.conceptset.ConceptSetItemRepository; -import org.ohdsi.webapi.conceptset.ConceptSetRepository; -import org.ohdsi.webapi.conceptset.dto.ConceptSetVersionFullDTO; -import org.ohdsi.webapi.conceptset.annotation.ConceptSetAnnotation; -import org.ohdsi.webapi.conceptset.annotation.ConceptSetAnnotationRepository; -import org.ohdsi.webapi.exception.ConceptNotExistException; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.security.PermissionService; -import org.ohdsi.webapi.service.VocabularyService; -import org.ohdsi.webapi.service.annotations.SearchDataTransformer; -import org.ohdsi.webapi.service.dto.AnnotationDetailsDTO; -import org.ohdsi.webapi.service.dto.ConceptSetDTO; -import org.ohdsi.webapi.service.dto.SaveConceptSetAnnotationsRequest; -import org.ohdsi.webapi.service.dto.AnnotationDTO; -import org.ohdsi.webapi.service.dto.CopyAnnotationsRequest; -import org.ohdsi.webapi.shiro.Entities.UserEntity; -import org.ohdsi.webapi.shiro.Entities.UserRepository; -import org.ohdsi.webapi.shiro.management.datasource.SourceAccessor; -import org.ohdsi.webapi.source.Source; -import org.ohdsi.webapi.source.SourceInfo; -import org.ohdsi.webapi.source.SourceService; -import org.ohdsi.webapi.tag.TagService; -import org.ohdsi.webapi.tag.domain.Tag; -import org.ohdsi.webapi.tag.dto.TagNameListRequestDTO; -import org.ohdsi.webapi.util.ExportUtil; -import org.ohdsi.webapi.util.NameUtils; -import org.ohdsi.webapi.util.ExceptionUtils; -import org.ohdsi.webapi.versioning.domain.ConceptSetVersion; -import org.ohdsi.webapi.versioning.domain.Version; -import org.ohdsi.webapi.versioning.domain.VersionBase; -import org.ohdsi.webapi.versioning.domain.VersionType; -import org.ohdsi.webapi.versioning.dto.VersionDTO; -import org.ohdsi.webapi.versioning.dto.VersionUpdateDTO; -import org.ohdsi.webapi.versioning.service.VersionService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -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.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionTemplate; -import org.springframework.web.bind.annotation.*; - -import static org.ohdsi.webapi.service.ConceptSetService.CachingSetup.CONCEPT_SET_LIST_CACHE; - -/** - * Provides REST services for working with concept sets using Spring MVC. - * This is a Spring MVC migration of the original Jersey-based ConceptSetService. - * - * @summary Concept Set (Spring MVC) - */ -@RestController -@RequestMapping("/conceptset") -@Transactional -public class ConceptSetMvcController extends AbstractMvcController { - - private static final Logger log = LoggerFactory.getLogger(ConceptSetMvcController.class); - - @Autowired - private ConceptSetRepository conceptSetRepository; - - @Autowired - private ConceptSetItemRepository conceptSetItemRepository; - - @Autowired - private ConceptSetAnnotationRepository conceptSetAnnotationRepository; - - @Autowired - private ConceptSetGenerationInfoRepository conceptSetGenerationInfoRepository; - - @Autowired - private VocabularyService vocabService; - - @Autowired - private SourceService sourceService; - - @Autowired - private SourceAccessor sourceAccessor; - - @Autowired - private UserRepository userRepository; - - @Autowired - private GenericConversionService conversionService; - - @Autowired - private PermissionService permissionService; - - @Autowired - private ConceptSetChecker checker; - - @Autowired - private VersionService versionService; - - @Autowired - private SearchDataTransformer searchDataTransformer; - - @Autowired - private ObjectMapper mapper; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Autowired - private TagService tagService; - - @Value("${security.defaultGlobalReadPermissions}") - private boolean defaultGlobalReadPermissions; - - public static final String COPY_NAME = "copyName"; - - protected ConceptSetRepository getConceptSetRepository() { - return conceptSetRepository; - } - - protected ConceptSetItemRepository getConceptSetItemRepository() { - return conceptSetItemRepository; - } - - protected ConceptSetAnnotationRepository getConceptSetAnnotationRepository() { - return conceptSetAnnotationRepository; - } - - protected TransactionTemplate getTransactionTemplate() { - return transactionTemplate; - } - - protected UserEntity getCurrentUserEntity() { - return userRepository.findByLogin(security.getSubject()); - } - - protected void checkOwnerOrAdminOrGranted(ConceptSet entity) { - // Check permission - if user doesn't have write access, this will throw an exception - if (!permissionService.hasWriteAccess(entity)) { - throw new org.apache.shiro.authz.UnauthorizedException("No write access to this concept set"); - } - } - - protected void assignTag(ConceptSet entity, int tagId) { - if (Objects.nonNull(entity)) { - Tag tag = tagService.getById(tagId); - if (Objects.nonNull(tag)) { - entity.getTags().add(tag); - getConceptSetRepository().save(entity); - } - } - } - - protected void unassignTag(ConceptSet entity, int tagId) { - if (Objects.nonNull(entity)) { - Set tags = entity.getTags().stream() - .filter(t -> t.getId() != tagId) - .collect(Collectors.toSet()); - entity.setTags(tags); - getConceptSetRepository().save(entity); - } - } - - protected List listByTags( - List entities, - List names, - Class clazz) { - return entities.stream() - .filter(e -> e.getTags().stream() - .map(tag -> tag.getName().toLowerCase(Locale.ROOT)) - .anyMatch(names::contains)) - .map(e -> conversionService.convert(e, clazz)) - .collect(Collectors.toList()); - } - - /** - * Get the concept set based in the identifier - * - * @summary Get concept set by ID - * @param id The concept set ID - * @return The concept set definition - */ - @GetMapping("/{id}") - public ResponseEntity getConceptSet(@PathVariable("id") final int id) { - ConceptSet conceptSet = getConceptSetRepository().findById(id).orElse(null); - ExceptionUtils.throwNotFoundExceptionIfNull(conceptSet, String.format("There is no concept set with id = %d.", id)); - return ok(conversionService.convert(conceptSet, ConceptSetDTO.class)); - } - - /** - * Get the full list of concept sets in the WebAPI database - * - * @summary Get all concept sets - * @return A list of all concept sets in the WebAPI database - */ - @GetMapping("/") - @Cacheable(cacheNames = CONCEPT_SET_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()") - public ResponseEntity> getConceptSets() { - Collection result = getTransactionTemplate().execute( - transactionStatus -> StreamSupport.stream(getConceptSetRepository().findAll().spliterator(), false) - .filter(!defaultGlobalReadPermissions ? entity -> permissionService.hasReadAccess(entity) : entity -> true) - .map(conceptSet -> { - ConceptSetDTO dto = conversionService.convert(conceptSet, ConceptSetDTO.class); - permissionService.fillWriteAccess(conceptSet, dto); - permissionService.fillReadAccess(conceptSet, dto); - return dto; - }) - .collect(Collectors.toList())); - return ok(result); - } - - /** - * Get the concept set items for a selected concept set ID. - * - * @summary Get the concept set items - * @param id The concept set identifier - * @return A list of concept set items - */ - @GetMapping("/{id}/items") - public ResponseEntity> getConceptSetItems(@PathVariable("id") final int id) { - return ok(getConceptSetItemRepository().findAllByConceptSetId(id)); - } - - /** - * Get the concept set expression for a selected version of the expression - * - * @summary Get concept set expression by version - * @param id The concept set ID - * @param version The version identifier - * @return The concept set expression - */ - @GetMapping("/{id}/version/{version}/expression") - public ResponseEntity getConceptSetExpressionByVersion( - @PathVariable("id") final int id, - @PathVariable("version") final int version) { - SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); - if (sourceInfo == null) { - throw new UnauthorizedException(); - } - return ok(getConceptSetExpression(id, version, sourceInfo)); - } - - /** - * Get the concept set expression by version for the selected - * source key. NOTE: This method requires the specification - * of a source key but it does not appear to be used by the underlying - * code. - * - * @summary Get concept set expression by version and source. - * @param id The concept set identifier - * @param version The version of the concept set - * @param sourceKey The source key - * @return The concept set expression for the selected version - */ - @GetMapping("/{id}/version/{version}/expression/{sourceKey}") - public ResponseEntity getConceptSetExpressionByVersionAndSource( - @PathVariable("id") final int id, - @PathVariable("version") final int version, - @PathVariable("sourceKey") final String sourceKey) { - SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); - if (sourceInfo == null) { - throw new UnauthorizedException(); - } - return ok(getConceptSetExpression(id, version, sourceInfo)); - } - - /** - * Get the concept set expression by identifier - * - * @summary Get concept set by ID - * @param id The concept set identifier - * @return The concept set expression - */ - @GetMapping("/{id}/expression") - public ResponseEntity getConceptSetExpressionById(@PathVariable("id") final int id) { - SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); - if (sourceInfo == null) { - throw new UnauthorizedException(); - } - return ok(getConceptSetExpression(id, null, sourceInfo)); - } - - /** - * Get the concept set expression by identifier and source key - * - * @summary Get concept set by ID and source - * @param id The concept set ID - * @param sourceKey The source key - * @return The concept set expression - */ - @GetMapping("/{id}/expression/{sourceKey}") - public ResponseEntity getConceptSetExpressionByIdAndSource( - @PathVariable("id") final int id, - @PathVariable("sourceKey") final String sourceKey) { - Source source = sourceService.findBySourceKey(sourceKey); - sourceAccessor.checkAccess(source); - return ok(getConceptSetExpression(id, null, source.getSourceInfo())); - } - - private ConceptSetExpression getConceptSetExpression(int id, Integer version, SourceInfo sourceInfo) { - HashMap map = new HashMap<>(); - - // create our expression to return - ConceptSetExpression expression = new ConceptSetExpression(); - ArrayList expressionItems = new ArrayList<>(); - - List repositoryItems = new ArrayList<>(); - if (Objects.isNull(version)) { - getConceptSetItemRepository().findAllByConceptSetId(id).forEach(repositoryItems::add); - } else { - ConceptSetVersionFullDTO dto = getVersion(id, version); - repositoryItems.addAll(dto.getItems()); - } - - // collect the unique concept IDs so we can load the concept object later. - for (ConceptSetItem csi : repositoryItems) { - map.put(csi.getConceptId(), null); - } - - // lookup the concepts we need information for - long[] identifiers = new long[map.size()]; - int identifierIndex = 0; - for (Long identifier : map.keySet()) { - identifiers[identifierIndex] = identifier; - identifierIndex++; - } - - String sourceKey; - if (Objects.isNull(sourceInfo)) { - sourceKey = sourceService.getPriorityVocabularySource().getSourceKey(); - } else { - sourceKey = sourceInfo.sourceKey; - } - - Collection concepts = vocabService.executeIdentifierLookup(sourceKey, identifiers); - if (concepts.size() != identifiers.length) { - String ids = Arrays.stream(identifiers).boxed() - .filter(identifier -> concepts.stream().noneMatch(c -> c.conceptId.equals(identifier))) - .map(String::valueOf) - .collect(Collectors.joining(",", "(", ")")); - throw new ConceptNotExistException("Current data source does not contain required concepts " + ids); - } - for(Concept concept : concepts) { - map.put(concept.conceptId, concept); // associate the concept object to the conceptID in the map - } - - // put the concept information into the expression along with the concept set item information - for (ConceptSetItem repositoryItem : repositoryItems) { - ConceptSetExpression.ConceptSetItem currentItem = new ConceptSetExpression.ConceptSetItem(); - currentItem.concept = map.get(repositoryItem.getConceptId()); - currentItem.includeDescendants = (repositoryItem.getIncludeDescendants() == 1); - currentItem.includeMapped = (repositoryItem.getIncludeMapped() == 1); - currentItem.isExcluded = (repositoryItem.getIsExcluded() == 1); - expressionItems.add(currentItem); - } - expression.items = expressionItems.toArray(new ConceptSetExpression.ConceptSetItem[0]); // this will return a new array - - return expression; - } - - /** - * Check if the concept set name exists (DEPRECATED) - * - * @summary DO NOT USE - * @deprecated - * @param id The concept set ID - * @param name The concept set name - * @return The concept set expression - */ - @Deprecated - @GetMapping("/{id}/{name}/exists") - public ResponseEntity> getConceptSetExistsDeprecated( - @PathVariable("id") final int id, - @PathVariable("name") String name) { - String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: conceptset/{id}/exists?name={name}"; - Collection cs = getConceptSetRepository().conceptSetExists(id, name); - return ResponseEntity.ok() - .header("Warning", "299 - " + warningMessage) - .body(cs); - } - - /** - * Check if a concept set with the same name exists in the WebAPI - * database. The name is checked against the selected concept set ID - * to ensure that only the selected concept set ID has the name specified. - * - * @summary Concept set with same name exists - * @param id The concept set ID - * @param name The name of the concept set - * @return The count of concept sets with the name, excluding the - * specified concept set ID. - */ - @GetMapping("/{id}/exists") - public ResponseEntity getCountCSetWithSameName( - @PathVariable("id") final int id, - @RequestParam(value = "name", required = false) String name) { - return ok(getConceptSetRepository().getCountCSetWithSameName(id, name)); - } - - /** - * Update the concept set items for the selected concept set ID in the - * WebAPI database. - * - * The concept set has two parts: 1) the elements of the ConceptSetDTO that - * consist of the identifier, name, etc. 2) the concept set items which - * contain the concepts and their mapping (i.e. include descendants). - * - * @summary Update concept set items - * @param id The concept set ID - * @param items An array of ConceptSetItems - * @return Boolean: true if the save is successful - */ - @PutMapping("/{id}/items") - public ResponseEntity saveConceptSetItems( - @PathVariable("id") final int id, - @RequestBody ConceptSetItem[] items) { - getConceptSetItemRepository().deleteByConceptSetId(id); - - for (ConceptSetItem csi : items) { - // ID must be set to null in case of copying from version, so the new item will be created - csi.setId(0); - csi.setConceptSetId(id); - getConceptSetItemRepository().save(csi); - } - - return ok(true); - } - - /** - * Exports a list of concept sets, based on the conceptSetList argument, - * to one or more comma separated value (CSV) file(s), compresses the files - * into a ZIP file and sends the ZIP file to the client. - * - * @summary Export concept set list to CSV files - * @param conceptSetList A list of concept set identifiers in the format - * conceptset=++ - * @return - * @throws Exception - */ - @GetMapping("/exportlist") - public ResponseEntity exportConceptSetList( - @RequestParam("conceptsets") final String conceptSetList) throws Exception { - ArrayList conceptSetIds = new ArrayList<>(); - try { - String[] conceptSetItems = conceptSetList.split("\\+"); - for(String csi : conceptSetItems) { - conceptSetIds.add(Integer.valueOf(csi)); - } - if (conceptSetIds.size() <= 0) { - throw new IllegalArgumentException("You must supply a querystring value for conceptsets that is of the form: ?conceptset=++"); - } - } catch (Exception e) { - throw e; - } - - ByteArrayOutputStream baos; - Source source = sourceService.getPriorityVocabularySource(); - ArrayList cs = new ArrayList<>(); - try { - // Load all of the concept sets requested - for (int i = 0; i < conceptSetIds.size(); i++) { - // Get the concept set information - cs.add(getConceptSetForExport(conceptSetIds.get(i), new SourceInfo(source))); - } - // Write Concept Set Expression to a CSV - baos = ExportUtil.writeConceptSetExportToCSVAndZip(cs); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); - headers.setContentDispositionFormData("attachment", "conceptSetExport.zip"); - - return ResponseEntity.ok() - .headers(headers) - .body(baos.toByteArray()); - - } catch (Exception ex) { - throw ex; - } - } - - /** - * Exports a single concept set to a comma separated value (CSV) - * file, compresses to a ZIP file and sends to the client. - * - * @param id The concept set ID - * @return A zip file containing the exported concept set - * @throws Exception - */ - @GetMapping("/{id}/export") - public ResponseEntity exportConceptSetToCSV(@PathVariable("id") final String id) throws Exception { - return this.exportConceptSetList(id); - } - - /** - * Save a new concept set to the WebAPI database - * - * @summary Create a new concept set - * @param conceptSetDTO The concept set to save - * @return The concept set saved with the concept set identifier - */ - @PostMapping("/") - @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) - public ResponseEntity createConceptSet(@RequestBody ConceptSetDTO conceptSetDTO) { - UserEntity user = getCurrentUserEntity(); - ConceptSet conceptSet = conversionService.convert(conceptSetDTO, ConceptSet.class); - ConceptSet updated = new ConceptSet(); - updated.setCreatedBy(user); - updated.setCreatedDate(new Date()); - updated.setTags(null); - updateConceptSet(updated, conceptSet); - return ok(conversionService.convert(updated, ConceptSetDTO.class)); - } - - /** - * Creates a concept set name, based on the selected concept set ID, - * that is used when generating a copy of an existing concept set. This - * function is generally used in conjunction with the copy endpoint to - * create a unique name and then save a copy of an existing concept set. - * - * @summary Get concept set name suggestion for copying - * @param id The concept set ID - * @return A map of the new concept set name and the existing concept set - * name - */ - @GetMapping("/{id}/copy-name") - public ResponseEntity> getNameForCopy(@PathVariable("id") final int id) { - ConceptSetDTO source = getConceptSet(id).getBody(); - String name = NameUtils.getNameForCopy(source.getName(), this::getNamesLike, getConceptSetRepository().findByName(source.getName())); - return ok(Collections.singletonMap(COPY_NAME, name)); - } - - public List getNamesLike(String copyName) { - return getConceptSetRepository().findAllByNameStartsWith(copyName).stream().map(ConceptSet::getName).collect(Collectors.toList()); - } - - /** - * Updates the concept set for the selected concept set. - * - * The concept set has two parts: 1) the elements of the ConceptSetDTO that - * consist of the identifier, name, etc. 2) the concept set items which - * contain the concepts and their mapping (i.e. include descendants). - * - * @summary Update concept set - * @param id The concept set identifier - * @param conceptSetDTO The concept set header - * @return The - * @throws Exception - */ - @PutMapping("/{id}") - @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) - public ResponseEntity updateConceptSet( - @PathVariable("id") final int id, - @RequestBody ConceptSetDTO conceptSetDTO) throws Exception { - ConceptSet updated = getConceptSetRepository().findById(id).orElse(null); - if (updated == null) { - throw new Exception("Concept Set does not exist."); - } - - saveVersion(id); - - ConceptSet conceptSet = conversionService.convert(conceptSetDTO, ConceptSet.class); - return ok(conversionService.convert(updateConceptSet(updated, conceptSet), ConceptSetDTO.class)); - } - - private ConceptSet updateConceptSet(ConceptSet dst, ConceptSet src) { - UserEntity user = getCurrentUserEntity(); - dst.setName(src.getName()); - dst.setDescription(src.getDescription()); - dst.setModifiedDate(new Date()); - dst.setModifiedBy(user); - - dst = this.getConceptSetRepository().save(dst); - return dst; - } - - private ConceptSetExport getConceptSetForExport(int conceptSetId, SourceInfo vocabSource) { - ConceptSetExport cs = new ConceptSetExport(); - - // Set the concept set id - cs.ConceptSetId = conceptSetId; - // Get the concept set information - cs.ConceptSetName = this.getConceptSet(conceptSetId).getBody().getName(); - // Get the concept set expression - cs.csExpression = this.getConceptSetExpressionById(conceptSetId).getBody(); - - // Lookup the identifiers - cs.identifierConcepts = vocabService.executeIncludedConceptLookup(vocabSource.sourceKey, cs.csExpression); - // Lookup the mapped items - cs.mappedConcepts = vocabService.executeMappedLookup(vocabSource.sourceKey, cs.csExpression); - - return cs; - } - - /** - * Get the concept set generation information for the selected concept - * set ID. This function only works with the configuration of the CEM - * data source. - * - * @link https://github.com/OHDSI/CommonEvidenceModel/wiki - * - * @summary Get concept set generation info - * @param id The concept set identifier. - * @return A collection of concept set generation info objects - */ - @GetMapping("/{id}/generationinfo") - public ResponseEntity> getConceptSetGenerationInfo(@PathVariable("id") final int id) { - return ok(this.conceptSetGenerationInfoRepository.findAllByConceptSetId(id)); - } - - /** - * Delete the selected concept set by concept set identifier - * - * @summary Delete concept set - * @param id The concept set ID - */ - @DeleteMapping("/{id}") - @Transactional(rollbackFor = Exception.class, noRollbackFor = EmptyResultDataAccessException.class) - @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) - public ResponseEntity deleteConceptSet(@PathVariable("id") final int id) { - // Remove any generation info - try { - this.conceptSetGenerationInfoRepository.deleteByConceptSetId(id); - } catch (EmptyResultDataAccessException e) { - // Ignore - there may be no data - log.warn("Failed to delete Generation Info by ConceptSet with ID = {}, {}", id, e); - } - catch (Exception e) { - throw e; - } - - // Remove the concept set items - try { - getConceptSetItemRepository().deleteByConceptSetId(id); - } catch (EmptyResultDataAccessException e) { - // Ignore - there may be no data - log.warn("Failed to delete ConceptSet items with ID = {}, {}", id, e); - } - catch (Exception e) { - throw e; - } - - // Remove the concept set - try { - getConceptSetRepository().deleteById(id); - } catch (EmptyResultDataAccessException e) { - // Ignore - there may be no data - log.warn("Failed to delete ConceptSet with ID = {}, {}", id, e); - } - catch (Exception e) { - throw e; - } - - return ok(); - } - - /** - * Assign tag to Concept Set - * - * @summary Assign concept set tag - * @since v2.10.0 - * @param id The concept set ID - * @param tagId The tag ID - */ - @PostMapping("/{id}/tag/") - @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) - public ResponseEntity assignTag( - @PathVariable("id") final Integer id, - @RequestBody int tagId) { - ConceptSet entity = getConceptSetRepository().findById(id).orElse(null); - assignTag(entity, tagId); - return ok(); - } - - /** - * Unassign tag from Concept Set - * - * @summary Remove tag from concept set - * @since v2.10.0 - * @param id The concept set ID - * @param tagId The tag ID - */ - @DeleteMapping("/{id}/tag/{tagId}") - @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) - public ResponseEntity unassignTag( - @PathVariable("id") final Integer id, - @PathVariable("tagId") final int tagId) { - ConceptSet entity = getConceptSetRepository().findById(id).orElse(null); - unassignTag(entity, tagId); - return ok(); - } - - /** - * Assign protected tag to Concept Set - * - * @summary Assign protected concept set tag - * @since v2.10.0 - * @param id The concept set ID - * @param tagId The tag ID - */ - @PostMapping("/{id}/protectedtag/") - public ResponseEntity assignPermissionProtectedTag( - @PathVariable("id") final int id, - @RequestBody final int tagId) { - assignTag(id, tagId); - return ok(); - } - - /** - * Unassign protected tag from Concept Set - * - * @summary Remove protected concept set tag - * @since v2.10.0 - * @param id The concept set ID - * @param tagId The tag ID - */ - @DeleteMapping("/{id}/protectedtag/{tagId}") - @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) - public ResponseEntity unassignPermissionProtectedTag( - @PathVariable("id") final int id, - @PathVariable("tagId") final int tagId) { - unassignTag(id, tagId); - return ok(); - } - - /** - * Checks a concept set for diagnostic problems. At this time, - * this appears to be an endpoint used to check to see which tags - * are applied to a concept set. - * - * @summary Concept set tag check - * @since v2.10.0 - * @param conceptSetDTO The concept set - * @return A check result - */ - @PostMapping("/check") - public ResponseEntity runDiagnostics(@RequestBody ConceptSetDTO conceptSetDTO) { - return ok(new CheckResult(checker.check(conceptSetDTO))); - } - - /** - * Get a list of versions of the selected concept set - * - * @summary Get concept set version list - * @since v2.10.0 - * @param id The concept set ID - * @return A list of version information - */ - @GetMapping("/{id}/version/") - public ResponseEntity> getVersions(@PathVariable("id") final int id) { - List versions = versionService.getVersions(VersionType.CONCEPT_SET, id); - List result = versions.stream() - .map(v -> conversionService.convert(v, VersionDTO.class)) - .collect(Collectors.toList()); - return ok(result); - } - - /** - * Get a specific version of a concept set - * - * @summary Get concept set by version - * @since v2.10.0 - * @param id The concept set ID - * @param version The version ID - * @return The concept set for the selected version - */ - @GetMapping("/{id}/version/{version}") - public ResponseEntity getVersionResponse( - @PathVariable("id") final int id, - @PathVariable("version") final int version) { - return ok(getVersion(id, version)); - } - - private ConceptSetVersionFullDTO getVersion(int id, int version) { - checkVersion(id, version, false); - ConceptSetVersion conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version); - return conversionService.convert(conceptSetVersion, ConceptSetVersionFullDTO.class); - } - - /** - * Update a specific version of a selected concept set - * - * @summary Update a concept set version - * @since v2.10.0 - * @param id The concept set ID - * @param version The version ID - * @param updateDTO The version update - * @return The version information - */ - @PutMapping("/{id}/version/{version}") - public ResponseEntity updateVersion( - @PathVariable("id") final int id, - @PathVariable("version") final int version, - @RequestBody VersionUpdateDTO updateDTO) { - checkVersion(id, version); - updateDTO.setAssetId(id); - updateDTO.setVersion(version); - ConceptSetVersion updated = versionService.update(VersionType.CONCEPT_SET, updateDTO); - return ok(conversionService.convert(updated, VersionDTO.class)); - } - - /** - * Delete a version of a concept set - * - * @summary Delete a concept set version - * @since v2.10.0 - * @param id The concept ID - * @param version The version ID - */ - @DeleteMapping("/{id}/version/{version}") - public ResponseEntity deleteVersion( - @PathVariable("id") final int id, - @PathVariable("version") final int version) { - checkVersion(id, version); - versionService.delete(VersionType.CONCEPT_SET, id, version); - return ok(); - } - - /** - * Create a new asset from a specific version of the selected - * concept set - * - * @summary Create a concept set copy from a specific concept set version - * @since v2.10.0 - * @param id The concept set ID - * @param version The version ID - * @return The concept set copy - */ - @PutMapping("/{id}/version/{version}/createAsset") - @CacheEvict(cacheNames = CONCEPT_SET_LIST_CACHE, allEntries = true) - public ResponseEntity copyAssetFromVersion( - @PathVariable("id") final int id, - @PathVariable("version") final int version) { - checkVersion(id, version, false); - ConceptSetVersion conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version); - - ConceptSetVersionFullDTO fullDTO = conversionService.convert(conceptSetVersion, ConceptSetVersionFullDTO.class); - ConceptSetDTO conceptSetDTO = fullDTO.getEntityDTO(); - // Reset id so it won't be used during saving - conceptSetDTO.setId(0); - conceptSetDTO.setTags(null); - conceptSetDTO.setName(NameUtils.getNameForCopy(conceptSetDTO.getName(), this::getNamesLike, getConceptSetRepository().findByName(conceptSetDTO.getName()))); - ConceptSetDTO createdDTO = createConceptSet(conceptSetDTO).getBody(); - saveConceptSetItems(createdDTO.getId(), fullDTO.getItems().toArray(new ConceptSetItem[0])); - - return ok(createdDTO); - } - - /** - * Get list of concept sets with their assigned tags - * - * @summary Get concept sets and tag information - * @param requestDTO The tagNameListRequest - * @return A list of concept sets with their assigned tags - */ - @PostMapping("/byTags") - public ResponseEntity> listByTags(@RequestBody TagNameListRequestDTO requestDTO) { - if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { - return ok(Collections.emptyList()); - } - List names = requestDTO.getNames().stream() - .map(name -> name.toLowerCase(Locale.ROOT)) - .collect(Collectors.toList()); - List entities = getConceptSetRepository().findByTags(names); - return ok(listByTags(entities, names, ConceptSetDTO.class)); - } - - private void checkVersion(int id, int version) { - checkVersion(id, version, true); - } - - private void checkVersion(int id, int version, boolean checkOwnerShip) { - Version conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version); - ExceptionUtils.throwNotFoundExceptionIfNull(conceptSetVersion, String.format("There is no concept set version with id = %d.", version)); - - ConceptSet entity = getConceptSetRepository().findById(id).orElse(null); - if (checkOwnerShip) { - checkOwnerOrAdminOrGranted(entity); - } - } - - private ConceptSetVersion saveVersion(int id) { - ConceptSet def = getConceptSetRepository().findById(id).orElse(null); - ConceptSetVersion version = conversionService.convert(def, ConceptSetVersion.class); - - UserEntity user = Objects.nonNull(def.getModifiedBy()) ? def.getModifiedBy() : def.getCreatedBy(); - Date versionDate = Objects.nonNull(def.getModifiedDate()) ? def.getModifiedDate() : def.getCreatedDate(); - version.setCreatedBy(user); - version.setCreatedDate(versionDate); - return versionService.create(VersionType.CONCEPT_SET, version); - } - - /** - * Update the concept set annotation for each concept in concept set ID in the - * WebAPI database. - *

- * The body has two parts: 1) the elements new concept which added to the - * concept set. 2) the elements concept which remove from concept set. - * - * @param conceptSetId The concept set ID - * @param request An object of 2 Array new annotation and remove annotation - * @return Boolean: true if the save is successful - * @summary Create new or delete concept set annotation items - */ - @PutMapping("/{id}/annotation") - public ResponseEntity saveConceptSetAnnotation( - @PathVariable("id") final int conceptSetId, - @RequestBody SaveConceptSetAnnotationsRequest request) { - removeAnnotations(conceptSetId, request); - if (request.getNewAnnotation() != null && !request.getNewAnnotation().isEmpty()) { - List annotationList = request.getNewAnnotation() - .stream() - .map(newAnnotationData -> { - ConceptSetAnnotation conceptSetAnnotation = new ConceptSetAnnotation(); - conceptSetAnnotation.setConceptSetId(conceptSetId); - try { - AnnotationDetailsDTO annotationDetailsDTO = new AnnotationDetailsDTO(); - annotationDetailsDTO.setId(newAnnotationData.getId()); - annotationDetailsDTO.setConceptId(newAnnotationData.getConceptId()); - annotationDetailsDTO.setSearchData(newAnnotationData.getSearchData()); - conceptSetAnnotation.setAnnotationDetails(mapper.writeValueAsString(annotationDetailsDTO)); - } catch (JsonProcessingException e) { - log.error("Could not serialize Concept Set AnnotationDetailsDTO", e); - throw new RuntimeException(e); - } - conceptSetAnnotation.setVocabularyVersion(newAnnotationData.getVocabularyVersion()); - conceptSetAnnotation.setConceptSetVersion(newAnnotationData.getConceptSetVersion()); - conceptSetAnnotation.setConceptId(newAnnotationData.getConceptId()); - conceptSetAnnotation.setCreatedBy(getCurrentUserEntity()); - conceptSetAnnotation.setCreatedDate(new Date()); - return conceptSetAnnotation; - }).collect(Collectors.toList()); - - this.getConceptSetAnnotationRepository().saveAll(annotationList); - } - - return ok(true); - } - - private void removeAnnotations(int id, SaveConceptSetAnnotationsRequest request) { - if (request.getRemoveAnnotation() != null && !request.getRemoveAnnotation().isEmpty()) { - for (AnnotationDTO annotationDTO : request.getRemoveAnnotation()) { - this.getConceptSetAnnotationRepository().deleteAnnotationByConceptSetIdAndConceptId(id, annotationDTO.getConceptId()); - } - } - } - - @PostMapping("/copy-annotations") - public ResponseEntity copyAnnotations(@RequestBody CopyAnnotationsRequest copyAnnotationsRequest) { - List sourceAnnotations = getConceptSetAnnotationRepository().findByConceptSetId(copyAnnotationsRequest.getSourceConceptSetId()); - List copiedAnnotations= sourceAnnotations.stream() - .map(sourceAnnotation -> copyAnnotation(sourceAnnotation, copyAnnotationsRequest.getSourceConceptSetId(), copyAnnotationsRequest.getTargetConceptSetId())) - .collect(Collectors.toList()); - getConceptSetAnnotationRepository().saveAll(copiedAnnotations); - return ok(); - } - - private ConceptSetAnnotation copyAnnotation(ConceptSetAnnotation sourceConceptSetAnnotation, int sourceConceptSetId, int targetConceptSetId) { - ConceptSetAnnotation targetConceptSetAnnotation = new ConceptSetAnnotation(); - targetConceptSetAnnotation.setConceptSetId(targetConceptSetId); - targetConceptSetAnnotation.setConceptSetVersion(sourceConceptSetAnnotation.getConceptSetVersion()); - targetConceptSetAnnotation.setAnnotationDetails(sourceConceptSetAnnotation.getAnnotationDetails()); - targetConceptSetAnnotation.setConceptId(sourceConceptSetAnnotation.getConceptId()); - targetConceptSetAnnotation.setVocabularyVersion(sourceConceptSetAnnotation.getVocabularyVersion()); - targetConceptSetAnnotation.setCreatedBy(sourceConceptSetAnnotation.getCreatedBy()); - targetConceptSetAnnotation.setCreatedDate(sourceConceptSetAnnotation.getCreatedDate()); - targetConceptSetAnnotation.setModifiedBy(sourceConceptSetAnnotation.getModifiedBy()); - targetConceptSetAnnotation.setModifiedDate(sourceConceptSetAnnotation.getModifiedDate()); - targetConceptSetAnnotation.setCopiedFromConceptSetIds(appendCopiedFromConceptSetId(sourceConceptSetAnnotation.getCopiedFromConceptSetIds(), sourceConceptSetId)); - return targetConceptSetAnnotation; - } - - private String appendCopiedFromConceptSetId(String copiedFromConceptSetIds, int sourceConceptSetId) { - if(copiedFromConceptSetIds == null || copiedFromConceptSetIds.isEmpty()){ - return Integer.toString(sourceConceptSetId); - } - return copiedFromConceptSetIds.concat(",").concat(Integer.toString(sourceConceptSetId)); - } - - @GetMapping("/{id}/annotation") - public ResponseEntity> getConceptSetAnnotation(@PathVariable("id") final int id) { - List annotationList = getConceptSetAnnotationRepository().findByConceptSetId(id); - List result = annotationList.stream() - .map(this::convertAnnotationEntityToDTO) - .collect(Collectors.toList()); - return ok(result); - } - - private AnnotationDTO convertAnnotationEntityToDTO(ConceptSetAnnotation conceptSetAnnotation) { - AnnotationDetailsDTO annotationDetails; - try { - annotationDetails = mapper.readValue(conceptSetAnnotation.getAnnotationDetails(), AnnotationDetailsDTO.class); - } catch (JsonProcessingException e) { - log.error("Could not deserialize Concept Set AnnotationDetailsDTO", e); - throw new RuntimeException(e); - } - - AnnotationDTO annotationDTO = new AnnotationDTO(); - - annotationDTO.setId(conceptSetAnnotation.getId()); - annotationDTO.setConceptId(conceptSetAnnotation.getConceptId()); - - String searchDataJSON = annotationDetails.getSearchData(); - String humanReadableData = searchDataTransformer.convertJsonToReadableFormat(searchDataJSON); - annotationDTO.setSearchData(humanReadableData); - - annotationDTO.setVocabularyVersion(conceptSetAnnotation.getVocabularyVersion()); - annotationDTO.setConceptSetVersion(conceptSetAnnotation.getConceptSetVersion()); - annotationDTO.setCopiedFromConceptSetIds(conceptSetAnnotation.getCopiedFromConceptSetIds()); - annotationDTO.setCreatedBy(conceptSetAnnotation.getCreatedBy() != null ? conceptSetAnnotation.getCreatedBy().getName() : null); - annotationDTO.setCreatedDate(conceptSetAnnotation.getCreatedDate() != null ? conceptSetAnnotation.getCreatedDate().toString() : null); - return annotationDTO; - } - - @DeleteMapping("/{conceptSetId}/annotation/{annotationId}") - public ResponseEntity deleteConceptSetAnnotation( - @PathVariable("conceptSetId") final int conceptSetId, - @PathVariable("annotationId") final int annotationId) { - ConceptSetAnnotation conceptSetAnnotation = getConceptSetAnnotationRepository().findById(annotationId); - if (conceptSetAnnotation != null) { - getConceptSetAnnotationRepository().deleteById(annotationId); - return ok(); - } else { - return notFound(); - } - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/DDLMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/DDLMvcController.java deleted file mode 100644 index cc3bce3d69..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/DDLMvcController.java +++ /dev/null @@ -1,215 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import org.apache.commons.lang3.ObjectUtils; -import org.ohdsi.circe.helper.ResourceHelper; -import org.ohdsi.webapi.common.DBMSType; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.service.SqlRenderService; -import org.ohdsi.webapi.sqlrender.SourceStatement; -import org.ohdsi.webapi.sqlrender.TranslatedStatement; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.*; - -/** - * Spring MVC version of DDLService - * - * Migration Status: Replaces /service/DDLService.java (Jersey) - * Endpoints: 3 GET endpoints with query parameters - * Complexity: Simple - GET operations generating DDL SQL - */ -@RestController -@RequestMapping("/ddl") -public class DDLMvcController extends AbstractMvcController { - - public static final String VOCAB_SCHEMA = "vocab_schema"; - public static final String RESULTS_SCHEMA = "results_schema"; - public static final String CEM_SCHEMA = "cem_results_schema"; - public static final String TEMP_SCHEMA = "oracle_temp_schema"; - - private static final Collection RESULT_DDL_FILE_PATHS = Arrays.asList( - "/ddl/results/cohort.sql", - "/ddl/results/cohort_censor_stats.sql", - "/ddl/results/cohort_inclusion.sql", - "/ddl/results/cohort_inclusion_result.sql", - "/ddl/results/cohort_inclusion_stats.sql", - "/ddl/results/cohort_summary_stats.sql", - "/ddl/results/cohort_cache.sql", - "/ddl/results/cohort_censor_stats_cache.sql", - "/ddl/results/cohort_inclusion_result_cache.sql", - "/ddl/results/cohort_inclusion_stats_cache.sql", - "/ddl/results/cohort_summary_stats_cache.sql", - "/ddl/results/feas_study_inclusion_stats.sql", - "/ddl/results/feas_study_index_stats.sql", - "/ddl/results/feas_study_result.sql", - "/ddl/results/heracles_analysis.sql", - "/ddl/results/heracles_heel_results.sql", - "/ddl/results/heracles_results.sql", - "/ddl/results/heracles_results_dist.sql", - "/ddl/results/heracles_periods.sql", - "/ddl/results/cohort_sample_element.sql", - "/ddl/results/ir_analysis_dist.sql", - "/ddl/results/ir_analysis_result.sql", - "/ddl/results/ir_analysis_strata_stats.sql", - "/ddl/results/ir_strata.sql", - "/ddl/results/cohort_characterizations.sql", - "/ddl/results/pathway_analysis_codes.sql", - "/ddl/results/pathway_analysis_events.sql", - "/ddl/results/pathway_analysis_paths.sql", - "/ddl/results/pathway_analysis_stats.sql" - ); - - private static final String INIT_HERACLES_PERIODS = "/ddl/results/init_heracles_periods.sql"; - - public static final Collection RESULT_INIT_FILE_PATHS = Arrays.asList( - "/ddl/results/init_heracles_analysis.sql", INIT_HERACLES_PERIODS - ); - - public static final Collection HIVE_RESULT_INIT_FILE_PATHS = Arrays.asList( - "/ddl/results/init_hive_heracles_analysis.sql", INIT_HERACLES_PERIODS - ); - - public static final Collection INIT_CONCEPT_HIERARCHY_FILE_PATHS = Arrays.asList( - "/ddl/results/concept_hierarchy.sql", - "/ddl/results/init_concept_hierarchy.sql" - ); - - private static final Collection RESULT_INDEX_FILE_PATHS = Arrays.asList( - "/ddl/results/create_index.sql", - "/ddl/results/pathway_analysis_events_indexes.sql" - ); - - private static final Collection CEMRESULT_DDL_FILE_PATHS = Arrays.asList( - "/ddl/cemresults/nc_results.sql" - ); - - public static final Collection CEMRESULT_INIT_FILE_PATHS = Collections.emptyList(); - private static final Collection CEMRESULT_INDEX_FILE_PATHS = Collections.emptyList(); - - private static final Collection ACHILLES_DDL_FILE_PATHS = Arrays.asList( - "/ddl/achilles/achilles_result_concept_count.sql" - ); - - private static final Collection DBMS_NO_INDEXES = Arrays.asList("redshift", "impala", "netezza", "spark"); - - /** - * Get DDL for results schema - * - * Jersey: GET /WebAPI/ddl/results - * Spring MVC: GET /WebAPI/v2/ddl/results - */ - @GetMapping(value = "/results", produces = MediaType.TEXT_PLAIN_VALUE) - public ResponseEntity generateResultSQL( - @RequestParam(value = "dialect", required = false) String dialect, - @RequestParam(value = "vocabSchema", defaultValue = "vocab") String vocabSchema, - @RequestParam(value = "schema", defaultValue = "results") String resultSchema, - @RequestParam(value = "initConceptHierarchy", defaultValue = "true") Boolean initConceptHierarchy, - @RequestParam(value = "tempSchema", required = false) String tempSchema) { - - Collection resultDDLFilePaths = new ArrayList<>(RESULT_DDL_FILE_PATHS); - - if (initConceptHierarchy) { - resultDDLFilePaths.addAll(INIT_CONCEPT_HIERARCHY_FILE_PATHS); - } - String oracleTempSchema = ObjectUtils.firstNonNull(tempSchema, resultSchema); - Map params = new HashMap<>() {{ - put(VOCAB_SCHEMA, vocabSchema); - put(RESULTS_SCHEMA, resultSchema); - put(TEMP_SCHEMA, oracleTempSchema); - }}; - - String sql = generateSQL(dialect, params, resultDDLFilePaths, getResultInitFilePaths(dialect), RESULT_INDEX_FILE_PATHS); - return ok(sql); - } - - /** - * Get DDL for Common Evidence Model results schema - * - * Jersey: GET /WebAPI/ddl/cemresults - * Spring MVC: GET /WebAPI/v2/ddl/cemresults - */ - @GetMapping(value = "/cemresults", produces = MediaType.TEXT_PLAIN_VALUE) - public ResponseEntity generateCemResultSQL( - @RequestParam(value = "dialect", required = false) String dialect, - @RequestParam(value = "schema", defaultValue = "cemresults") String schema) { - - Map params = new HashMap<>() {{ - put(CEM_SCHEMA, schema); - }}; - - String sql = generateSQL(dialect, params, CEMRESULT_DDL_FILE_PATHS, CEMRESULT_INIT_FILE_PATHS, CEMRESULT_INDEX_FILE_PATHS); - return ok(sql); - } - - /** - * Get DDL for Achilles results tables - * - * Jersey: GET /WebAPI/ddl/achilles - * Spring MVC: GET /WebAPI/v2/ddl/achilles - */ - @GetMapping(value = "/achilles", produces = MediaType.TEXT_PLAIN_VALUE) - public ResponseEntity generateAchillesSQL( - @RequestParam(value = "dialect", required = false) String dialect, - @RequestParam(value = "vocabSchema", defaultValue = "vocab") String vocabSchema, - @RequestParam(value = "schema", defaultValue = "results") String resultSchema) { - - final Collection achillesDDLFilePaths = new ArrayList<>(ACHILLES_DDL_FILE_PATHS); - - Map params = new HashMap<>() {{ - put(VOCAB_SCHEMA, vocabSchema); - put(RESULTS_SCHEMA, resultSchema); - }}; - - String sql = generateSQL(dialect, params, achillesDDLFilePaths, Collections.emptyList(), Collections.emptyList()); - return ok(sql); - } - - // Helper methods (same logic as original) - - private Collection getResultInitFilePaths(String dialect) { - if (Objects.equals(DBMSType.HIVE.getOhdsiDB(), dialect)) { - return HIVE_RESULT_INIT_FILE_PATHS; - } else { - return RESULT_INIT_FILE_PATHS; - } - } - - private String generateSQL(String dialect, Map params, Collection filePaths, - Collection initFilePaths, Collection indexFilePaths) { - StringBuilder sqlBuilder = new StringBuilder(); - for (String fileName : filePaths) { - sqlBuilder.append("\n").append(ResourceHelper.GetResourceAsString(fileName)); - } - - for (String fileName : initFilePaths) { - sqlBuilder.append("\n").append(ResourceHelper.GetResourceAsString(fileName)); - } - - if (dialect == null || DBMS_NO_INDEXES.stream().noneMatch(dbms -> dbms.equals(dialect.toLowerCase()))) { - for (String fileName : indexFilePaths) { - sqlBuilder.append("\n").append(ResourceHelper.GetResourceAsString(fileName)); - } - } - String result = sqlBuilder.toString(); - if (dialect != null) { - result = translateSqlFile(result, dialect, params); - } - return result.replaceAll(";", ";\n"); - } - - private String translateSqlFile(String sql, String dialect, Map params) { - SourceStatement statement = new SourceStatement(); - statement.setTargetDialect(dialect.toLowerCase()); - statement.setOracleTempSchema(params.get(TEMP_SCHEMA)); - statement.setSql(sql); - statement.getParameters().putAll(params); - - TranslatedStatement translatedStatement = SqlRenderService.translateSQL(statement); - return translatedStatement.getTargetSQL(); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/FeasibilityMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/FeasibilityMvcController.java deleted file mode 100644 index a47bca7686..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/FeasibilityMvcController.java +++ /dev/null @@ -1,256 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import org.ohdsi.webapi.feasibility.FeasibilityReport; -import org.ohdsi.webapi.job.JobExecutionResource; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.service.FeasibilityService; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -/** - * Spring MVC version of FeasibilityService - * - * Migration Status: Replaces /service/FeasibilityService.java (Jersey) - * Endpoints: 9 endpoints (4 GET, 2 PUT, 2 DELETE, 1 GET with generation) - * Complexity: High - complex business logic, all endpoints marked as deprecated - */ -@RestController -@RequestMapping("/feasibility") -public class FeasibilityMvcController extends AbstractMvcController { - - private final FeasibilityService feasibilityService; - - public FeasibilityMvcController(FeasibilityService feasibilityService) { - this.feasibilityService = feasibilityService; - } - - /** - * DO NOT USE - * - * Jersey: GET /WebAPI/feasibility/ - * Spring MVC: GET /WebAPI/v2/feasibility - * - * @summary DO NOT USE - * @deprecated - * @return List - */ - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - @Deprecated - public ResponseEntity> getFeasibilityStudyList() { - List studies = feasibilityService.getFeasibilityStudyList(); - return ok(studies); - } - - /** - * Creates the feasibility study - * - * Jersey: PUT /WebAPI/feasibility/ - * Spring MVC: PUT /WebAPI/v2/feasibility - * - * @summary DO NOT USE - * @deprecated - * @param study The feasibility study - * @return Feasibility study - */ - @PutMapping( - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - @Transactional - @Deprecated - public ResponseEntity createStudy( - @RequestBody FeasibilityService.FeasibilityStudyDTO study) { - FeasibilityService.FeasibilityStudyDTO createdStudy = feasibilityService.createStudy(study); - return ok(createdStudy); - } - - /** - * Get the feasibility study by ID - * - * Jersey: GET /WebAPI/feasibility/{id} - * Spring MVC: GET /WebAPI/v2/feasibility/{id} - * - * @summary DO NOT USE - * @deprecated - * @param id The study ID - * @return Feasibility study - */ - @GetMapping( - value = "/{id}", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - @Transactional(readOnly = true) - @Deprecated - public ResponseEntity getStudy(@PathVariable("id") int id) { - FeasibilityService.FeasibilityStudyDTO study = feasibilityService.getStudy(id); - return ok(study); - } - - /** - * Update the feasibility study - * - * Jersey: PUT /WebAPI/feasibility/{id} - * Spring MVC: PUT /WebAPI/v2/feasibility/{id} - * - * @summary DO NOT USE - * @deprecated - * @param id The study ID - * @param study The study information - * @return The updated study information - */ - @PutMapping( - value = "/{id}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - @Transactional - @Deprecated - public ResponseEntity saveStudy( - @PathVariable("id") int id, - @RequestBody FeasibilityService.FeasibilityStudyDTO study) { - FeasibilityService.FeasibilityStudyDTO savedStudy = feasibilityService.saveStudy(id, study); - return ok(savedStudy); - } - - /** - * Generate the feasibility study - * - * Jersey: GET /WebAPI/feasibility/{study_id}/generate/{sourceKey} - * Spring MVC: GET /WebAPI/v2/feasibility/{study_id}/generate/{sourceKey} - * - * @summary DO NOT USE - * @deprecated - * @param study_id The study ID - * @param sourceKey The source key - * @return JobExecutionResource - */ - @GetMapping( - value = "/{study_id}/generate/{sourceKey}", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - @Deprecated - public ResponseEntity performStudy( - @PathVariable("study_id") int study_id, - @PathVariable("sourceKey") String sourceKey) { - JobExecutionResource jobExecution = feasibilityService.performStudy(study_id, sourceKey); - return ok(jobExecution); - } - - /** - * Get simulation information - * - * Jersey: GET /WebAPI/feasibility/{id}/info - * Spring MVC: GET /WebAPI/v2/feasibility/{id}/info - * - * @summary DO NOT USE - * @deprecated - * @param id The study ID - * @return List - */ - @GetMapping( - value = "/{id}/info", - produces = MediaType.APPLICATION_JSON_VALUE - ) - @Transactional(readOnly = true) - @Deprecated - public ResponseEntity> getSimulationInfo(@PathVariable("id") int id) { - List info = feasibilityService.getSimulationInfo(id); - return ok(info); - } - - /** - * Get simulation report - * - * Jersey: GET /WebAPI/feasibility/{id}/report/{sourceKey} - * Spring MVC: GET /WebAPI/v2/feasibility/{id}/report/{sourceKey} - * - * @summary DO NOT USE - * @deprecated - * @param id The study ID - * @param sourceKey The source key - * @return FeasibilityReport - */ - @GetMapping( - value = "/{id}/report/{sourceKey}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - @Transactional - @Deprecated - public ResponseEntity getSimulationReport( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - FeasibilityReport report = feasibilityService.getSimulationReport(id, sourceKey); - return ok(report); - } - - /** - * Copies the specified cohort definition - * - * Jersey: GET /WebAPI/feasibility/{id}/copy - * Spring MVC: GET /WebAPI/v2/feasibility/{id}/copy - * - * @summary DO NOT USE - * @deprecated - * @param id - the Cohort Definition ID to copy - * @return the copied feasibility study as a FeasibilityStudyDTO - */ - @GetMapping( - value = "/{id}/copy", - produces = MediaType.APPLICATION_JSON_VALUE - ) - @jakarta.transaction.Transactional - @Deprecated - public ResponseEntity copy(@PathVariable("id") int id) { - FeasibilityService.FeasibilityStudyDTO copiedStudy = feasibilityService.copy(id); - return ok(copiedStudy); - } - - /** - * Deletes the specified feasibility study - * - * Jersey: DELETE /WebAPI/feasibility/{id} - * Spring MVC: DELETE /WebAPI/v2/feasibility/{id} - * - * @summary DO NOT USE - * @deprecated - * @param id The study ID - */ - @DeleteMapping( - value = "/{id}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - @Deprecated - public ResponseEntity delete(@PathVariable("id") int id) { - feasibilityService.delete(id); - return ok(); - } - - /** - * Deletes the specified study for the selected source - * - * Jersey: DELETE /WebAPI/feasibility/{id}/info/{sourceKey} - * Spring MVC: DELETE /WebAPI/v2/feasibility/{id}/info/{sourceKey} - * - * @summary DO NOT USE - * @deprecated - * @param id The study ID - * @param sourceKey The source key - */ - @DeleteMapping( - value = "/{id}/info/{sourceKey}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - @Transactional - @Deprecated - public ResponseEntity deleteInfo( - @PathVariable("id") int id, - @PathVariable("sourceKey") String sourceKey) { - feasibilityService.deleteInfo(id, sourceKey); - return ok(); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/I18nMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/I18nMvcController.java deleted file mode 100644 index 0599cdc79e..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/I18nMvcController.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import com.google.common.collect.ImmutableList; -import org.ohdsi.webapi.i18n.I18nService; -import org.ohdsi.webapi.i18n.LocaleDTO; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; -import java.util.Locale; -import java.util.Objects; - -/** - * Spring MVC version of I18nController - * - * Migration Status: Replaces /i18n/I18nController.java (Jersey) - * Endpoints: 2 GET endpoints - * Complexity: Simple - i18n resource handling - * - * Note: Original used @Controller with JAX-RS annotations (mixed framework) - * This is pure Spring MVC implementation - */ -@RestController -@RequestMapping("/i18n") -public class I18nMvcController extends AbstractMvcController { - - @Value("${i18n.enabled}") - private boolean i18nEnabled = true; - - @Value("${i18n.defaultLocale}") - private String defaultLocale = "en"; - - @Autowired - private I18nService i18nService; - - /** - * Get i18n resources for current locale - * - * Jersey: GET /WebAPI/i18n/ - * Spring MVC: GET /WebAPI/v2/i18n/ - * - * Note: Locale is resolved by LocaleInterceptor and stored in LocaleContextHolder - */ - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getResources() { - // Get locale from LocaleContextHolder (set by LocaleInterceptor) - Locale locale = LocaleContextHolder.getLocale(); - - if (!this.i18nEnabled || locale == null || !isLocaleSupported(locale.getLanguage())) { - locale = Locale.forLanguageTag(defaultLocale); - } - - String messages = i18nService.getLocaleResource(locale); - return ok(messages); - } - - /** - * Get list of available locales - * - * Jersey: GET /WebAPI/i18n/locales - * Spring MVC: GET /WebAPI/v2/i18n/locales - */ - @GetMapping(value = "/locales", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getAvailableLocales() { - if (this.i18nEnabled) { - return ok(i18nService.getAvailableLocales()); - } - - // if i18n is disabled, then return only default locale - return ok(ImmutableList.of(new LocaleDTO(this.defaultLocale, null, true))); - } - - private boolean isLocaleSupported(String code) { - return i18nService.getAvailableLocales().stream().anyMatch(l -> Objects.equals(code, l.getCode())); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/InfoMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/InfoMvcController.java deleted file mode 100644 index 5fce562671..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/InfoMvcController.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import org.ohdsi.webapi.info.Info; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.ohdsi.webapi.info.InfoService; - -/** - * Spring MVC version of InfoService - * - * Migration Status: Replaces /info/InfoService.java (Jersey) - * Endpoints: 1 GET endpoint - * Complexity: Simple - read-only, no parameters - * - * Note: This controller delegates to the existing Jersey InfoService to get the Info object. - * This allows us to avoid duplicate dependency injection issues with BuildProperties/BuildInfo - * while still providing the same endpoint via Spring MVC. - */ -@RestController -@RequestMapping("/info") -public class InfoMvcController extends AbstractMvcController { - - @Autowired - private InfoService infoService; - - /** - * Get info about the WebAPI instance - * - * Jersey: GET /WebAPI/info/ - * Spring MVC: GET /WebAPI/v2/info/ - */ - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getInfo() { - // Delegate to the existing InfoService which is already configured - // with BuildProperties and BuildInfo - Info info = infoService.getInfo(); - return ok(info); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/JobMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/JobMvcController.java deleted file mode 100644 index 1f5a24cc59..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/JobMvcController.java +++ /dev/null @@ -1,156 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import org.ohdsi.webapi.job.JobExecutionResource; -import org.ohdsi.webapi.job.JobInstanceResource; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.springframework.batch.core.launch.NoSuchJobException; -import org.springframework.data.domain.Page; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -/** - * Spring MVC version of JobService - * - * Migration Status: Replaces /service/JobService.java (Jersey) - * Endpoints: 6 GET endpoints - * Complexity: Simple - mostly delegation to service layer - */ -@RestController -@RequestMapping("/job") -public class JobMvcController extends AbstractMvcController { - - private final org.ohdsi.webapi.service.JobService jobService; - - public JobMvcController(org.ohdsi.webapi.service.JobService jobService) { - this.jobService = jobService; - } - - /** - * Get the job information by job ID - * - * Jersey: GET /WebAPI/job/{jobId} - * Spring MVC: GET /WebAPI/v2/job/{jobId} - * - * @summary Get job by ID - * @param jobId The job ID - * @return The job information - */ - @GetMapping(value = "/{jobId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity findJob(@PathVariable("jobId") Long jobId) { - JobInstanceResource job = jobService.findJob(jobId); - if (job == null) { - return notFound(); - } - return ok(job); - } - - /** - * Get the job execution information by job type and name - * - * Jersey: GET /WebAPI/job/type/{jobType}/name/{jobName} - * Spring MVC: GET /WebAPI/v2/job/type/{jobType}/name/{jobName} - * - * @summary Get job by name and type - * @param jobName The job name - * @param jobType The job type - * @return JobExecutionResource - */ - @GetMapping(value = "/type/{jobType}/name/{jobName}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity findJobByName( - @PathVariable("jobName") String jobName, - @PathVariable("jobType") String jobType) { - JobExecutionResource jobExecution = jobService.findJobByName(jobName, jobType); - if (jobExecution == null) { - return notFound(); - } - return ok(jobExecution); - } - - /** - * Get the job execution information by execution ID and job ID - * - * Jersey: GET /WebAPI/job/{jobId}/execution/{executionId} - * Spring MVC: GET /WebAPI/v2/job/{jobId}/execution/{executionId} - * - * @summary Get job by job ID and execution ID - * @param jobId The job ID - * @param executionId The execution ID - * @return JobExecutionResource - */ - @GetMapping(value = "/{jobId}/execution/{executionId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity findJobExecutionByJobId( - @PathVariable("jobId") Long jobId, - @PathVariable("executionId") Long executionId) { - JobExecutionResource jobExecution = jobService.findJobExecution(jobId, executionId); - if (jobExecution == null) { - return notFound(); - } - return ok(jobExecution); - } - - /** - * Find job execution by execution ID - * - * Jersey: GET /WebAPI/job/execution/{executionId} - * Spring MVC: GET /WebAPI/v2/job/execution/{executionId} - * - * @summary Get job by execution ID - * @param executionId The job execution ID - * @return JobExecutionResource - */ - @GetMapping(value = "/execution/{executionId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity findJobExecution(@PathVariable("executionId") Long executionId) { - JobExecutionResource jobExecution = jobService.findJobExecution(executionId); - if (jobExecution == null) { - return notFound(); - } - return ok(jobExecution); - } - - /** - * Get job names (unique names). Note: this path (GET /job) should really - * return pages of job instances. This could be implemented should the need - * arise. - * - * Jersey: GET /WebAPI/job - * Spring MVC: GET /WebAPI/v2/job - * - * @summary Get list of jobs - * @return A list of jobs - */ - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> findJobNames() { - List jobNames = jobService.findJobNames(); - return ok(jobNames); - } - - /** - * Return a paged collection of job executions. Filter for a given job. - * Returned in pages. - * - * Jersey: GET /WebAPI/job/execution?jobName={jobName}&pageIndex={pageIndex}&pageSize={pageSize}&comprehensivePage={comprehensivePage} - * Spring MVC: GET /WebAPI/v2/job/execution?jobName={jobName}&pageIndex={pageIndex}&pageSize={pageSize}&comprehensivePage={comprehensivePage} - * - * @summary Get job executions with filters - * @param jobName name of the job - * @param pageIndex start index for the job execution list - * @param pageSize page size for the list - * @param comprehensivePage boolean if true returns a comprehensive resultset - * as a page (i.e. pageRequest(0,resultset.size())) - * @return collection of JobExecutionInfo - * @throws NoSuchJobException - */ - @GetMapping(value = "/execution", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> list( - @RequestParam(value = "jobName", required = false) String jobName, - @RequestParam(value = "pageIndex", defaultValue = "0") Integer pageIndex, - @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize, - @RequestParam(value = "comprehensivePage", required = false, defaultValue = "false") boolean comprehensivePage) - throws NoSuchJobException { - Page page = jobService.list(jobName, pageIndex, pageSize, comprehensivePage); - return ok(page); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/PermissionMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/PermissionMvcController.java deleted file mode 100644 index 58586404ec..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/PermissionMvcController.java +++ /dev/null @@ -1,211 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.security.AccessType; -import org.ohdsi.webapi.security.PermissionService; -import org.ohdsi.webapi.security.dto.AccessRequestDTO; -import org.ohdsi.webapi.security.dto.RoleDTO; -import org.ohdsi.webapi.security.model.EntityType; -import org.ohdsi.webapi.service.UserService; -import org.ohdsi.webapi.shiro.Entities.PermissionEntity; -import org.ohdsi.webapi.shiro.PermissionManager; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.core.convert.ConversionService; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -/** - * Spring MVC version of PermissionController - * - * Migration Status: Replaces /security/PermissionController.java (Jersey) - * Endpoints: 6 endpoints (3 GET, 1 POST, 1 DELETE, 1 GET with query) - * Complexity: Medium - permission management and authorization logic - */ -@RestController -@RequestMapping("/permission") -@Transactional -public class PermissionMvcController extends AbstractMvcController { - - private final PermissionService permissionService; - private final PermissionManager permissionManager; - private final ConversionService conversionService; - - public PermissionMvcController( - PermissionService permissionService, - PermissionManager permissionManager, - @Qualifier("conversionService") ConversionService conversionService) { - this.permissionService = permissionService; - this.permissionManager = permissionManager; - this.conversionService = conversionService; - } - - /** - * Get the list of permissions for a user - * - * Jersey: GET /WebAPI/permission - * Spring MVC: GET /WebAPI/v2/permission - * - * @return A list of permissions - */ - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getPermissions() { - Iterable permissionEntities = permissionManager.getPermissions(); - List permissions = StreamSupport.stream(permissionEntities.spliterator(), false) - .map(UserService.Permission::new) - .collect(Collectors.toList()); - return ok(permissions); - } - - /** - * Get the roles matching the roleSearch value - * - * Jersey: GET /WebAPI/permission/access/suggest?roleSearch={roleSearch} - * Spring MVC: GET /WebAPI/v2/permission/access/suggest?roleSearch={roleSearch} - * - * @summary Role search - * @param roleSearch The role to search - * @return The list of roles - */ - @GetMapping( - value = "/access/suggest", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity> listAccessesForEntity(@RequestParam("roleSearch") String roleSearch) { - List roles = permissionService.suggestRoles(roleSearch); - List roleDTOs = roles.stream() - .map(re -> conversionService.convert(re, RoleDTO.class)) - .collect(Collectors.toList()); - return ok(roleDTOs); - } - - /** - * Get roles that have a permission type (READ/WRITE) to entity - * - * Jersey: GET /WebAPI/permission/access/{entityType}/{entityId}/{permType} - * Spring MVC: GET /WebAPI/v2/permission/access/{entityType}/{entityId}/{permType} - * - * @summary Get roles that have a specific permission (READ/WRITE) for the entity - * @param entityType The entity type - * @param entityId The entity ID - * @param permType The permission type - * @return The list of permissions for the permission type - * @throws Exception - */ - @GetMapping( - value = "/access/{entityType}/{entityId}/{permType}", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity> listAccessesForEntityByPermType( - @PathVariable("entityType") EntityType entityType, - @PathVariable("entityId") Integer entityId, - @PathVariable("permType") AccessType permType) throws Exception { - permissionService.checkCommonEntityOwnership(entityType, entityId); - var permissionTemplates = permissionService.getTemplatesForType(entityType, permType).keySet(); - - List permissions = permissionTemplates.stream() - .map(pt -> permissionService.getPermission(pt, entityId)) - .collect(Collectors.toList()); - - List roles = permissionService.finaAllRolesHavingPermissions(permissions); - - List roleDTOs = roles.stream() - .map(re -> conversionService.convert(re, RoleDTO.class)) - .collect(Collectors.toList()); - return ok(roleDTOs); - } - - /** - * Get roles that have a permission type (READ/WRITE) to entity - * - * Jersey: GET /WebAPI/permission/access/{entityType}/{entityId} - * Spring MVC: GET /WebAPI/v2/permission/access/{entityType}/{entityId} - * - * @summary Get roles that have a specific permission (READ/WRITE) for the entity - * @param entityType The entity type - * @param entityId The entity ID - * @return The list of permissions for the permission type - * @throws Exception - */ - @GetMapping( - value = "/access/{entityType}/{entityId}", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity> listAccessesForEntity( - @PathVariable("entityType") EntityType entityType, - @PathVariable("entityId") Integer entityId) throws Exception { - return listAccessesForEntityByPermType(entityType, entityId, AccessType.WRITE); - } - - /** - * Grant group of permissions (READ / WRITE / ...) for the specified entity to the given role. - * Only owner of the entity can do that. - * - * Jersey: POST /WebAPI/permission/access/{entityType}/{entityId}/role/{roleId} - * Spring MVC: POST /WebAPI/v2/permission/access/{entityType}/{entityId}/role/{roleId} - * - * @summary Grant permissions - * @param entityType The entity type - * @param entityId The entity ID - * @param roleId The role ID - * @param accessRequestDTO The access request object - * @throws Exception - */ - @PostMapping( - value = "/access/{entityType}/{entityId}/role/{roleId}", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity grantEntityPermissionsForRole( - @PathVariable("entityType") EntityType entityType, - @PathVariable("entityId") Integer entityId, - @PathVariable("roleId") Long roleId, - @RequestBody AccessRequestDTO accessRequestDTO) throws Exception { - permissionService.checkCommonEntityOwnership(entityType, entityId); - - var permissionTemplates = permissionService.getTemplatesForType(entityType, accessRequestDTO.getAccessType()); - - org.ohdsi.webapi.shiro.Entities.RoleEntity role = permissionManager.getRole(roleId); - permissionManager.addPermissionsFromTemplate(role, permissionTemplates, entityId.toString()); - - return ok(); - } - - /** - * Remove group of permissions for the specified entity to the given role. - * - * Jersey: DELETE /WebAPI/permission/access/{entityType}/{entityId}/role/{roleId} - * Spring MVC: DELETE /WebAPI/v2/permission/access/{entityType}/{entityId}/role/{roleId} - * - * @summary Remove permissions - * @param entityType The entity type - * @param entityId The entity ID - * @param roleId The role ID - * @param accessRequestDTO The access request object - * @throws Exception - */ - @DeleteMapping( - value = "/access/{entityType}/{entityId}/role/{roleId}", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity revokeEntityPermissionsFromRole( - @PathVariable("entityType") EntityType entityType, - @PathVariable("entityId") Integer entityId, - @PathVariable("roleId") Long roleId, - @RequestBody AccessRequestDTO accessRequestDTO) throws Exception { - permissionService.checkCommonEntityOwnership(entityType, entityId); - var permissionTemplates = permissionService.getTemplatesForType(entityType, accessRequestDTO.getAccessType()); - permissionService.removePermissionsFromRole(permissionTemplates, entityId, roleId); - - return ok(); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/SourceMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/SourceMvcController.java deleted file mode 100644 index 712248e2de..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/SourceMvcController.java +++ /dev/null @@ -1,398 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.ohdsi.webapi.arachne.logging.event.AddDataSourceEvent; -import org.ohdsi.webapi.arachne.logging.event.ChangeDataSourceEvent; -import org.ohdsi.webapi.arachne.logging.event.DeleteDataSourceEvent; -import org.ohdsi.webapi.common.DBMSType; -import org.ohdsi.webapi.exception.SourceDuplicateKeyException; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.service.VocabularyService; -import org.ohdsi.webapi.shiro.Entities.UserEntity; -import org.ohdsi.webapi.shiro.Entities.UserRepository; -import org.ohdsi.webapi.source.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.server.ResponseStatusException; - -import jakarta.persistence.PersistenceException; -import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; - -/** - * Spring MVC version of SourceController - * - * Migration Status: Replaces /source/SourceController.java (Jersey) - * Endpoints: 10 endpoints (3 GET, 2 POST, 1 PUT, 1 DELETE) - * Special: Multipart file upload endpoints for keyfile handling - */ -@RestController -@RequestMapping("/source") -@Transactional -public class SourceMvcController extends AbstractMvcController { - - public static final String SECURE_MODE_ERROR = "This feature requires the administrator to enable security for the application"; - - @Autowired - private ApplicationEventPublisher publisher; - - @Autowired - private VocabularyService vocabularyService; - - @Autowired - private SourceService sourceService; - - @Autowired - private SourceRepository sourceRepository; - - @Autowired - private SourceDaimonRepository sourceDaimonRepository; - - @Autowired - private GenericConversionService conversionService; - - @Autowired - private UserRepository userRepository; - - @Value("#{!'${security.provider}'.equals('DisabledSecurity')}") - private boolean securityEnabled; - - protected UserEntity getCurrentUserEntity() { - return userRepository.findByLogin(security.getSubject()); - } - - /** - * Gets the list of all Sources in WebAPI database. Sources with a non-null - * deleted_date are not returned (ie: these are soft deleted) - * - * @summary Get Sources - * @return A list of all CDM sources with the ID, name, SQL dialect, and key - * for each source. The {sourceKey} is used in other WebAPI endpoints to - * identify CDMs. - */ - @GetMapping(value = "/sources", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getSources() { - return ok(sourceService.getSources().stream().map(SourceInfo::new).collect(Collectors.toList())); - } - - /** - * Refresh cached CDM database metadata - * - * @summary Refresh Sources - * @return A list of all CDM sources with the ID, name, SQL dialect, and key - * for each source (same as the 'sources' endpoint) after refreshing the cached sourced data. - */ - @GetMapping(value = "/refresh", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> refreshSources() { - sourceService.invalidateCache(); - vocabularyService.clearVocabularyInfoCache(); - sourceService.ensureSourceEncrypted(); - return getSources(); - } - - /** - * Get the priority vocabulary source. - * - * WebAPI designates one CDM vocabulary as the priority vocabulary to be used for vocabulary searches in Atlas. - * - * @summary Get Priority Vocabulary Source - * @return The CDM metadata for the priority vocabulary. - */ - @GetMapping(value = "/priorityVocabulary", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getPriorityVocabularySourceInfo() { - return ok(sourceService.getPriorityVocabularySourceInfo()); - } - - /** - * Get source by key - * @summary Get Source By Key - * @param sourceKey - * @return Metadata for a single Source that matches the sourceKey. - */ - @GetMapping(value = "/{key}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getSource(@PathVariable("key") final String sourceKey) { - return ok(sourceRepository.findBySourceKey(sourceKey).getSourceInfo()); - } - - /** - * Get Source Details - * - * Source Details contains connection-specific information like JDBC url and authentication information. - * - * @summary Get Source Details - * @param sourceId - * @return - */ - @GetMapping(value = "/details/{sourceId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getSourceDetails(@PathVariable("sourceId") Integer sourceId) { - if (!securityEnabled) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, SECURE_MODE_ERROR); - } - Source source = sourceRepository.findBySourceId(sourceId); - return ok(new SourceDetails(source)); - } - - /** - * Create a Source - * - * @summary Create Source - * @param keyfile the keyfile - * @param source contains the source information (name, key, etc) - * @return a new SourceInfo for the created source - * @throws Exception - */ - @PostMapping(value = "", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true) - public ResponseEntity createSource( - @RequestPart(value = "keyfile", required = false) MultipartFile keyfile, - @RequestPart("source") SourceRequest source) throws Exception { - if (!securityEnabled) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, SECURE_MODE_ERROR); - } - Source sourceByKey = sourceRepository.findBySourceKey(source.getKey()); - if (Objects.nonNull(sourceByKey)) { - throw new SourceDuplicateKeyException("The source key has been already used."); - } - Source sourceEntity = conversionService.convert(source, Source.class); - if (sourceEntity.getDaimons() != null) { - // First source should get priority = 1 - Iterable sources = sourceRepository.findAll(); - sourceEntity.getDaimons() - .stream() - .filter(sd -> sd.getPriority() <= 0) - .filter(sd -> { - boolean accept = true; - // Check if source daimon of given type with priority > 0 already exists in other sources - for (Source innerSource : sources) { - accept = !innerSource.getDaimons() - .stream() - .anyMatch(innerDaimon -> innerDaimon.getPriority() > 0 - && innerDaimon.getDaimonType().equals(sd.getDaimonType())); - if (!accept) { - break; - } - } - return accept; - }) - .forEach(sd -> sd.setPriority(1)); - } - Source original = new Source(); - original.setSourceDialect(sourceEntity.getSourceDialect()); - setKeyfileData(sourceEntity, original, keyfile); - sourceEntity.setCreatedBy(getCurrentUserEntity()); - sourceEntity.setCreatedDate(new Date()); - try { - Source saved = sourceRepository.saveAndFlush(sourceEntity); - sourceService.invalidateCache(); - SourceInfo sourceInfo = new SourceInfo(saved); - publisher.publishEvent(new AddDataSourceEvent(this, sourceEntity.getSourceId(), sourceEntity.getSourceName())); - return ok(sourceInfo); - } catch (PersistenceException ex) { - throw new SourceDuplicateKeyException("You cannot use this Source Key, please use different one"); - } - } - - /** - * Updates a Source with the provided details from multiple files - * - * @summary Update Source - * @param sourceId - * @param keyfile the keyfile - * @param source contains the source information (name, key, etc) - * @return the updated SourceInfo for the source - * @throws Exception - */ - @PutMapping(value = "/{sourceId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional - @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true) - public ResponseEntity updateSource( - @PathVariable("sourceId") Integer sourceId, - @RequestPart(value = "keyfile", required = false) MultipartFile keyfile, - @RequestPart("source") SourceRequest source) throws IOException { - if (!securityEnabled) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, SECURE_MODE_ERROR); - } - Source updated = conversionService.convert(source, Source.class); - Source existingSource = sourceRepository.findBySourceId(sourceId); - if (existingSource != null) { - updated.setSourceId(sourceId); - updated.setSourceKey(existingSource.getSourceKey()); - if (StringUtils.isBlank(updated.getUsername()) || - Objects.equals(updated.getUsername().trim(), Source.MASQUERADED_USERNAME)) { - updated.setUsername(existingSource.getUsername()); - } - if (StringUtils.isBlank(updated.getPassword()) || - Objects.equals(updated.getPassword().trim(), Source.MASQUERADED_PASSWORD)) { - updated.setPassword(existingSource.getPassword()); - } - setKeyfileData(updated, existingSource, keyfile); - transformIfRequired(updated); - if (source.isCheckConnection() == null) { - updated.setCheckConnection(existingSource.isCheckConnection()); - } - updated.setModifiedBy(getCurrentUserEntity()); - updated.setModifiedDate(new Date()); - - reuseDeletedDaimons(updated, existingSource); - - List removed = existingSource.getDaimons().stream() - .filter(d -> !updated.getDaimons().contains(d)) - .collect(Collectors.toList()); - // Delete MUST be called after fetching user or source data to prevent autoflush (see DefaultPersistEventListener.onPersist) - sourceDaimonRepository.deleteAll(removed); - Source result = sourceRepository.save(updated); - publisher.publishEvent(new ChangeDataSourceEvent(this, updated.getSourceId(), updated.getSourceName())); - sourceService.invalidateCache(); - return ok(new SourceInfo(result)); - } else { - return notFound(); - } - } - - private void reuseDeletedDaimons(Source updated, Source source) { - List daimons = updated.getDaimons().stream().filter(d -> source.getDaimons().contains(d)) - .collect(Collectors.toList()); - List newDaimons = updated.getDaimons().stream().filter(d -> !source.getDaimons().contains(d)) - .collect(Collectors.toList()); - - List allDaimons = sourceDaimonRepository.findBySource(source); - - for (SourceDaimon newSourceDaimon : newDaimons) { - Optional reusedDaimonOpt = allDaimons.stream() - .filter(d -> d.equals(newSourceDaimon)) - .findFirst(); - if (reusedDaimonOpt.isPresent()) { - SourceDaimon reusedDaimon = reusedDaimonOpt.get(); - reusedDaimon.setPriority(newSourceDaimon.getPriority()); - reusedDaimon.setTableQualifier(newSourceDaimon.getTableQualifier()); - daimons.add(reusedDaimon); - } else { - daimons.add(newSourceDaimon); - } - } - updated.setDaimons(daimons); - } - - private void transformIfRequired(Source source) { - if (DBMSType.BIGQUERY.getOhdsiDB().equals(source.getSourceDialect()) && ArrayUtils.isNotEmpty(source.getKeyfile())) { - String connStr = source.getSourceConnection().replaceAll("OAuthPvtKeyPath=.+?(;|\\z)", ""); - source.setSourceConnection(connStr); - } - } - - private void setKeyfileData(Source updated, Source source, MultipartFile file) throws IOException { - if (source.supportsKeyfile()) { - if (updated.getKeyfileName() != null) { - if (!Objects.equals(updated.getKeyfileName(), source.getKeyfileName())) { - byte[] fileBytes = file != null ? file.getBytes() : new byte[0]; - updated.setKeyfile(fileBytes); - } else { - updated.setKeyfile(source.getKeyfile()); - } - return; - } - } - updated.setKeyfile(null); - updated.setKeyfileName(null); - } - - /** - * Delete a source. - * - * @summary Delete Source - * @param sourceId - * @return - * @throws Exception - */ - @DeleteMapping("/{sourceId}") - @Transactional - @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true) - public ResponseEntity delete(@PathVariable("sourceId") Integer sourceId) throws Exception { - if (!securityEnabled) { - return unauthorized(); - } - Source source = sourceRepository.findBySourceId(sourceId); - if (source != null) { - sourceRepository.delete(source); - publisher.publishEvent(new DeleteDataSourceEvent(this, sourceId, source.getSourceName())); - sourceService.invalidateCache(); - return ok(); - } else { - return notFound(); - } - } - - /** - * Check source connection. - * - * This method attempts to connect to the source by calling 'select 1' on the source connection. - * @summary Check connection - * @param sourceKey - * @return - */ - @GetMapping(value = "/connection/{key}", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional(noRollbackFor = CannotGetJdbcConnectionException.class) - public ResponseEntity checkConnection(@PathVariable("key") final String sourceKey) { - final Source source = sourceService.findBySourceKey(sourceKey); - sourceService.checkConnection(source); - return ok(source.getSourceInfo()); - } - - /** - * Get the first daimon (ad associated source) that has priority. In the event - * of a tie, the first source searched wins. - * - * @summary Get Priority Daimons - * @return - */ - @GetMapping(value = "/daimon/priority", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getPriorityDaimons() { - return ok(sourceService.getPriorityDaimons() - .entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> new SourceInfo(e.getValue()) - ))); - } - - /** - * Set priority of daimon - * - * Set the priority of the specified daimon of the specified source, and set the other daimons to 0. - * @summary Set Priority - * @param sourceKey - * @param daimonTypeName - * @return - */ - @PostMapping(value = "/{sourceKey}/daimons/{daimonType}/set-priority", produces = MediaType.APPLICATION_JSON_VALUE) - @CacheEvict(cacheNames = SourceService.CachingSetup.SOURCE_LIST_CACHE, allEntries = true) - public ResponseEntity updateSourcePriority( - @PathVariable("sourceKey") final String sourceKey, - @PathVariable("daimonType") final String daimonTypeName) { - if (!securityEnabled) { - return unauthorized(); - } - SourceDaimon.DaimonType daimonType = SourceDaimon.DaimonType.valueOf(daimonTypeName); - List daimonList = sourceDaimonRepository.findByDaimonType(daimonType); - daimonList.forEach(daimon -> { - Integer newPriority = daimon.getSource().getSourceKey().equals(sourceKey) ? 1 : 0; - daimon.setPriority(newPriority); - sourceDaimonRepository.save(daimon); - }); - sourceService.invalidateCache(); - return ok(); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/SqlRenderMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/SqlRenderMvcController.java deleted file mode 100644 index 5ccb77b7f9..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/SqlRenderMvcController.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.service.SqlRenderService; -import org.ohdsi.webapi.sqlrender.SourceStatement; -import org.ohdsi.webapi.sqlrender.TranslatedStatement; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static org.ohdsi.webapi.Constants.SqlSchemaPlaceholders.TEMP_DATABASE_SCHEMA_PLACEHOLDER; - -/** - * Spring MVC version of SqlRenderService - * - * Migration Status: Replaces /service/SqlRenderService.java (Jersey) - * Endpoints: 1 POST endpoint - * Complexity: Simple - POST with JSON request body - */ -@RestController -@RequestMapping("/sqlrender") -public class SqlRenderMvcController extends AbstractMvcController { - - /** - * Translate an OHDSI SQL to a supported target SQL dialect - * - * Jersey: POST /WebAPI/sqlrender/translate - * Spring MVC: POST /WebAPI/v2/sqlrender/translate - * - * @param sourceStatement JSON with parameters, source SQL, and target dialect - * @return rendered and translated SQL - */ - @PostMapping( - value = "/translate", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity translateSQLFromSourceStatement(@RequestBody SourceStatement sourceStatement) { - if (sourceStatement == null) { - return ok(new TranslatedStatement()); - } - sourceStatement.setOracleTempSchema(TEMP_DATABASE_SCHEMA_PLACEHOLDER); - TranslatedStatement result = SqlRenderService.translateSQL(sourceStatement); - return ok(result); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/UserMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/UserMvcController.java deleted file mode 100644 index 55960f7f4c..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/UserMvcController.java +++ /dev/null @@ -1,368 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import org.ohdsi.webapi.arachne.logging.event.*; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.shiro.Entities.PermissionEntity; -import org.ohdsi.webapi.shiro.Entities.RoleEntity; -import org.ohdsi.webapi.shiro.Entities.UserEntity; -import org.ohdsi.webapi.shiro.PermissionManager; -import org.ohdsi.webapi.user.Role; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -/** - * Spring MVC version of UserService - * - * Migration Status: Replaces /service/UserService.java (Jersey) - * Endpoints: 15 endpoints (3 GET user, 5 role management, 3 role permissions, 2 role users, 2 user roles/permissions) - * Complexity: Medium - CRUD operations for users, roles, and permissions with event publishing - * - * @author gennadiy.anisimov - */ -@RestController -@RequestMapping("") -public class UserMvcController extends AbstractMvcController { - - @Autowired - private PermissionManager authorizer; - - @Autowired - private ApplicationEventPublisher eventPublisher; - - @Value("${security.ad.default.import.group}#{T(java.util.Collections).emptyList()}") - private List defaultRoles; - - private Map roleCreatorPermissionsTemplate = new LinkedHashMap<>(); - - public UserMvcController() { - this.roleCreatorPermissionsTemplate.put("role:%s:permissions:*:put", "Add permissions to role with ID = %s"); - this.roleCreatorPermissionsTemplate.put("role:%s:permissions:*:delete", "Remove permissions from role with ID = %s"); - this.roleCreatorPermissionsTemplate.put("role:%s:put", "Update role with ID = %s"); - this.roleCreatorPermissionsTemplate.put("role:%s:delete", "Delete role with ID = %s"); - } - - public static class User implements Comparable { - public Long id; - public String login; - public String name; - public List permissions; - public Map> permissionIdx; - - public User() {} - - public User(UserEntity userEntity) { - this.id = userEntity.getId(); - this.login = userEntity.getLogin(); - this.name = userEntity.getName(); - } - - @Override - public int compareTo(User o) { - Comparator c = Comparator.naturalOrder(); - if (this.id == null && o.id == null) - return c.compare(this.login, o.login); - else - return c.compare(this.id, o.id); - } - } - - public static class Permission implements Comparable { - public Long id; - public String permission; - public String description; - - public Permission() {} - - public Permission(PermissionEntity permissionEntity) { - this.id = permissionEntity.getId(); - this.permission = permissionEntity.getValue(); - this.description = permissionEntity.getDescription(); - } - - @Override - public int compareTo(Permission o) { - Comparator c = Comparator.naturalOrder(); - if (this.id == null && o.id == null) - return c.compare(this.permission, o.permission); - else - return c.compare(this.id, o.id); - } - } - - /** - * Get all users - * - * Jersey: GET /WebAPI/user - * Spring MVC: GET /WebAPI/v2/user - */ - @GetMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getUsers() { - Iterable userEntities = this.authorizer.getUsers(); - ArrayList users = convertUsers(userEntities); - return ok(users); - } - - /** - * Get current user - * - * Jersey: GET /WebAPI/user/me - * Spring MVC: GET /WebAPI/v2/user/me - */ - @GetMapping(value = "/user/me", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getCurrentUserDetails() throws Exception { - UserEntity currentUser = this.authorizer.getCurrentUser(); - Iterable permissions = this.authorizer.getUserPermissions(currentUser.getId()); - - User user = new User(); - user.id = currentUser.getId(); - user.login = currentUser.getLogin(); - user.name = currentUser.getName(); - user.permissions = convertPermissions(permissions); - user.permissionIdx = authorizer.queryUserPermissions(currentUser.getLogin()).permissions; - - return ok(user); - } - - /** - * Get user permissions - * - * Jersey: GET /WebAPI/user/{userId}/permissions - * Spring MVC: GET /WebAPI/v2/user/{userId}/permissions - */ - @GetMapping(value = "/user/{userId}/permissions", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getUsersPermissions(@PathVariable("userId") Long userId) throws Exception { - Set permissionEntities = this.authorizer.getUserPermissions(userId); - List permissions = convertPermissions(permissionEntities); - Collections.sort(permissions); - return ok(permissions); - } - - /** - * Get user roles - * - * Jersey: GET /WebAPI/user/{userId}/roles - * Spring MVC: GET /WebAPI/v2/user/{userId}/roles - */ - @GetMapping(value = "/user/{userId}/roles", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getUserRoles(@PathVariable("userId") Long userId) throws Exception { - Set roleEntities = this.authorizer.getUserRoles(userId); - ArrayList roles = convertRoles(roleEntities); - Collections.sort(roles); - return ok(roles); - } - - /** - * Create a new role - * - * Jersey: POST /WebAPI/role - * Spring MVC: POST /WebAPI/v2/role - */ - @PostMapping(value = "/role", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createRole(@RequestBody Role role) throws Exception { - RoleEntity roleEntity = this.authorizer.addRole(role.role, true); - RoleEntity personalRole = this.authorizer.getCurrentUserPersonalRole(); - this.authorizer.addPermissionsFromTemplate( - personalRole, - this.roleCreatorPermissionsTemplate, - String.valueOf(roleEntity.getId())); - Role newRole = new Role(roleEntity); - eventPublisher.publishEvent(new AddRoleEvent(this, newRole.id, newRole.role)); - return ok(newRole); - } - - /** - * Update a role - * - * Jersey: PUT /WebAPI/role/{roleId} - * Spring MVC: PUT /WebAPI/v2/role/{roleId} - */ - @PutMapping(value = "/role/{roleId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity updateRole(@PathVariable("roleId") Long id, @RequestBody Role role) throws Exception { - RoleEntity roleEntity = this.authorizer.getRole(id); - if (roleEntity == null) { - throw new Exception("Role doesn't exist"); - } - roleEntity.setName(role.role); - roleEntity = this.authorizer.updateRole(roleEntity); - eventPublisher.publishEvent(new ChangeRoleEvent(this, id, role.role)); - return ok(new Role(roleEntity)); - } - - /** - * Get all roles - * - * Jersey: GET /WebAPI/role - * Spring MVC: GET /WebAPI/v2/role - */ - @GetMapping(value = "/role", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getRoles( - @RequestParam(value = "include_personal", defaultValue = "false") boolean includePersonalRoles) { - Iterable roleEntities = this.authorizer.getRoles(includePersonalRoles); - ArrayList roles = convertRoles(roleEntities); - return ok(roles); - } - - /** - * Get a role by ID - * - * Jersey: GET /WebAPI/role/{roleId} - * Spring MVC: GET /WebAPI/v2/role/{roleId} - */ - @GetMapping(value = "/role/{roleId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getRole(@PathVariable("roleId") Long id) { - RoleEntity roleEntity = this.authorizer.getRole(id); - Role role = new Role(roleEntity); - return ok(role); - } - - /** - * Delete a role - * - * Jersey: DELETE /WebAPI/role/{roleId} - * Spring MVC: DELETE /WebAPI/v2/role/{roleId} - */ - @DeleteMapping(value = "/role/{roleId}") - public ResponseEntity removeRole(@PathVariable("roleId") Long roleId) { - this.authorizer.removeRole(roleId); - this.authorizer.removePermissionsFromTemplate(this.roleCreatorPermissionsTemplate, String.valueOf(roleId)); - return ok(); - } - - /** - * Get role permissions - * - * Jersey: GET /WebAPI/role/{roleId}/permissions - * Spring MVC: GET /WebAPI/v2/role/{roleId}/permissions - */ - @GetMapping(value = "/role/{roleId}/permissions", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getRolePermissions(@PathVariable("roleId") Long roleId) throws Exception { - Set permissionEntities = this.authorizer.getRolePermissions(roleId); - List permissions = convertPermissions(permissionEntities); - Collections.sort(permissions); - return ok(permissions); - } - - /** - * Add permissions to a role - * - * Jersey: PUT /WebAPI/role/{roleId}/permissions/{permissionIdList} - * Spring MVC: PUT /WebAPI/v2/role/{roleId}/permissions/{permissionIdList} - */ - @PutMapping(value = "/role/{roleId}/permissions/{permissionIdList}") - public ResponseEntity addPermissionToRole( - @PathVariable("roleId") Long roleId, - @PathVariable("permissionIdList") String permissionIdList) throws Exception { - String[] ids = permissionIdList.split("\\+"); - for (String permissionIdString : ids) { - Long permissionId = Long.parseLong(permissionIdString); - this.authorizer.addPermission(roleId, permissionId); - eventPublisher.publishEvent(new AddPermissionEvent(this, permissionId, roleId)); - } - return ok(); - } - - /** - * Remove permissions from a role - * - * Jersey: DELETE /WebAPI/role/{roleId}/permissions/{permissionIdList} - * Spring MVC: DELETE /WebAPI/v2/role/{roleId}/permissions/{permissionIdList} - */ - @DeleteMapping(value = "/role/{roleId}/permissions/{permissionIdList}") - public ResponseEntity removePermissionFromRole( - @PathVariable("roleId") Long roleId, - @PathVariable("permissionIdList") String permissionIdList) { - String[] ids = permissionIdList.split("\\+"); - for (String permissionIdString : ids) { - Long permissionId = Long.parseLong(permissionIdString); - this.authorizer.removePermission(permissionId, roleId); - eventPublisher.publishEvent(new DeletePermissionEvent(this, permissionId, roleId)); - } - return ok(); - } - - /** - * Get users assigned to a role - * - * Jersey: GET /WebAPI/role/{roleId}/users - * Spring MVC: GET /WebAPI/v2/role/{roleId}/users - */ - @GetMapping(value = "/role/{roleId}/users", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getRoleUsers(@PathVariable("roleId") Long roleId) throws Exception { - Set userEntities = this.authorizer.getRoleUsers(roleId); - ArrayList users = this.convertUsers(userEntities); - Collections.sort(users); - return ok(users); - } - - /** - * Assign users to a role - * - * Jersey: PUT /WebAPI/role/{roleId}/users/{userIdList} - * Spring MVC: PUT /WebAPI/v2/role/{roleId}/users/{userIdList} - */ - @PutMapping(value = "/role/{roleId}/users/{userIdList}") - public ResponseEntity addUserToRole( - @PathVariable("roleId") Long roleId, - @PathVariable("userIdList") String userIdList) throws Exception { - String[] ids = userIdList.split("\\+"); - for (String userIdString : ids) { - Long userId = Long.parseLong(userIdString); - this.authorizer.addUser(userId, roleId); - eventPublisher.publishEvent(new AssignRoleEvent(this, roleId, userId)); - } - return ok(); - } - - /** - * Remove users from a role - * - * Jersey: DELETE /WebAPI/role/{roleId}/users/{userIdList} - * Spring MVC: DELETE /WebAPI/v2/role/{roleId}/users/{userIdList} - */ - @DeleteMapping(value = "/role/{roleId}/users/{userIdList}") - public ResponseEntity removeUserFromRole( - @PathVariable("roleId") Long roleId, - @PathVariable("userIdList") String userIdList) { - String[] ids = userIdList.split("\\+"); - for (String userIdString : ids) { - Long userId = Long.parseLong(userIdString); - this.authorizer.removeUser(userId, roleId); - eventPublisher.publishEvent(new UnassignRoleEvent(this, roleId, userId)); - } - return ok(); - } - - // Helper methods - - private List convertPermissions(final Iterable permissionEntities) { - return StreamSupport.stream(permissionEntities.spliterator(), false) - .map(UserMvcController.Permission::new) - .collect(Collectors.toList()); - } - - private ArrayList convertRoles(final Iterable roleEntities) { - ArrayList roles = new ArrayList<>(); - for (RoleEntity roleEntity : roleEntities) { - Role role = new Role(roleEntity, defaultRoles.contains(roleEntity.getName())); - roles.add(role); - } - return roles; - } - - private ArrayList convertUsers(final Iterable userEntities) { - ArrayList users = new ArrayList<>(); - for (UserEntity userEntity : userEntities) { - User user = new User(userEntity); - users.add(user); - } - return users; - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/VocabularyMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/VocabularyMvcController.java deleted file mode 100644 index 12af606dbe..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/VocabularyMvcController.java +++ /dev/null @@ -1,715 +0,0 @@ -package org.ohdsi.webapi.mvc.controller; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.ohdsi.circe.cohortdefinition.ConceptSet; -import org.ohdsi.circe.vocabulary.ConceptSetExpression; -import org.ohdsi.vocabulary.Concept; -import org.ohdsi.webapi.conceptset.ConceptSetComparison; -import org.ohdsi.webapi.conceptset.ConceptSetExport; -import org.ohdsi.webapi.conceptset.ConceptSetOptimizationResult; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.service.VocabularyService; -import org.ohdsi.webapi.service.cscompare.CompareArbitraryDto; -import org.ohdsi.webapi.source.SourceInfo; -import org.ohdsi.webapi.vocabulary.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Collection; -import java.util.List; -import java.util.Map; - -/** - * Spring MVC version of VocabularyService - * - * Migration Status: Replaces /service/VocabularyService.java (Jersey) - * Endpoints: 40+ endpoints for vocabulary operations - * Complexity: High - complex search, lookups, concept set operations - * - * Note: This controller delegates to the existing Jersey VocabularyService to reuse - * all business logic while providing Spring MVC endpoints. - */ -@RestController -@RequestMapping("/vocabulary") -public class VocabularyMvcController extends AbstractMvcController { - - @Autowired - private VocabularyService vocabularyService; - - @Autowired - private ObjectMapper objectMapper; - - /** - * DTO for ancestor/descendant concept ID lists - */ - public static class ConceptIdListsDto { - public List ancestors; - public List descendants; - } - - /** - * Calculates the full set of ancestor and descendant concepts for a list of - * ancestor and descendant concepts specified. - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/lookup/identifiers/ancestors - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/lookup/identifiers/ancestors - */ - @PostMapping(value = "/{sourceKey}/lookup/identifiers/ancestors", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity>> calculateAscendants( - @PathVariable("sourceKey") String sourceKey, - @RequestBody ConceptIdListsDto dto) throws Exception { - // Convert our DTO to the private Ids class using reflection and JSON serialization - String json = objectMapper.writeValueAsString(dto); - Class idsClass = Class.forName("org.ohdsi.webapi.service.VocabularyService$Ids"); - Object ids = objectMapper.readValue(json, idsClass); - - // Use reflection to call the method since Ids is private - java.lang.reflect.Method method = VocabularyService.class.getMethod( - "calculateAscendants", String.class, idsClass); - @SuppressWarnings("unchecked") - Map> result = (Map>) method.invoke( - vocabularyService, sourceKey, ids); - return ok(result); - } - - /** - * Get concepts from concept identifiers (IDs) from a specific source - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/lookup/identifiers - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/lookup/identifiers - */ - @PostMapping(value = "/{sourceKey}/lookup/identifiers", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> executeIdentifierLookup( - @PathVariable("sourceKey") String sourceKey, - @RequestBody long[] identifiers) { - Collection concepts = vocabularyService.executeIdentifierLookup(sourceKey, identifiers); - return ok(concepts); - } - - /** - * Get concepts from concept identifiers (IDs) from the default vocabulary source - * - * Jersey: POST /WebAPI/vocabulary/lookup/identifiers - * Spring MVC: POST /WebAPI/v2/vocabulary/lookup/identifiers - */ - @PostMapping(value = "/lookup/identifiers", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> executeIdentifierLookupDefault(@RequestBody long[] identifiers) { - Collection concepts = vocabularyService.executeIdentifierLookup(identifiers); - return ok(concepts); - } - - /** - * Get concepts from source codes from a specific source - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/lookup/sourcecodes - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/lookup/sourcecodes - */ - @PostMapping(value = "/{sourceKey}/lookup/sourcecodes", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> executeSourcecodeLookup( - @PathVariable("sourceKey") String sourceKey, - @RequestBody String[] sourcecodes) { - Collection concepts = vocabularyService.executeSourcecodeLookup(sourceKey, sourcecodes); - return ok(concepts); - } - - /** - * Get concepts from source codes from the default vocabulary source - * - * Jersey: POST /WebAPI/vocabulary/lookup/sourcecodes - * Spring MVC: POST /WebAPI/v2/vocabulary/lookup/sourcecodes - */ - @PostMapping(value = "/lookup/sourcecodes", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> executeSourcecodeLookupDefault(@RequestBody String[] sourcecodes) { - Collection concepts = vocabularyService.executeSourcecodeLookup(sourcecodes); - return ok(concepts); - } - - /** - * Get concepts mapped to the selected concept identifiers from a specific source - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/lookup/mapped - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/lookup/mapped - */ - @PostMapping(value = "/{sourceKey}/lookup/mapped", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> executeMappedLookup( - @PathVariable("sourceKey") String sourceKey, - @RequestBody long[] identifiers) { - Collection concepts = vocabularyService.executeMappedLookup(sourceKey, identifiers); - return ok(concepts); - } - - /** - * Get concepts mapped to the selected concept identifiers from the default source - * - * Jersey: POST /WebAPI/vocabulary/lookup/mapped - * Spring MVC: POST /WebAPI/v2/vocabulary/lookup/mapped - */ - @PostMapping(value = "/lookup/mapped", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> executeMappedLookupDefault(@RequestBody long[] identifiers) { - Collection concepts = vocabularyService.executeMappedLookup(identifiers); - return ok(concepts); - } - - /** - * Search for a concept on the selected source (POST with ConceptSearch) - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/search - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/search - */ - @PostMapping(value = "/{sourceKey}/search", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> executeSearchPost( - @PathVariable("sourceKey") String sourceKey, - @RequestBody ConceptSearch search) { - Collection concepts = vocabularyService.executeSearch(sourceKey, search); - return ok(concepts); - } - - /** - * Search for a concept on the default vocabulary source (POST with ConceptSearch) - * - * Jersey: POST /WebAPI/vocabulary/search - * Spring MVC: POST /WebAPI/v2/vocabulary/search - */ - @PostMapping(value = "/search", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> executeSearchPostDefault(@RequestBody ConceptSearch search) { - Collection concepts = vocabularyService.executeSearch(search); - return ok(concepts); - } - - /** - * Search for a concept based on a query using the selected vocabulary source (with path variable) - * - * Jersey: GET /WebAPI/vocabulary/{sourceKey}/search/{query} - * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/search/{query} - */ - @GetMapping(value = "/{sourceKey}/search/{query}", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> executeSearchPathQuery( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("query") String query) { - Collection concepts = vocabularyService.executeSearch(sourceKey, query); - return ok(concepts); - } - - /** - * Search for a concept using query parameters - * - * Jersey: GET /WebAPI/vocabulary/{sourceKey}/search?query=...&rows=... - * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/search?query=...&rows=... - */ - @GetMapping(value = "/{sourceKey}/search", - produces = MediaType.APPLICATION_JSON_VALUE, - params = "query") - public ResponseEntity> executeSearchQueryParam( - @PathVariable("sourceKey") String sourceKey, - @RequestParam("query") String query, - @RequestParam(value = "rows", defaultValue = VocabularyService.DEFAULT_SEARCH_ROWS) String rows) { - Collection concepts = vocabularyService.executeSearch(sourceKey, query, rows); - return ok(concepts); - } - - /** - * Search for a concept based on a query using the default vocabulary source (path variable) - * - * Jersey: GET /WebAPI/vocabulary/search/{query} - * Spring MVC: GET /WebAPI/v2/vocabulary/search/{query} - */ - @GetMapping(value = "/search/{query}", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> executeSearchDefaultPathQuery(@PathVariable("query") String query) { - Collection concepts = vocabularyService.executeSearch(query); - return ok(concepts); - } - - /** - * Get a concept based on the concept identifier from the specified source - * - * Jersey: GET /WebAPI/vocabulary/{sourceKey}/concept/{id} - * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/concept/{id} - */ - @GetMapping(value = "/{sourceKey}/concept/{id}", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getConcept( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") long id) { - Concept concept = vocabularyService.getConcept(sourceKey, id); - return ok(concept); - } - - /** - * Get a concept based on the concept identifier from the default vocabulary source - * - * Jersey: GET /WebAPI/vocabulary/concept/{id} - * Spring MVC: GET /WebAPI/v2/vocabulary/concept/{id} - */ - @GetMapping(value = "/concept/{id}", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getConceptDefault(@PathVariable("id") long id) { - Concept concept = vocabularyService.getConcept(id); - return ok(concept); - } - - /** - * Get related concepts for the selected concept identifier from a source - * - * Jersey: GET /WebAPI/vocabulary/{sourceKey}/concept/{id}/related - * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/concept/{id}/related - */ - @GetMapping(value = "/{sourceKey}/concept/{id}/related", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getRelatedConcepts( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") Long id) { - Collection concepts = vocabularyService.getRelatedConcepts(sourceKey, id); - return ok(concepts); - } - - /** - * Get related standard mapped concepts - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/related-standard - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/related-standard - */ - @PostMapping(value = "/{sourceKey}/related-standard", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getRelatedStandardMappedConcepts( - @PathVariable("sourceKey") String sourceKey, - @RequestBody List allConceptIds) { - Collection concepts = vocabularyService.getRelatedStandardMappedConcepts(sourceKey, allConceptIds); - return ok(concepts); - } - - /** - * Get ancestor and descendant concepts for the selected concept identifier - * - * Jersey: GET /WebAPI/vocabulary/{sourceKey}/concept/{id}/ancestorAndDescendant - * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/concept/{id}/ancestorAndDescendant - */ - @GetMapping(value = "/{sourceKey}/concept/{id}/ancestorAndDescendant", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getConceptAncestorAndDescendant( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") Long id) { - Collection concepts = vocabularyService.getConceptAncestorAndDescendant(sourceKey, id); - return ok(concepts); - } - - /** - * Get related concepts for the selected concept identifier (default vocabulary) - * - * Jersey: GET /WebAPI/vocabulary/concept/{id}/related - * Spring MVC: GET /WebAPI/v2/vocabulary/concept/{id}/related - */ - @GetMapping(value = "/concept/{id}/related", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getRelatedConceptsDefault(@PathVariable("id") Long id) { - Collection concepts = vocabularyService.getRelatedConcepts(id); - return ok(concepts); - } - - /** - * Get common ancestor concepts - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/commonAncestors - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/commonAncestors - */ - @PostMapping(value = "/{sourceKey}/commonAncestors", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCommonAncestors( - @PathVariable("sourceKey") String sourceKey, - @RequestBody Object[] identifiers) { - Collection concepts = vocabularyService.getCommonAncestors(sourceKey, identifiers); - return ok(concepts); - } - - /** - * Get common ancestor concepts (default vocabulary) - * - * Jersey: POST /WebAPI/vocabulary/commonAncestors - * Spring MVC: POST /WebAPI/v2/vocabulary/commonAncestors - */ - @PostMapping(value = "/commonAncestors", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCommonAncestorsDefault(@RequestBody Object[] identifiers) { - Collection concepts = vocabularyService.getCommonAncestors(identifiers); - return ok(concepts); - } - - /** - * Resolve a concept set expression into a collection of concept identifiers - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/resolveConceptSetExpression - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/resolveConceptSetExpression - */ - @PostMapping(value = "/{sourceKey}/resolveConceptSetExpression", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> resolveConceptSetExpression( - @PathVariable("sourceKey") String sourceKey, - @RequestBody ConceptSetExpression conceptSetExpression) { - Collection identifiers = vocabularyService.resolveConceptSetExpression(sourceKey, conceptSetExpression); - return ok(identifiers); - } - - /** - * Resolve a concept set expression (default vocabulary) - * - * Jersey: POST /WebAPI/vocabulary/resolveConceptSetExpression - * Spring MVC: POST /WebAPI/v2/vocabulary/resolveConceptSetExpression - */ - @PostMapping(value = "/resolveConceptSetExpression", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> resolveConceptSetExpressionDefault( - @RequestBody ConceptSetExpression conceptSetExpression) { - Collection identifiers = vocabularyService.resolveConceptSetExpression(conceptSetExpression); - return ok(identifiers); - } - - /** - * Get included concept counts for concept set expression - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/included-concepts/count - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/included-concepts/count - */ - @PostMapping(value = "/{sourceKey}/included-concepts/count", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity countIncludedConceptSets( - @PathVariable("sourceKey") String sourceKey, - @RequestBody ConceptSetExpression conceptSetExpression) { - Integer count = vocabularyService.countIncludedConceptSets(sourceKey, conceptSetExpression); - return ok(count); - } - - /** - * Get included concept counts for concept set expression (default vocabulary) - * - * Jersey: POST /WebAPI/vocabulary/included-concepts/count - * Spring MVC: POST /WebAPI/v2/vocabulary/included-concepts/count - */ - @PostMapping(value = "/included-concepts/count", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity countIncludedConceptSetsDefault( - @RequestBody ConceptSetExpression conceptSetExpression) { - Integer count = vocabularyService.countIncludedConcepSets(conceptSetExpression); - return ok(count); - } - - /** - * Get SQL to resolve concept set expression - * - * Jersey: POST /WebAPI/vocabulary/conceptSetExpressionSQL - * Spring MVC: POST /WebAPI/v2/vocabulary/conceptSetExpressionSQL - */ - @PostMapping(value = "/conceptSetExpressionSQL", - produces = MediaType.TEXT_PLAIN_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getConceptSetExpressionSQL(@RequestBody ConceptSetExpression conceptSetExpression) { - String sql = vocabularyService.getConceptSetExpressionSQL(conceptSetExpression); - return ok(sql); - } - - /** - * Get descendant concepts for the selected concept identifier - * - * Jersey: GET /WebAPI/vocabulary/{sourceKey}/concept/{id}/descendants - * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/concept/{id}/descendants - */ - @GetMapping(value = "/{sourceKey}/concept/{id}/descendants", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDescendantConcepts( - @PathVariable("sourceKey") String sourceKey, - @PathVariable("id") Long id) { - Collection concepts = vocabularyService.getDescendantConcepts(sourceKey, id); - return ok(concepts); - } - - /** - * Get descendant concepts (default vocabulary) - * - * Jersey: GET /WebAPI/vocabulary/concept/{id}/descendants - * Spring MVC: GET /WebAPI/v2/vocabulary/concept/{id}/descendants - */ - @GetMapping(value = "/concept/{id}/descendants", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDescendantConceptsDefault(@PathVariable("id") Long id) { - Collection concepts = vocabularyService.getDescendantConcepts(id); - return ok(concepts); - } - - /** - * Get domains - * - * Jersey: GET /WebAPI/vocabulary/{sourceKey}/domains - * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/domains - */ - @GetMapping(value = "/{sourceKey}/domains", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDomains(@PathVariable("sourceKey") String sourceKey) { - Collection domains = vocabularyService.getDomains(sourceKey); - return ok(domains); - } - - /** - * Get domains (default vocabulary) - * - * Jersey: GET /WebAPI/vocabulary/domains - * Spring MVC: GET /WebAPI/v2/vocabulary/domains - */ - @GetMapping(value = "/domains", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDomainsDefault() { - Collection domains = vocabularyService.getDomains(); - return ok(domains); - } - - /** - * Get vocabularies - * - * Jersey: GET /WebAPI/vocabulary/{sourceKey}/vocabularies - * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/vocabularies - */ - @GetMapping(value = "/{sourceKey}/vocabularies", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getVocabularies(@PathVariable("sourceKey") String sourceKey) { - Collection vocabularies = vocabularyService.getVocabularies(sourceKey); - return ok(vocabularies); - } - - /** - * Get vocabularies (default vocabulary) - * - * Jersey: GET /WebAPI/vocabulary/vocabularies - * Spring MVC: GET /WebAPI/v2/vocabulary/vocabularies - */ - @GetMapping(value = "/vocabularies", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getVocabulariesDefault() { - Collection vocabularies = vocabularyService.getVocabularies(); - return ok(vocabularies); - } - - /** - * Get vocabulary version info - * - * Jersey: GET /WebAPI/vocabulary/{sourceKey}/info - * Spring MVC: GET /WebAPI/v2/vocabulary/{sourceKey}/info - */ - @GetMapping(value = "/{sourceKey}/info", - produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getInfo(@PathVariable("sourceKey") String sourceKey) { - VocabularyInfo info = vocabularyService.getInfo(sourceKey); - return ok(info); - } - - /** - * Get descendant concepts by source - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/descendantofancestor - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/descendantofancestor - */ - @PostMapping(value = "/{sourceKey}/descendantofancestor", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDescendantOfAncestorConcepts( - @PathVariable("sourceKey") String sourceKey, - @RequestBody DescendentOfAncestorSearch search) { - Collection concepts = vocabularyService.getDescendantOfAncestorConcepts(sourceKey, search); - return ok(concepts); - } - - /** - * Get descendant concepts (default vocabulary) - * - * Jersey: POST /WebAPI/vocabulary/descendantofancestor - * Spring MVC: POST /WebAPI/v2/vocabulary/descendantofancestor - */ - @PostMapping(value = "/descendantofancestor", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDescendantOfAncestorConceptsDefault( - @RequestBody DescendentOfAncestorSearch search) { - Collection concepts = vocabularyService.getDescendantOfAncestorConcepts(search); - return ok(concepts); - } - - /** - * Get related concepts - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/relatedconcepts - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/relatedconcepts - */ - @PostMapping(value = "/{sourceKey}/relatedconcepts", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getRelatedConceptsFiltered( - @PathVariable("sourceKey") String sourceKey, - @RequestBody RelatedConceptSearch search) { - Collection concepts = vocabularyService.getRelatedConcepts(sourceKey, search); - return ok(concepts); - } - - /** - * Get related concepts (default vocabulary) - * - * Jersey: POST /WebAPI/vocabulary/relatedconcepts - * Spring MVC: POST /WebAPI/v2/vocabulary/relatedconcepts - */ - @PostMapping(value = "/relatedconcepts", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getRelatedConceptsFilteredDefault( - @RequestBody RelatedConceptSearch search) { - Collection concepts = vocabularyService.getRelatedConcepts(search); - return ok(concepts); - } - - /** - * Get descendant concepts for selected concepts - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/conceptlist/descendants - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/conceptlist/descendants - */ - @PostMapping(value = "/{sourceKey}/conceptlist/descendants", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDescendantConceptsByList( - @PathVariable("sourceKey") String sourceKey, - @RequestBody String[] conceptList) { - Collection concepts = vocabularyService.getDescendantConceptsByList(sourceKey, conceptList); - return ok(concepts); - } - - /** - * Get descendant concepts for selected concepts (default vocabulary) - * - * Jersey: POST /WebAPI/vocabulary/conceptlist/descendants - * Spring MVC: POST /WebAPI/v2/vocabulary/conceptlist/descendants - */ - @PostMapping(value = "/conceptlist/descendants", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getDescendantConceptsByListDefault( - @RequestBody String[] conceptList) { - Collection concepts = vocabularyService.getDescendantConceptsByList(conceptList); - return ok(concepts); - } - - /** - * Get recommended concepts for selected concepts - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/lookup/recommended - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/lookup/recommended - */ - @PostMapping(value = "/{sourceKey}/lookup/recommended", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getRecommendedConceptsByList( - @PathVariable("sourceKey") String sourceKey, - @RequestBody long[] conceptList) { - Collection concepts = vocabularyService.getRecommendedConceptsByList(sourceKey, conceptList); - return ok(concepts); - } - - /** - * Compare concept sets - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/compare - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/compare - */ - @PostMapping(value = "/{sourceKey}/compare", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> compareConceptSets( - @PathVariable("sourceKey") String sourceKey, - @RequestBody ConceptSetExpression[] conceptSetExpressionList) throws Exception { - Collection comparison = vocabularyService.compareConceptSets(sourceKey, conceptSetExpressionList); - return ok(comparison); - } - - /** - * Compare concept sets (arbitrary/CSV) - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/compare-arbitrary - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/compare-arbitrary - */ - @PostMapping(value = "/{sourceKey}/compare-arbitrary", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> compareConceptSetsCsv( - @PathVariable("sourceKey") String sourceKey, - @RequestBody CompareArbitraryDto dto) throws Exception { - Collection comparison = vocabularyService.compareConceptSetsCsv(sourceKey, dto); - return ok(comparison); - } - - /** - * Compare concept sets (default vocabulary) - * - * Jersey: POST /WebAPI/vocabulary/compare - * Spring MVC: POST /WebAPI/v2/vocabulary/compare - */ - @PostMapping(value = "/compare", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> compareConceptSetsDefault( - @RequestBody ConceptSetExpression[] conceptSetExpressionList) throws Exception { - Collection comparison = vocabularyService.compareConceptSets(conceptSetExpressionList); - return ok(comparison); - } - - /** - * Optimize concept set (default vocabulary) - * - * Jersey: POST /WebAPI/vocabulary/optimize - * Spring MVC: POST /WebAPI/v2/vocabulary/optimize - */ - @PostMapping(value = "/optimize", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity optimizeConceptSetDefault( - @RequestBody ConceptSetExpression conceptSetExpression) throws Exception { - ConceptSetOptimizationResult result = vocabularyService.optimizeConceptSet(conceptSetExpression); - return ok(result); - } - - /** - * Optimize concept set - * - * Jersey: POST /WebAPI/vocabulary/{sourceKey}/optimize - * Spring MVC: POST /WebAPI/v2/vocabulary/{sourceKey}/optimize - */ - @PostMapping(value = "/{sourceKey}/optimize", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity optimizeConceptSet( - @PathVariable("sourceKey") String sourceKey, - @RequestBody ConceptSetExpression conceptSetExpression) throws Exception { - ConceptSetOptimizationResult result = vocabularyService.optimizeConceptSet(sourceKey, conceptSetExpression); - return ok(result); - } -} diff --git a/src/main/java/org/ohdsi/webapi/reusable/ReusableMvcController.java b/src/main/java/org/ohdsi/webapi/reusable/ReusableMvcController.java deleted file mode 100644 index f66d9ac12e..0000000000 --- a/src/main/java/org/ohdsi/webapi/reusable/ReusableMvcController.java +++ /dev/null @@ -1,358 +0,0 @@ -package org.ohdsi.webapi.reusable; - -import org.ohdsi.webapi.Pagination; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.reusable.dto.ReusableDTO; -import org.ohdsi.webapi.reusable.dto.ReusableVersionFullDTO; -import org.ohdsi.webapi.tag.dto.TagNameListRequestDTO; -import org.ohdsi.webapi.versioning.dto.VersionDTO; -import org.ohdsi.webapi.versioning.dto.VersionUpdateDTO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Collections; -import java.util.List; - -/** - * Spring MVC version of ReusableController - * - * Migration Status: Replaces /reusable/ReusableController.java (Jersey) - * Endpoints: 14 endpoints (POST, GET, PUT, DELETE) - * Complexity: Medium - CRUD operations with versioning and tagging - */ -@RestController -@RequestMapping("/reusable") -public class ReusableMvcController extends AbstractMvcController { - - private final ReusableService reusableService; - - @Autowired - public ReusableMvcController(ReusableService reusableService) { - this.reusableService = reusableService; - } - - /** - * Create a new reusable - * - * Jersey: POST /WebAPI/reusable/ - * Spring MVC: POST /WebAPI/v2/reusable/ - * - * @param dto the reusable DTO - * @return created reusable - */ - @PostMapping( - value = "/", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity create(@RequestBody ReusableDTO dto) { - return ok(reusableService.create(dto)); - } - - /** - * Get paginated list of reusables - * - * Jersey: GET /WebAPI/reusable/ - * Spring MVC: GET /WebAPI/v2/reusable/ - * - * @param pageable pagination parameters - * @return page of reusables - */ - @GetMapping( - value = "/", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity> page(@Pagination Pageable pageable) { - return ok(reusableService.page(pageable)); - } - - /** - * Update an existing reusable - * - * Jersey: PUT /WebAPI/reusable/{id} - * Spring MVC: PUT /WebAPI/v2/reusable/{id} - * - * @param id the reusable ID - * @param dto the reusable DTO - * @return updated reusable - */ - @PutMapping( - value = "/{id}", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity update(@PathVariable("id") Integer id, @RequestBody ReusableDTO dto) { - return ok(reusableService.update(id, dto)); - } - - /** - * Copy a reusable - * - * Jersey: POST /WebAPI/reusable/{id} - * Spring MVC: POST /WebAPI/v2/reusable/{id} - * - * @param id the reusable ID - * @return copied reusable - */ - @PostMapping( - value = "/{id}", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity copy(@PathVariable("id") int id) { - return ok(reusableService.copy(id)); - } - - /** - * Get a reusable by ID - * - * Jersey: GET /WebAPI/reusable/{id} - * Spring MVC: GET /WebAPI/v2/reusable/{id} - * - * @param id the reusable ID - * @return the reusable - */ - @GetMapping( - value = "/{id}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity get(@PathVariable("id") Integer id) { - return ok(reusableService.getDTOById(id)); - } - - /** - * Check if a reusable name exists - * - * Jersey: GET /WebAPI/reusable/{id}/exists - * Spring MVC: GET /WebAPI/v2/reusable/{id}/exists - * - * @param id the reusable ID (default 0) - * @param name the name to check - * @return true if exists - */ - @GetMapping( - value = "/{id}/exists", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity exists( - @PathVariable("id") int id, - @RequestParam(value = "name", required = false) String name) { - return ok(reusableService.exists(id, name)); - } - - /** - * Delete a reusable - * - * Jersey: DELETE /WebAPI/reusable/{id} - * Spring MVC: DELETE /WebAPI/v2/reusable/{id} - * - * @param id the reusable ID - */ - @DeleteMapping( - value = "/{id}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity delete(@PathVariable("id") Integer id) { - reusableService.delete(id); - return ok(); - } - - /** - * Assign tag to Reusable - * - * Jersey: POST /WebAPI/reusable/{id}/tag/ - * Spring MVC: POST /WebAPI/v2/reusable/{id}/tag/ - * - * @param id the reusable ID - * @param tagId the tag ID - */ - @PostMapping( - value = "/{id}/tag/", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity assignTag(@PathVariable("id") int id, @RequestBody int tagId) { - reusableService.assignTag(id, tagId); - return ok(); - } - - /** - * Unassign tag from Reusable - * - * Jersey: DELETE /WebAPI/reusable/{id}/tag/{tagId} - * Spring MVC: DELETE /WebAPI/v2/reusable/{id}/tag/{tagId} - * - * @param id the reusable ID - * @param tagId the tag ID - */ - @DeleteMapping( - value = "/{id}/tag/{tagId}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity unassignTag(@PathVariable("id") int id, @PathVariable("tagId") int tagId) { - reusableService.unassignTag(id, tagId); - return ok(); - } - - /** - * Assign protected tag to Reusable - * - * Jersey: POST /WebAPI/reusable/{id}/protectedtag/ - * Spring MVC: POST /WebAPI/v2/reusable/{id}/protectedtag/ - * - * @param id the reusable ID - * @param tagId the tag ID - */ - @PostMapping( - value = "/{id}/protectedtag/", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity assignPermissionProtectedTag(@PathVariable("id") int id, @RequestBody int tagId) { - reusableService.assignTag(id, tagId); - return ok(); - } - - /** - * Unassign protected tag from Reusable - * - * Jersey: DELETE /WebAPI/reusable/{id}/protectedtag/{tagId} - * Spring MVC: DELETE /WebAPI/v2/reusable/{id}/protectedtag/{tagId} - * - * @param id the reusable ID - * @param tagId the tag ID - */ - @DeleteMapping( - value = "/{id}/protectedtag/{tagId}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity unassignPermissionProtectedTag(@PathVariable("id") int id, @PathVariable("tagId") int tagId) { - reusableService.unassignTag(id, tagId); - return ok(); - } - - /** - * Get list of versions of Reusable - * - * Jersey: GET /WebAPI/reusable/{id}/version/ - * Spring MVC: GET /WebAPI/v2/reusable/{id}/version/ - * - * @param id the reusable ID - * @return list of versions - */ - @GetMapping( - value = "/{id}/version/", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity> getVersions(@PathVariable("id") long id) { - return ok(reusableService.getVersions(id)); - } - - /** - * Get version of Reusable - * - * Jersey: GET /WebAPI/reusable/{id}/version/{version} - * Spring MVC: GET /WebAPI/v2/reusable/{id}/version/{version} - * - * @param id the reusable ID - * @param version the version number - * @return the version - */ - @GetMapping( - value = "/{id}/version/{version}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity getVersion(@PathVariable("id") int id, @PathVariable("version") int version) { - return ok(reusableService.getVersion(id, version)); - } - - /** - * Update version of Reusable - * - * Jersey: PUT /WebAPI/reusable/{id}/version/{version} - * Spring MVC: PUT /WebAPI/v2/reusable/{id}/version/{version} - * - * @param id the reusable ID - * @param version the version number - * @param updateDTO the version update DTO - * @return updated version - */ - @PutMapping( - value = "/{id}/version/{version}", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity updateVersion( - @PathVariable("id") int id, - @PathVariable("version") int version, - @RequestBody VersionUpdateDTO updateDTO) { - return ok(reusableService.updateVersion(id, version, updateDTO)); - } - - /** - * Delete version of Reusable - * - * Jersey: DELETE /WebAPI/reusable/{id}/version/{version} - * Spring MVC: DELETE /WebAPI/v2/reusable/{id}/version/{version} - * - * @param id the reusable ID - * @param version the version number - */ - @DeleteMapping( - value = "/{id}/version/{version}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity deleteVersion(@PathVariable("id") int id, @PathVariable("version") int version) { - reusableService.deleteVersion(id, version); - return ok(); - } - - /** - * Create a new asset from version of Reusable - * - * Jersey: PUT /WebAPI/reusable/{id}/version/{version}/createAsset - * Spring MVC: PUT /WebAPI/v2/reusable/{id}/version/{version}/createAsset - * - * @param id the reusable ID - * @param version the version number - * @return new reusable created from version - */ - @PutMapping( - value = "/{id}/version/{version}/createAsset", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity copyAssetFromVersion(@PathVariable("id") int id, @PathVariable("version") int version) { - return ok(reusableService.copyAssetFromVersion(id, version)); - } - - /** - * Get list of reusables with assigned tags - * - * Jersey: POST /WebAPI/reusable/byTags - * Spring MVC: POST /WebAPI/v2/reusable/byTags - * - * @param requestDTO tag name list request - * @return list of reusables - */ - @PostMapping( - value = "/byTags", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity> listByTags(@RequestBody TagNameListRequestDTO requestDTO) { - if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { - return ok(Collections.emptyList()); - } - return ok(reusableService.listByTags(requestDTO)); - } -} diff --git a/src/main/java/org/ohdsi/webapi/reusable/ReusableService.java b/src/main/java/org/ohdsi/webapi/reusable/ReusableService.java index 846a005703..a4dd80b517 100644 --- a/src/main/java/org/ohdsi/webapi/reusable/ReusableService.java +++ b/src/main/java/org/ohdsi/webapi/reusable/ReusableService.java @@ -1,5 +1,6 @@ package org.ohdsi.webapi.reusable; +import org.ohdsi.webapi.Pagination; import org.ohdsi.webapi.reusable.domain.Reusable; import org.ohdsi.webapi.reusable.dto.ReusableDTO; import org.ohdsi.webapi.reusable.dto.ReusableVersionFullDTO; @@ -23,11 +24,21 @@ import org.springframework.core.convert.ConversionService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; +import org.springframework.http.MediaType; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import jakarta.persistence.EntityManager; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -35,7 +46,8 @@ import java.util.stream.Collectors; import java.util.Optional; -@Service +@RestController +@RequestMapping("/reusable") @Transactional public class ReusableService extends AbstractDaoService implements HasTags { private final ReusableRepository reusableRepository; @@ -58,7 +70,12 @@ public ReusableService( this.versionService = versionService; } - public ReusableDTO create(ReusableDTO dto) { + @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ReusableDTO create(@RequestBody ReusableDTO dto) { + return createInternal(dto); + } + + private ReusableDTO createInternal(ReusableDTO dto) { Reusable reusable = conversionService.convert(dto, Reusable.class); Reusable saved = create(reusable); return conversionService.convert(saved, ReusableDTO.class); @@ -77,7 +94,8 @@ public Reusable getById(Integer id) { return reusableRepository.findById(id).orElse(null); } - public ReusableDTO getDTOById(Integer id) { + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ReusableDTO getDTOById(@PathVariable("id") Integer id) { Reusable reusable = reusableRepository.findById(id).orElse(null); return conversionService.convert(reusable, ReusableDTO.class); } @@ -86,7 +104,8 @@ public List list() { return reusableRepository.findAll(); } - public Page page(final Pageable pageable) { + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public Page page(@Pagination Pageable pageable) { return reusableRepository.findAll(pageable) .map(reusable -> { final ReusableDTO dto = conversionService.convert(reusable, ReusableDTO.class); @@ -95,7 +114,8 @@ public Page page(final Pageable pageable) { }); } - public ReusableDTO update(Integer id, ReusableDTO entity) { + @PutMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ReusableDTO update(@PathVariable("id") Integer id, @RequestBody ReusableDTO entity) { Date currentTime = Calendar.getInstance().getTime(); saveVersion(id); @@ -113,26 +133,42 @@ public ReusableDTO update(Integer id, ReusableDTO entity) { return conversionService.convert(saved, ReusableDTO.class); } - public ReusableDTO copy(Integer id) { + @PostMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ReusableDTO copy(@PathVariable("id") Integer id) { ReusableDTO def = getDTOById(id); def.setId(null); def.setTags(null); def.setName(NameUtils.getNameForCopy(def.getName(), this::getNamesLike, reusableRepository.findByName(def.getName()))); - return create(def); + return createInternal(def); } - public void assignTag(Integer id, int tagId) { + @PostMapping(value = "/{id}/tag/", produces = MediaType.APPLICATION_JSON_VALUE) + public void assignTag(@PathVariable("id") Integer id, @RequestBody int tagId) { Reusable entity = getById(id); assignTag(entity, tagId); } - public void unassignTag(Integer id, int tagId) { + @DeleteMapping(value = "/{id}/tag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) + public void unassignTag(@PathVariable("id") Integer id, @PathVariable("tagId") int tagId) { Reusable entity = getById(id); unassignTag(entity, tagId); } - public void delete(Integer id) { + @PostMapping(value = "/{id}/protectedtag/", produces = MediaType.APPLICATION_JSON_VALUE) + public void assignPermissionProtectedTag(@PathVariable("id") int id, @RequestBody int tagId) { + Reusable entity = getById(id); + assignTag(entity, tagId); + } + + @DeleteMapping(value = "/{id}/protectedtag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) + public void unassignPermissionProtectedTag(@PathVariable("id") int id, @PathVariable("tagId") int tagId) { + Reusable entity = getById(id); + unassignTag(entity, tagId); + } + + @DeleteMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public void delete(@PathVariable("id") Integer id) { Reusable existing = reusableRepository.findById(id).orElse(null); checkOwnerOrAdminOrModerator(existing.getCreatedBy()); @@ -140,21 +176,24 @@ public void delete(Integer id) { reusableRepository.deleteById(id); } - public List getVersions(long id) { + @GetMapping(value = "/{id}/version/", produces = MediaType.APPLICATION_JSON_VALUE) + public List getVersions(@PathVariable("id") long id) { List versions = versionService.getVersions(VersionType.REUSABLE, id); return versions.stream() .map(v -> conversionService.convert(v, VersionDTO.class)) .collect(Collectors.toList()); } - public ReusableVersionFullDTO getVersion(int id, int version) { + @GetMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) + public ReusableVersionFullDTO getVersion(@PathVariable("id") int id, @PathVariable("version") int version) { checkVersion(id, version, false); ReusableVersion reusableVersion = versionService.getById(VersionType.REUSABLE, id, version); return conversionService.convert(reusableVersion, ReusableVersionFullDTO.class); } - public VersionDTO updateVersion(int id, int version, VersionUpdateDTO updateDTO) { + @PutMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public VersionDTO updateVersion(@PathVariable("id") int id, @PathVariable("version") int version, @RequestBody VersionUpdateDTO updateDTO) { checkVersion(id, version); updateDTO.setAssetId(id); updateDTO.setVersion(version); @@ -163,12 +202,14 @@ public VersionDTO updateVersion(int id, int version, VersionUpdateDTO updateDTO) return conversionService.convert(updated, VersionDTO.class); } - public void deleteVersion(int id, int version) { + @DeleteMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) + public void deleteVersion(@PathVariable("id") int id, @PathVariable("version") int version) { checkVersion(id, version); versionService.delete(VersionType.REUSABLE, id, version); } - public ReusableDTO copyAssetFromVersion(int id, int version) { + @PutMapping(value = "/{id}/version/{version}/createAsset", produces = MediaType.APPLICATION_JSON_VALUE) + public ReusableDTO copyAssetFromVersion(@PathVariable("id") int id, @PathVariable("version") int version) { checkVersion(id, version, false); ReusableVersion reusableVersion = versionService.getById(VersionType.REUSABLE, id, version); ReusableVersionFullDTO fullDTO = conversionService.convert(reusableVersion, ReusableVersionFullDTO.class); @@ -177,10 +218,14 @@ public ReusableDTO copyAssetFromVersion(int id, int version) { dto.setTags(null); dto.setName(NameUtils.getNameForCopy(dto.getName(), this::getNamesLike, reusableRepository.findByName(dto.getName()))); - return create(dto); + return createInternal(dto); } - public List listByTags(TagNameListRequestDTO requestDTO) { + @PostMapping(value = "/byTags", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public List listByTags(@RequestBody TagNameListRequestDTO requestDTO) { + if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { + return Collections.emptyList(); + } List names = requestDTO.getNames().stream() .map(name -> name.toLowerCase(Locale.ROOT)) .collect(Collectors.toList()); @@ -220,7 +265,8 @@ private Reusable save(Reusable reusable) { return reusableRepository.findById(reusable.getId()).orElse(null); } - public boolean exists(final int id, final String name) { + @GetMapping(value = "/{id}/exists", produces = MediaType.APPLICATION_JSON_VALUE) + public boolean exists(@PathVariable("id") int id, @RequestParam(value = "name", required = false) String name) { return reusableRepository.existsCount(id, name) > 0; } diff --git a/src/main/java/org/ohdsi/webapi/security/PermissionService.java b/src/main/java/org/ohdsi/webapi/security/PermissionService.java index 505c600eba..9516cede5c 100644 --- a/src/main/java/org/ohdsi/webapi/security/PermissionService.java +++ b/src/main/java/org/ohdsi/webapi/security/PermissionService.java @@ -4,11 +4,14 @@ import com.cosium.spring.data.jpa.entity.graph.domain2.DynamicEntityGraph; import org.apache.shiro.authz.UnauthorizedException; import org.ohdsi.webapi.model.CommonEntity; +import org.ohdsi.webapi.security.dto.AccessRequestDTO; +import org.ohdsi.webapi.security.dto.RoleDTO; import org.ohdsi.webapi.security.model.EntityPermissionSchema; import org.ohdsi.webapi.security.model.EntityPermissionSchemaResolver; import org.ohdsi.webapi.security.model.EntityType; import org.ohdsi.webapi.security.model.SourcePermissionSchema; import org.ohdsi.webapi.security.model.UserSimpleAuthorizationInfo; +import org.ohdsi.webapi.service.UserService; import org.ohdsi.webapi.service.dto.CommonEntityDTO; import org.ohdsi.webapi.shiro.Entities.PermissionEntity; import org.ohdsi.webapi.shiro.Entities.PermissionRepository; @@ -28,7 +31,16 @@ import org.springframework.core.convert.ConversionService; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.support.Repositories; -import org.springframework.stereotype.Service; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.WebApplicationContext; import jakarta.annotation.PostConstruct; @@ -39,12 +51,15 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.permission.WildcardPermission; import org.apache.shiro.subject.Subject; -@Service +@RestController +@RequestMapping("/permission") +@Transactional public class PermissionService { private final Logger logger = LoggerFactory.getLogger(PermissionService.class); @@ -248,4 +263,145 @@ public String getAssetListCacheKey() { public String getSubjectCacheKey() { return this.isSecurityEnabled() ? permissionManager.getSubjectName() : "ALL_USERS"; } + + // ==================== REST Endpoints ==================== + + /** + * Get the list of permissions for a user + * + * @return A list of permissions + */ + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getPermissions() { + Iterable permissionEntities = permissionManager.getPermissions(); + return StreamSupport.stream(permissionEntities.spliterator(), false) + .map(UserService.Permission::new) + .collect(Collectors.toList()); + } + + /** + * Get the roles matching the roleSearch value + * + * @summary Role search + * @param roleSearch The role to search + * @return The list of roles + */ + @GetMapping( + value = "/access/suggest", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public List listAccessesForEntitySuggest(@RequestParam("roleSearch") String roleSearch) { + List roles = suggestRoles(roleSearch); + return roles.stream() + .map(re -> conversionService.convert(re, RoleDTO.class)) + .collect(Collectors.toList()); + } + + /** + * Get roles that have a permission type (READ/WRITE) to entity + * + * @summary Get roles that have a specific permission (READ/WRITE) for the entity + * @param entityType The entity type + * @param entityId The entity ID + * @param permType The permission type + * @return The list of permissions for the permission type + * @throws Exception + */ + @GetMapping( + value = "/access/{entityType}/{entityId}/{permType}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public List listAccessesForEntityByPermType( + @PathVariable("entityType") EntityType entityType, + @PathVariable("entityId") Integer entityId, + @PathVariable("permType") AccessType permType) throws Exception { + checkCommonEntityOwnership(entityType, entityId); + var permissionTemplates = getTemplatesForType(entityType, permType).keySet(); + + List permissions = permissionTemplates.stream() + .map(pt -> getPermission(pt, entityId)) + .collect(Collectors.toList()); + + List roles = finaAllRolesHavingPermissions(permissions); + + return roles.stream() + .map(re -> conversionService.convert(re, RoleDTO.class)) + .collect(Collectors.toList()); + } + + /** + * Get roles that have a permission type (READ/WRITE) to entity + * + * @summary Get roles that have a specific permission (READ/WRITE) for the entity + * @param entityType The entity type + * @param entityId The entity ID + * @return The list of permissions for the permission type + * @throws Exception + */ + @GetMapping( + value = "/access/{entityType}/{entityId}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public List listAccessesForEntity( + @PathVariable("entityType") EntityType entityType, + @PathVariable("entityId") Integer entityId) throws Exception { + return listAccessesForEntityByPermType(entityType, entityId, AccessType.WRITE); + } + + /** + * Grant group of permissions (READ / WRITE / ...) for the specified entity to the given role. + * Only owner of the entity can do that. + * + * @summary Grant permissions + * @param entityType The entity type + * @param entityId The entity ID + * @param roleId The role ID + * @param accessRequestDTO The access request object + * @throws Exception + */ + @PostMapping( + value = "/access/{entityType}/{entityId}/role/{roleId}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public void grantEntityPermissionsForRole( + @PathVariable("entityType") EntityType entityType, + @PathVariable("entityId") Integer entityId, + @PathVariable("roleId") Long roleId, + @RequestBody AccessRequestDTO accessRequestDTO) throws Exception { + checkCommonEntityOwnership(entityType, entityId); + + var permissionTemplates = getTemplatesForType(entityType, accessRequestDTO.getAccessType()); + + RoleEntity role = permissionManager.getRole(roleId); + permissionManager.addPermissionsFromTemplate(role, permissionTemplates, entityId.toString()); + } + + /** + * Remove group of permissions for the specified entity to the given role. + * + * @summary Remove permissions + * @param entityType The entity type + * @param entityId The entity ID + * @param roleId The role ID + * @param accessRequestDTO The access request object + * @throws Exception + */ + @DeleteMapping( + value = "/access/{entityType}/{entityId}/role/{roleId}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public void revokeEntityPermissionsFromRole( + @PathVariable("entityType") EntityType entityType, + @PathVariable("entityId") Integer entityId, + @PathVariable("roleId") Long roleId, + @RequestBody AccessRequestDTO accessRequestDTO) throws Exception { + checkCommonEntityOwnership(entityType, entityId); + var permissionTemplates = getTemplatesForType(entityType, accessRequestDTO.getAccessType()); + removePermissionsFromRole(permissionTemplates, entityId, roleId); + } } diff --git a/src/main/java/org/ohdsi/webapi/service/CDMResultsService.java b/src/main/java/org/ohdsi/webapi/service/CDMResultsService.java index ce46582110..825c7be5e4 100644 --- a/src/main/java/org/ohdsi/webapi/service/CDMResultsService.java +++ b/src/main/java/org/ohdsi/webapi/service/CDMResultsService.java @@ -40,11 +40,19 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.DependsOn; import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; @@ -63,13 +71,14 @@ import static org.ohdsi.webapi.cdmresults.AchillesCacheTasklet.OBSERVATION_PERIOD; import static org.ohdsi.webapi.cdmresults.AchillesCacheTasklet.PERSON; import static org.ohdsi.webapi.cdmresults.AchillesCacheTasklet.TREEMAP; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.http.HttpStatus; /** + * CDM Results Service - provides REST endpoints for CDM results and Achilles reports. + * * @author fdefalco */ -@Component +@RestController +@RequestMapping("/cdmresults") @DependsOn({"jobInvalidator", "flyway"}) public class CDMResultsService extends AbstractDaoService implements InitializingBean { private final Logger logger = LoggerFactory.getLogger(CDMResultsService.class); @@ -147,6 +156,7 @@ public void scheduledWarmCaches(){ *

* * @param sourceKey The unique identifier for a CDM source (e.g. SYNPUF5PCT) + * @param identifiers List of concept IDs * * @return A javascript object with one element per concept. Each element is an array of lenth two containing the * record count and descendent record count for the concept. @@ -169,7 +179,12 @@ public void scheduledWarmCaches(){ *

* For concept id "201826" in the SYNPUF5PCT data source the record count is 612861 and the descendant record count is 653173. */ - public List>> getConceptRecordCount(String sourceKey, List identifiers) { + @PostMapping(value = "/{sourceKey}/conceptRecordCount", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public List>> getConceptRecordCount( + @PathVariable("sourceKey") String sourceKey, + @RequestBody List identifiers) { Source source = sourceService.findBySourceKey(sourceKey); if (source != null) { List entities = cdmCacheService.findAndCache(source, identifiers); @@ -196,10 +211,12 @@ private List>> convertToResponse(Collection + * @param sourceKey The source key + * @param domain The domain + * @return ArrayNode */ + @GetMapping(value = "/{sourceKey}/{domain}/", produces = MediaType.APPLICATION_JSON_VALUE) @AchillesCache(TREEMAP) public ArrayNode getTreemap( - final String domain, - final String sourceKey) { + @PathVariable("sourceKey") final String sourceKey, + @PathVariable("domain") final String domain) { return getRawTreeMap(domain, sourceKey); } @@ -359,16 +388,18 @@ public ArrayNode getRawTreeMap(String domain, String sourceKey) { /** * Queries for drilldown results - * + * + * @param sourceKey The source key * @param domain The domain for the drilldown * @param conceptId The concept ID - * @param sourceKey The source key * @return The JSON results */ + @GetMapping(value = "/{sourceKey}/{domain}/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) @AchillesCache(DRILLDOWN) - public JsonNode getDrilldown(final String domain, - final int conceptId, - final String sourceKey) { + public JsonNode getDrilldown( + @PathVariable("sourceKey") final String sourceKey, + @PathVariable("domain") final String domain, + @PathVariable("conceptId") final int conceptId) { return getRawDrilldown(domain, conceptId, sourceKey); } diff --git a/src/main/java/org/ohdsi/webapi/service/CohortAnalysisService.java b/src/main/java/org/ohdsi/webapi/service/CohortAnalysisService.java index d8c71c06aa..57e9fd7aef 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortAnalysisService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortAnalysisService.java @@ -27,22 +27,29 @@ import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.jdbc.core.RowMapper; -import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; /** - * REST Services related to running - * cohort analysis (a.k.a Heracles) analyses. + * REST Services related to running + * cohort analysis (a.k.a Heracles) analyses. * More information on the Heracles project * can be found at {@link https://www.ohdsi.org/web/wiki/doku.php?id=documentation:software:heracles}. * The implementation found in WebAPI represents a migration of the functionality * from the stand-alone HERACLES application to integrate it into WebAPI and * ATLAS. - * + * * @summary Cohort Analysis (a.k.a Heracles) */ -@Component +@RestController +@RequestMapping("/cohortanalysis") public class CohortAnalysisService extends AbstractDaoService implements GeneratesNotification { public static final String NAME = "cohortAnalysisJob"; @@ -112,10 +119,11 @@ private void mapAnalysis(final Analysis analysis, final ResultSet rs, final int /** * Returns all cohort analyses in the WebAPI database - * + * * @summary Get all cohort analyses * @return List of all cohort analyses */ + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public List getCohortAnalyses() { String sqlPath = "/resources/cohortanalysis/sql/getCohortAnalyses.sql"; String search = "ohdsi_database_schema"; @@ -124,16 +132,17 @@ public List getCohortAnalyses() { return getJdbcTemplate().query(psr.getSql(), psr.getSetter(), this.analysisMapper); } - /** - * Returns all cohort analyses in the WebAPI database - * for the given cohort_definition_id - * - * @summary Get cohort analyses by cohort ID - * @param id The cohort definition identifier - * @return List of all cohort analyses and their statuses - * for the given cohort_definition_id - */ - public List getCohortAnalysesForCohortDefinition(final int id) { + /** + * Returns all cohort analyses in the WebAPI database + * for the given cohort_definition_id + * + * @summary Get cohort analyses by cohort ID + * @param id The cohort definition identifier + * @return List of all cohort analyses and their statuses + * for the given cohort_definition_id + */ + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public List getCohortAnalysesForCohortDefinition(@PathVariable("id") final int id) { String sqlPath = "/resources/cohortanalysis/sql/getCohortAnalysesForCohort.sql"; String tqName = "ohdsi_database_schema"; String tqValue = getOhdsiSchema(); @@ -141,16 +150,17 @@ public List getCohortAnalysesForCohortDefinition(final int id) { return getJdbcTemplate().query(psr.getSql(), psr.getSetter(), this.cohortAnalysisMapper); } - /** - * Returns the summary for the cohort - * - * @summary Cohort analysis summary - * @param id - the cohort_definition id - * @return Summary which includes the base cohort_definition, the cohort analyses list and their - * statuses for this cohort, and a base set of common cohort results that may or may not - * yet have been ran - */ - public CohortSummary getCohortSummary(final int id) { + /** + * Returns the summary for the cohort + * + * @summary Cohort analysis summary + * @param id - the cohort_definition id + * @return Summary which includes the base cohort_definition, the cohort analyses list and their + * statuses for this cohort, and a base set of common cohort results that may or may not + * yet have been ran + */ + @GetMapping(value = "/{id}/summary", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortSummary getCohortSummary(@PathVariable("id") final int id) { CohortSummary summary = new CohortSummary(); try { @@ -163,17 +173,20 @@ public CohortSummary getCohortSummary(final int id) { return summary; } - /** - * Generates a preview of the cohort analysis SQL used to run - * the Cohort Analysis Job + /** + * Generates a preview of the cohort analysis SQL used to run + * the Cohort Analysis Job * - * @summary Cohort analysis SQL preview + * @summary Cohort analysis SQL preview * @param task - the CohortAnalysisTask, be sure to have a least one * analysis_id and one cohort_definition id * @return - SQL for the given CohortAnalysisTask translated and rendered to * the current dialect */ - public String getRunCohortAnalysisSql(CohortAnalysisTask task) { + @PostMapping(value = "/preview", + produces = MediaType.TEXT_PLAIN_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public String getRunCohortAnalysisSql(@RequestBody CohortAnalysisTask task) { task.setSmallCellCount(Integer.parseInt(this.smallCellCount)); return heraclesQueryBuilder.buildHeraclesAnalysisQuery(task); } @@ -207,12 +220,14 @@ public String[] getRunCohortAnalysisSqlBatch(CohortAnalysisTask task) { * Queues up a cohort analysis task, that generates and translates SQL for the * given cohort definitions, analysis ids and concept ids * - * @summary Queue cohort analysis job + * @summary Queue cohort analysis job * @param task The cohort analysis task to be ran * @return information about the Cohort Analysis Job * @throws Exception */ - public JobExecutionResource queueCohortAnalysisJob(CohortAnalysisTask task) throws Exception { + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public JobExecutionResource queueCohortAnalysisJob(@RequestBody CohortAnalysisTask task) throws Exception { if (task == null) { return null; } diff --git a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java index b8e2854497..7e56a5d542 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java @@ -142,6 +142,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; /** * Provides REST services for working with cohort definitions. @@ -149,7 +150,8 @@ * @summary Provides REST services for working with cohort definitions. * @author cknoll1 */ -@Component +@RestController +@RequestMapping("/cohortdefinition") public class CohortDefinitionService extends AbstractDaoService implements HasTags { //create cache @@ -406,13 +408,14 @@ public GenerateSqlRequest() { ServletContext context; /** - * Returns OHDSI template SQL for a given cohort definition + * Returns OHDSI template SQL for a given cohort definition * * @summary Generate Sql * @param request A GenerateSqlRequest containing the cohort expression and options. * @return The OHDSI template SQL needed to generate the input cohort definition as a character string */ - public GenerateSqlResult generateSql(GenerateSqlRequest request) { + @PostMapping(value = "/sql", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public GenerateSqlResult generateSql(@RequestBody GenerateSqlRequest request) { CohortExpressionQueryBuilder.BuildExpressionQueryOptions options = request.options; GenerateSqlResult result = new GenerateSqlResult(); if (options == null) { @@ -431,6 +434,7 @@ public GenerateSqlResult generateSql(GenerateSqlRequest request) { * @return List of metadata about all cohort definitions in WebAPI * @see org.ohdsi.webapi.cohortdefinition.CohortMetadataDTO */ + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional @Cacheable(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()") public List getCohortDefinitionList() { @@ -456,9 +460,10 @@ public List getCohortDefinitionList() { * @param dto The cohort definition to create. * @return The newly created cohort definition */ + @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) - public CohortDTO createCohortDefinition(CohortDTO dto) { + public CohortDTO createCohortDefinition(@RequestBody CohortDTO dto) { Date currentTime = Calendar.getInstance().getTime(); @@ -497,7 +502,8 @@ public CohortDTO createCohortDefinition(CohortDTO dto) { * @param id The cohort definition id * @return The cohort definition JSON expression */ - public CohortRawDTO getCohortDefinitionRaw(final int id) { + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortRawDTO getCohortDefinitionRaw(@PathVariable("id") final int id) { return getTransactionTemplate().execute(transactionStatus -> { CohortDefinition d = this.cohortDefinitionRepository.findOneWithDetail(id); ExceptionUtils.throwNotFoundExceptionIfNull(d, String.format("There is no cohort definition with id = %d.", id)); @@ -534,7 +540,8 @@ public CohortDTO getCohortDefinition(final int id) { * @return 1 if the a cohort with the given name and id exist in WebAPI and 0 * otherwise */ - public int getCountCDefWithSameName(final int id, String name) { + @GetMapping(value = "/{id}/exists", produces = MediaType.APPLICATION_JSON_VALUE) + public int getCountCDefWithSameName(@PathVariable("id") final int id, @RequestParam("name") String name) { return cohortDefinitionRepository.getCountCDefWithSameName(id, name); } @@ -549,9 +556,10 @@ public int getCountCDefWithSameName(final int id, String name) { * @param id The cohort definition id * @return The updated CohortDefinition */ + @PutMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) - public CohortDTO saveCohortDefinition(final int id, CohortDTO def) { + public CohortDTO saveCohortDefinition(@PathVariable("id") final int id, @RequestBody CohortDTO def) { Date currentTime = Calendar.getInstance().getTime(); saveVersion(id); @@ -579,9 +587,10 @@ public CohortDTO saveCohortDefinition(final int id, CohortDTO def) { * @param sourceKey The source to execute the cohort generation * @return the job info for the cohort generation */ - public JobExecutionResource generateCohort(final int id, - final String sourceKey, - boolean demographicStat) { + @GetMapping(value = "/{id}/generate/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) + public JobExecutionResource generateCohort(@PathVariable("id") final int id, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "demographicStat", defaultValue = "false") boolean demographicStat) { // Load entities within a transaction and eagerly initialize all lazy fields Source source = transactionTemplate.execute(status -> { Source s = getSourceRepository().findBySourceKey(sourceKey); @@ -632,7 +641,8 @@ public JobExecutionResource generateCohort(final int id, * @param sourceKey the sourceKey for the target database for generation * @return */ - public ResponseEntity cancelGenerateCohort(final int id, final String sourceKey) { + @GetMapping(value = "/{id}/cancel/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity cancelGenerateCohort(@PathVariable("id") final int id, @PathVariable("sourceKey") final String sourceKey) { final Source source = Optional.ofNullable(getSourceRepository().findBySourceKey(sourceKey)) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); @@ -671,8 +681,9 @@ public ResponseEntity cancelGenerateCohort(final int id, final String sourceKey) * @return information about the Cohort Analysis Job for each source * @throws NotFoundException */ + @GetMapping(value = "/{id}/info", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public List getInfo(final int id) { + public List getInfo(@PathVariable("id") final int id) { CohortDefinition def = this.cohortDefinitionRepository.findById(id).orElse(null); ExceptionUtils.throwNotFoundExceptionIfNull(def, String.format("There is no cohort definition with id = %d.", id)); @@ -698,9 +709,10 @@ public List getInfo(final int id) { * @param id - the Cohort Definition ID to copy * @return the copied cohort definition as a CohortDTO */ + @GetMapping(value = "/{id}/copy", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) - public CohortDTO copy(final int id) { + public CohortDTO copy(@PathVariable("id") final int id) { CohortDTO sourceDef = getCohortDefinition(id); sourceDef.setId(null); // clear the ID sourceDef.setTags(null); @@ -724,8 +736,9 @@ public List getNamesLike(String copyName) { * @summary Delete Cohort Definition * @param id - the Cohort Definition ID to delete */ + @DeleteMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) - public void delete(final int id) { + public void delete(@PathVariable("id") final int id) { // perform the JPA update in a separate transaction this.getTransactionTemplateRequiresNew().execute(new TransactionCallbackWithoutResult() { @Override @@ -805,7 +818,8 @@ private List getConceptSetExports(CohortDefinition def, Source * @param id a cohort definition id * @return a binary stream containing the zip file with concept sets. */ - public ResponseEntity exportConceptSets(final int id) { + @GetMapping(value = "/{id}/export/conceptset", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity exportConceptSets(@PathVariable("id") final int id) { Source source = sourceService.getPriorityVocabularySource(); if (Objects.isNull(source)) { @@ -836,11 +850,13 @@ public ResponseEntity exportConceptSets(final int id) { * @param modeId the mode of the report: 0 = all events, 1 = best event * @return a binary stream containing the zip file with concept sets. */ + @GetMapping(value = "/{id}/report/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional public InclusionRuleReport getInclusionRuleReport( - final int id, - final String sourceKey, - int modeId, String ccGenerateId) { + @PathVariable("id") final int id, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "mode", defaultValue = "0") int modeId, + @RequestParam(value = "ccGenerateId", required = false) String ccGenerateId) { Source source = this.getSourceRepository().findBySourceKey(sourceKey); @@ -856,36 +872,38 @@ public InclusionRuleReport getInclusionRuleReport( return report; } - /** - * Checks the cohort definition for logic issues - * - * This method runs a series of logical checks on a cohort definition and - * returns the set of warning, info and error messages. - * - * @summary Check Cohort Definition - * @param expression - * The cohort definition expression - * @return The cohort check result - */ + /** + * Checks the cohort definition for logic issues + * + * This method runs a series of logical checks on a cohort definition and + * returns the set of warning, info and error messages. + * + * @summary Check Cohort Definition + * @param expression + * The cohort definition expression + * @return The cohort check result + */ + @PostMapping(value = "/check", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public CheckResultDTO runDiagnostics(CohortExpression expression) { + public CheckResultDTO runDiagnostics(@RequestBody CohortExpression expression) { Checker checker = new Checker(); return new CheckResultDTO(checker.check(expression)); } /** * Checks the cohort definition for logic issues - * + * * This method runs a series of logical checks on a cohort definition and returns the set of warning, info and error messages. - * + * * This method is similar to /check except this method accepts a ChortDTO which includes tags. * * @summary Check Cohort Definition * @param cohortDTO The cohort definition expression * @return The cohort check result */ + @PostMapping(value = "/checkV2", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public CheckResult runDiagnosticsWithTags(CohortDTO cohortDTO) { + public CheckResult runDiagnosticsWithTags(@RequestBody CohortDTO cohortDTO) { Checker checker = new Checker(); CheckResultDTO checkResultDTO = new CheckResultDTO(checker.check(cohortDTO.getExpression())); List circeWarnings = checkResultDTO.getWarnings().stream() @@ -911,7 +929,8 @@ public CheckResult runDiagnosticsWithTags(CohortDTO cohortDTO) { * @return an HTTP response with the content, with the appropriate MediaType * based on the format that was requested. */ - public ResponseEntity cohortPrintFriendly(CohortExpression expression, String format) { + @PostMapping(value = "/printfriendly/cohort", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity cohortPrintFriendly(@RequestBody CohortExpression expression, @RequestParam(value = "format", defaultValue = "html") String format) { String markdown = convertCohortExpressionToMarkdown(expression); return printFrindly(markdown, format); } @@ -930,7 +949,8 @@ public ResponseEntity cohortPrintFriendly(CohortExpression expression, String fo * @return an HTTP response with the content, with the appropriate MediaType * based on the format that was requested. */ - public ResponseEntity conceptSetListPrintFriendly(List conceptSetList, String format) { + @PostMapping(value = "/printfriendly/conceptsets", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity conceptSetListPrintFriendly(@RequestBody List conceptSetList, @RequestParam(value = "format", defaultValue = "html") String format) { String markdown = markdownPF.renderConceptSetList(conceptSetList.toArray(new ConceptSet[0])); return printFrindly(markdown, format); } @@ -964,9 +984,10 @@ private ResponseEntity printFrindly(String markdown, String format) { * @param id the cohort definition id * @param tagId the tag id */ + @PostMapping(value = "/{id}/tag", produces = MediaType.APPLICATION_JSON_VALUE) @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) @Transactional - public void assignTag(final Integer id, final int tagId) { + public void assignTag(@PathVariable("id") final Integer id, @RequestBody final int tagId) { CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); assignTag(entity, tagId); } @@ -978,9 +999,10 @@ public void assignTag(final Integer id, final int tagId) { * @param id the cohort definition id * @param tagId the tag id */ + @DeleteMapping(value = "/{id}/tag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) @Transactional - public void unassignTag(final Integer id, final int tagId) { + public void unassignTag(@PathVariable("id") final Integer id, @PathVariable("tagId") final int tagId) { CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); unassignTag(entity, tagId); } @@ -994,8 +1016,9 @@ public void unassignTag(final Integer id, final int tagId) { * @param id * @param tagId */ + @PostMapping(value = "/{id}/protectedtag", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public void assignPermissionProtectedTag(final int id, final int tagId) { + public void assignPermissionProtectedTag(@PathVariable("id") final int id, @RequestBody final int tagId) { assignTag(id, tagId); } @@ -1006,8 +1029,9 @@ public void assignPermissionProtectedTag(final int id, final int tagId) { * @param id * @param tagId */ + @DeleteMapping(value = "/{id}/protectedtag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public void unassignPermissionProtectedTag(final int id, final int tagId) { + public void unassignPermissionProtectedTag(@PathVariable("id") final int id, @PathVariable("tagId") final int tagId) { unassignTag(id, tagId); } @@ -1019,8 +1043,9 @@ public void unassignPermissionProtectedTag(final int id, final int tagId) { * @return the list of VersionDTO containing version info for the cohort * definition */ + @GetMapping(value = "/{id}/version", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public List getVersions(final long id) { + public List getVersions(@PathVariable("id") final long id) { List versions = versionService.getVersions(VersionType.COHORT, id); return versions.stream() .map(v -> conversionService.convert(v, VersionDTO.class)) @@ -1035,8 +1060,9 @@ public List getVersions(final long id) { * @param version The version to fetch * @return */ + @GetMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public CohortVersionFullDTO getVersion(final int id, final int version) { + public CohortVersionFullDTO getVersion(@PathVariable("id") final int id, @PathVariable("version") final int version) { checkVersion(id, version, false); CohortVersion cohortVersion = versionService.getById(VersionType.COHORT, id, version); @@ -1055,9 +1081,10 @@ public CohortVersionFullDTO getVersion(final int id, final int version) { * @param updateDTO the new version data * @return the updated version state as VersionDTO */ + @PutMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public VersionDTO updateVersion(final int id, final int version, - VersionUpdateDTO updateDTO) { + public VersionDTO updateVersion(@PathVariable("id") final int id, @PathVariable("version") final int version, + @RequestBody VersionUpdateDTO updateDTO) { checkVersion(id, version); updateDTO.setAssetId(id); updateDTO.setVersion(version); @@ -1073,8 +1100,9 @@ public VersionDTO updateVersion(final int id, final int version, * @param id the cohort definition id * @param version the version id */ + @DeleteMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public void deleteVersion(final int id, final int version) { + public void deleteVersion(@PathVariable("id") final int id, @PathVariable("version") final int version) { checkVersion(id, version); versionService.delete(VersionType.COHORT, id, version); } @@ -1091,9 +1119,10 @@ public void deleteVersion(final int id, final int version) { * @param version the version id * @return */ + @PutMapping(value = "/{id}/version/{version}/createAsset", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) - public CohortDTO copyAssetFromVersion(final int id, final int version) { + public CohortDTO copyAssetFromVersion(@PathVariable("id") final int id, @PathVariable("version") final int version) { checkVersion(id, version, false); CohortVersion cohortVersion = versionService.getById(VersionType.COHORT, id, version); CohortVersionFullDTO fullDTO = conversionService.convert(cohortVersion, CohortVersionFullDTO.class); @@ -1108,15 +1137,16 @@ public CohortDTO copyAssetFromVersion(final int id, final int version) { /** * Get list of cohort definitions with assigned tags. * - * This method accepts a TagNameListRequestDTO that contains the list of tag names - * to find cohort definitions with. + * This method accepts a TagNameListRequestDTO that contains the list of tag names + * to find cohort definitions with. * - * @summary List Cohorts By Tag + * @summary List Cohorts By Tag * @param requestDTO contains a list of tag names * @return the set of cohort definitions that match one of the included tag names. */ + @PostMapping(value = "/byTags", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public List listByTags(TagNameListRequestDTO requestDTO) { + public List listByTags(@RequestBody TagNameListRequestDTO requestDTO) { if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { return Collections.emptyList(); } diff --git a/src/main/java/org/ohdsi/webapi/service/CohortResultsService.java b/src/main/java/org/ohdsi/webapi/service/CohortResultsService.java index 2c8173d7c6..461a8169af 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortResultsService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortResultsService.java @@ -26,9 +26,12 @@ import org.ohdsi.webapi.util.PreparedStatementRenderer; import org.ohdsi.webapi.util.SessionUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; -import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.*; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -45,16 +48,17 @@ /** * REST Services related to retrieving - * cohort analysis (a.k.a Heracles Results) analyses results. + * cohort analysis (a.k.a Heracles Results) analyses results. * More information on the Heracles project * can be found at {@link https://www.ohdsi.org/web/wiki/doku.php?id=documentation:software:heracles}. * The implementation found in WebAPI represents a migration of the functionality * from the stand-alone HERACLES application to integrate it into WebAPI and * ATLAS. - * + * * @summary Cohort Analysis Results (a.k.a Heracles Results) */ -@Component +@RestController +@RequestMapping("/cohortresults") public class CohortResultsService extends AbstractDaoService { public static final String MIN_COVARIATE_PERSON_COUNT = "10"; @@ -93,11 +97,14 @@ public void init() { * @param sourceKey the source to retrieve results * @return List of key, value pairs */ - public List> getCohortResultsRaw(final int id, final String analysisGroup, - final String analysisName, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - String sourceKey) { + @GetMapping(value = "/{sourceKey}/{id}/raw/{analysisGroup}/{analysisName}", produces = MediaType.APPLICATION_JSON_VALUE) + public List> getCohortResultsRaw( + @PathVariable("id") final int id, + @PathVariable("analysisGroup") final String analysisGroup, + @PathVariable("analysisName") final String analysisName, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") String sourceKey) { List> results; String sqlPath = BASE_SQL_PATH + "/" + analysisGroup + "/" + analysisName + ".sql"; @@ -136,13 +143,16 @@ protected PreparedStatementRenderer prepareGetCohortResultsRaw(final int id, /** * Export the cohort analysis results to a ZIP file - * + * * @summary Export cohort analysis results * @param id The cohort ID * @param sourceKey The source Key * @return A response containing the .ZIP file of results */ - public ResponseEntity exportCohortResults(int id, String sourceKey) { + @GetMapping(value = "/{sourceKey}/{id}/export.zip", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity exportCohortResults( + @PathVariable("id") int id, + @PathVariable("sourceKey") String sourceKey) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zos = new ZipOutputStream(baos); @@ -222,37 +232,45 @@ public Void mapRow(ResultSet rs, int arg1) throws SQLException { throw new RuntimeException(ex); } - ResponseEntity response = ResponseEntity.ok() - .contentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM) - .body(baos); + ByteArrayResource resource = new ByteArrayResource(baos.toByteArray()); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentDispositionFormData("attachment", "cohort_" + id + "_export.zip"); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); - return response; + return ResponseEntity.ok() + .headers(headers) + .contentLength(resource.contentLength()) + .body(resource); } /** * Provides a warmup mechanism for the data visualization cache. This - * endpoint does not appear to be used and may be a hold over from the + * endpoint does not appear to be used and may be a hold over from the * original HERACLES implementation - * + * * @summary Warmup data visualizations * @param task The cohort analysis task * @return The number of report visualizations warmed */ - public int warmUpVisualizationData(CohortAnalysisTask task) { + @PostMapping(value = "/warmup", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public int warmUpVisualizationData(@RequestBody CohortAnalysisTask task) { return this.queryRunner.warmupData(this.getSourceJdbcTemplate(task.getSource()), task); } /** * Provides a list of cohort analysis visualizations that are completed - * + * * @summary Get completed cohort analysis visualizations * @param id The cohort ID * @param sourceKey The source key * @return A list of visualization keys that are complete */ - public Collection getCompletedVisualiztion(final int id, - final String sourceKey) { + @GetMapping(value = "/{sourceKey}/{id}/completed", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getCompletedVisualiztion( + @PathVariable("id") final int id, + @PathVariable("sourceKey") final String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); List vizData = this.visualizationDataRepository.findByCohortDefinitionIdAndSourceId(id, source.getSourceId()); Set completed = new HashSet<>(); @@ -267,13 +285,16 @@ public Collection getCompletedVisualiztion(final int id, /** * Retrieves the tornado plot - * + * * @summary Get the tornado plot * @param sourceKey The source key * @param cohortDefinitionId The cohort definition id * @return The tornado plot data */ - public TornadoReport getTornadoReport(final String sourceKey, final int cohortDefinitionId) { + @GetMapping(value = "/{sourceKey}/{id}/tornado", produces = MediaType.APPLICATION_JSON_VALUE) + public TornadoReport getTornadoReport( + @PathVariable("sourceKey") final String sourceKey, + @PathVariable("id") final int cohortDefinitionId) { Source source = getSourceRepository().findBySourceKey(sourceKey); TornadoReport tornadoReport = new TornadoReport(); tornadoReport.tornadoRecords = queryRunner.getTornadoRecords(getSourceJdbcTemplate(source), cohortDefinitionId, source); @@ -291,12 +312,14 @@ public TornadoReport getTornadoReport(final String sourceKey, final int cohortDe * @param demographicsOnly only render gender and age * @return CohortDashboard */ - public CohortDashboard getDashboard(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final boolean demographicsOnly, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/dashboard", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortDashboard getDashboard( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @RequestParam(value = "demographics_only", defaultValue = "false") final boolean demographicsOnly, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { final String key = CohortResultsAnalysisRunner.DASHBOARD; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -323,7 +346,7 @@ public CohortDashboard getDashboard(final int id, /** * Queries for cohort analysis condition treemap results for the given cohort * definition id - * + * * @summary Get condition treemap * @param sourceKey The source key * @param id The cohort ID @@ -332,10 +355,13 @@ public CohortDashboard getDashboard(final int id, * @param refresh Boolean - refresh visualization data * @return List */ - public List getConditionTreemap(String sourceKey, final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/condition/", produces = MediaType.APPLICATION_JSON_VALUE) + public List getConditionTreemap( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.CONDITION; @@ -358,16 +384,18 @@ public List getConditionTreemap(String sourceKey, fin /** * Get the distinct person count for a cohort - * + * * @summary Get distinct person count * @param sourceKey The source key * @param id The cohort ID * @param refresh Boolean - refresh visualization data * @return Distinct person count as integer */ - public Integer getRawDistinctPersonCount(String sourceKey, - String id, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/distinctPersonCount/", produces = MediaType.APPLICATION_JSON_VALUE) + public Integer getRawDistinctPersonCount( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") String id, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetRawDistinctPersonCount(id, source); Integer result = getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), new ResultSetExtractor() { @@ -394,7 +422,8 @@ protected PreparedStatementRenderer prepareGetRawDistinctPersonCount(String id, /** * Queries for cohort analysis condition drilldown results for the given * cohort definition id and condition id - * + * + * @summary Get condition drilldown report * @param sourceKey The source key * @param id The cohort ID * @param conditionId The condition concept ID @@ -403,12 +432,14 @@ protected PreparedStatementRenderer prepareGetRawDistinctPersonCount(String id, * @param refresh Boolean - refresh visualization data * @return The CohortConditionDrilldown detail object */ - public CohortConditionDrilldown getConditionResults(String sourceKey, - final int id, - final int conditionId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/condition/{conditionId}", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortConditionDrilldown getConditionResults( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") final int id, + @PathVariable("conditionId") final int conditionId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortConditionDrilldown drilldown = null; final String key = CohortResultsAnalysisRunner.CONDITION_DRILLDOWN; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -431,7 +462,8 @@ public CohortConditionDrilldown getConditionResults(String sourceKey, /** * Queries for cohort analysis condition era treemap results for the given * cohort definition id - * + * + * @summary Get condition era treemap * @param sourceKey The source key * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -439,11 +471,13 @@ public CohortConditionDrilldown getConditionResults(String sourceKey, * @param refresh Boolean - refresh visualization data * @return List */ - public List getConditionEraTreemap(final String sourceKey, - final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/conditionera/", produces = MediaType.APPLICATION_JSON_VALUE) + public List getConditionEraTreemap( + @PathVariable("sourceKey") final String sourceKey, + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.CONDITION_ERA; @@ -467,13 +501,16 @@ public List getConditionEraTreemap(final String sourc /** * Get the completed analyses IDs for the selected cohort and source key - * + * * @summary Get completed analyses IDs * @param sourceKey The source key * @param id The cohort ID * @return A list of completed analysis IDs */ - public List getCompletedAnalyses(String sourceKey, String id) { + @GetMapping(value = "/{sourceKey}/{id}/analyses", produces = MediaType.APPLICATION_JSON_VALUE) + public List getCompletedAnalyses( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") String id) { Source source = getSourceRepository().findBySourceKey(sourceKey); int sourceId = source.getSourceId(); @@ -530,13 +567,16 @@ public void setProgress(Integer progress) { /** * Get the analysis generation progress - * + * * @summary Get analysis progress * @param sourceKey The source key * @param id The cohort ID * @return The generation progress information */ - public GenerationInfoDTO getAnalysisProgress(String sourceKey, Integer id) { + @GetMapping(value = "/{sourceKey}/{id}/info", produces = MediaType.APPLICATION_JSON_VALUE) + public GenerationInfoDTO getAnalysisProgress( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") Integer id) { return getTransactionTemplateRequiresNew().execute(status -> { org.ohdsi.webapi.cohortdefinition.CohortDefinition def = cohortDefinitionRepository.findById(id).orElse(null); @@ -561,7 +601,7 @@ protected PreparedStatementRenderer prepareGetCompletedAnalysis(String id, int s /** * Queries for cohort analysis condition era drilldown results for the given * cohort definition id and condition id - * + * * @summary Get condition era drilldown report * @param id The cohort ID * @param conditionId The condition ID @@ -571,12 +611,14 @@ protected PreparedStatementRenderer prepareGetCompletedAnalysis(String id, int s * @param refresh Boolean - refresh visualization data * @return The CohortConditionEraDrilldown object */ - public CohortConditionEraDrilldown getConditionEraDrilldown(final int id, - final int conditionId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/conditionera/{conditionId}", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortConditionEraDrilldown getConditionEraDrilldown( + @PathVariable("id") final int id, + @PathVariable("conditionId") final int conditionId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortConditionEraDrilldown drilldown = null; final String key = CohortResultsAnalysisRunner.CONDITION_ERA_DRILLDOWN; @@ -601,7 +643,7 @@ public CohortConditionEraDrilldown getConditionEraDrilldown(final int id, /** * Queries for drug analysis treemap results for the given cohort * definition id - * + * * @summary Get drug treemap * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -610,11 +652,13 @@ public CohortConditionEraDrilldown getConditionEraDrilldown(final int id, * @param refresh Boolean - refresh visualization data * @return List */ - public List getDrugTreemap(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/drug/", produces = MediaType.APPLICATION_JSON_VALUE) + public List getDrugTreemap( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.DRUG; @@ -635,16 +679,10 @@ public List getDrugTreemap(final int id, return res; } - /** - * - * @param id cohort_defintion id - * @param drugId drug_id (from concept) - * @return CohortDrugDrilldown - */ /** * Queries for cohort analysis drug drilldown results for the given cohort * definition id and drug id - * + * * @summary Get drug drilldown report * @param id The cohort ID * @param drugId The drug concept ID @@ -652,13 +690,16 @@ public List getDrugTreemap(final int id, * @param minIntervalPersonCountParam The minimum interval person count * @param sourceKey The source key * @param refresh Boolean - refresh visualization data - * @return + * @return CohortDrugDrilldown */ - public CohortDrugDrilldown getDrugResults(final int id, final int drugId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/drug/{drugId}", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortDrugDrilldown getDrugResults( + @PathVariable("id") final int id, + @PathVariable("drugId") final int drugId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortDrugDrilldown drilldown = null; final String key = CohortResultsAnalysisRunner.DRUG_DRILLDOWN; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -681,7 +722,7 @@ public CohortDrugDrilldown getDrugResults(final int id, final int drugId, /** * Queries for cohort analysis drug era treemap results for the given cohort * definition id - * + * * @summary Get drug era treemap report * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -690,11 +731,13 @@ public CohortDrugDrilldown getDrugResults(final int id, final int drugId, * @param refresh Boolean - refresh visualization data * @return List */ - public List getDrugEraTreemap(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/drugera/", produces = MediaType.APPLICATION_JSON_VALUE) + public List getDrugEraTreemap( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { Source source = getSourceRepository().findBySourceKey(sourceKey); List res = null; @@ -715,16 +758,10 @@ public List getDrugEraTreemap(final int id, return res; } - /** - * - * @param id cohort_defintion id - * @param drugId drug_id (from concept) - * @return CohortDrugEraDrilldown - */ /** * Queries for cohort analysis drug era drilldown results for the given cohort * definition id and drug id - * + * * @summary Get drug era drilldown report * @param id The cohort ID * @param drugId The drug concept ID @@ -734,11 +771,14 @@ public List getDrugEraTreemap(final int id, * @param refresh Boolean - refresh visualization data * @return CohortDrugEraDrilldown */ - public CohortDrugEraDrilldown getDrugEraResults(final int id, final int drugId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/drugera/{drugId}", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortDrugEraDrilldown getDrugEraResults( + @PathVariable("id") final int id, + @PathVariable("drugId") final int drugId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortDrugEraDrilldown drilldown = null; Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.DRUG_ERA_DRILLDOWN; @@ -761,20 +801,22 @@ public CohortDrugEraDrilldown getDrugEraResults(final int id, final int drugId, /** * Queries for cohort analysis person results for the given cohort definition * id - * + * * @summary Get the person report * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person * @param minIntervalPersonCountParam The minimum interval person count * @param sourceKey The source key * @param refresh Boolean - refresh visualization data - * @return + * @return CohortPersonSummary */ - public CohortPersonSummary getPersonResults(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/person", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortPersonSummary getPersonResults( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortPersonSummary person = null; final String key = CohortResultsAnalysisRunner.PERSON; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -796,7 +838,7 @@ public CohortPersonSummary getPersonResults(final int id, /** * Queries for cohort analysis cohort specific results for the given cohort * definition id - * + * * @summary Get cohort specific results * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -805,11 +847,13 @@ public CohortPersonSummary getPersonResults(final int id, * @param refresh Boolean - refresh visualization data * @return CohortSpecificSummary */ - public CohortSpecificSummary getCohortSpecificResults(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/cohortspecific", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortSpecificSummary getCohortSpecificResults( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortSpecificSummary summary = null; Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.COHORT_SPECIFIC; @@ -831,7 +875,7 @@ public CohortSpecificSummary getCohortSpecificResults(final int id, /** * Queries for cohort analysis cohort specific treemap results for the given * cohort definition id - * + * * @summary Get cohort specific treemap * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -840,11 +884,13 @@ public CohortSpecificSummary getCohortSpecificResults(final int id, * @param refresh Boolean - refresh visualization data * @return CohortSpecificTreemap */ - public CohortSpecificTreemap getCohortSpecificTreemapResults(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/cohortspecifictreemap", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortSpecificTreemap getCohortSpecificTreemapResults( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortSpecificTreemap summary = null; final String key = CohortResultsAnalysisRunner.COHORT_SPECIFIC_TREEMAP; @@ -867,7 +913,7 @@ public CohortSpecificTreemap getCohortSpecificTreemapResults(final int id, /** * Queries for cohort analysis procedure drilldown results for the given * cohort definition id and concept id - * + * * @summary Get procedure drilldown report * @param id The cohort ID * @param conceptId The procedure concept ID @@ -877,12 +923,14 @@ public CohortSpecificTreemap getCohortSpecificTreemapResults(final int id, * @param refresh Boolean - refresh visualization data * @return List */ - public List getCohortProcedureDrilldown(final int id, - final int conceptId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/cohortspecificprocedure/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public List getCohortProcedureDrilldown( + @PathVariable("id") final int id, + @PathVariable("conceptId") final int conceptId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { List records = new ArrayList<>(); Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -904,16 +952,10 @@ public List getCohortProcedureDrilldown(final int id, return records; } - /** - * - * @param id cohort_definition id - * @param conceptId conceptId (from concept) - * @return List - */ /** * Queries for cohort analysis drug drilldown results for the given cohort * definition id and concept id - * + * * @summary Get drug drilldown report for specific concept * @param id The cohort ID * @param conceptId The drug concept ID @@ -923,12 +965,14 @@ public List getCohortProcedureDrilldown(final int id, * @param refresh Boolean - refresh visualization data * @return List */ - public List getCohortDrugDrilldown(final int id, - final int conceptId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/cohortspecificdrug/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public List getCohortDrugDrilldown( + @PathVariable("id") final int id, + @PathVariable("conceptId") final int conceptId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { List records = new ArrayList(); Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -951,7 +995,8 @@ public List getCohortDrugDrilldown(final int id, /** * Queries for cohort analysis condition drilldown results for the given * cohort definition id and concept id - * + * + * @summary Get condition drilldown report by concept ID * @param id The cohort ID * @param conceptId The condition concept ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -960,12 +1005,14 @@ public List getCohortDrugDrilldown(final int id, * @param refresh Boolean - refresh visualization data * @return List */ - public List getCohortConditionDrilldown(final int id, - final int conceptId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/cohortspecificcondition/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public List getCohortConditionDrilldown( + @PathVariable("id") final int id, + @PathVariable("conceptId") final int conceptId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { List records = null; @@ -990,7 +1037,7 @@ public List getCohortConditionDrilldown(final int id, /** * Queries for cohort analysis for observation treemap - * + * * @summary Get observation treemap report * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -999,11 +1046,13 @@ public List getCohortConditionDrilldown(final int id, * @param refresh Boolean - refresh visualization data * @return List */ - public List getCohortObservationResults(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/observation", produces = MediaType.APPLICATION_JSON_VALUE) + public List getCohortObservationResults( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { List res = null; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1027,7 +1076,7 @@ public List getCohortObservationResults(final int id, /** * Queries for cohort analysis observation drilldown results for the given * cohort definition id and observation concept id - * + * * @summary Get observation drilldown report for a concept ID * @param id The cohort ID * @param conceptId The observation concept ID @@ -1037,12 +1086,14 @@ public List getCohortObservationResults(final int id, * @param refresh Boolean - refresh visualization data * @return CohortObservationDrilldown */ - public CohortObservationDrilldown getCohortObservationResultsDrilldown(final int id, - final int conceptId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/observation/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortObservationDrilldown getCohortObservationResultsDrilldown( + @PathVariable("id") final int id, + @PathVariable("conceptId") final int conceptId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortObservationDrilldown drilldown = new CohortObservationDrilldown(); final String key = CohortResultsAnalysisRunner.OBSERVATION_DRILLDOWN; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1064,7 +1115,7 @@ public CohortObservationDrilldown getCohortObservationResultsDrilldown(final int /** * Queries for cohort analysis for measurement treemap - * + * * @summary Get measurement treemap report * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -1073,11 +1124,13 @@ public CohortObservationDrilldown getCohortObservationResultsDrilldown(final int * @param refresh Boolean - refresh visualization data * @return List */ - public List getCohortMeasurementResults(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/measurement", produces = MediaType.APPLICATION_JSON_VALUE) + public List getCohortMeasurementResults( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { List res = null; Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.MEASUREMENT; @@ -1097,16 +1150,10 @@ public List getCohortMeasurementResults(final int id, return res; } - /** - * - * @param id cohort_defintion id - * @param conceptId conceptId (from concept) - * @return CohortMeasurementDrilldown - */ /** * Queries for cohort analysis measurement drilldown results for the given * cohort definition id and measurement concept id - * + * * @summary Get measurement drilldown report for concept ID * @param id The cohort ID * @param conceptId The measurement concept ID @@ -1116,11 +1163,14 @@ public List getCohortMeasurementResults(final int id, * @param refresh Boolean - refresh visualization data * @return CohortMeasurementDrilldown */ - public CohortMeasurementDrilldown getCohortMeasurementResultsDrilldown(final int id, final int conceptId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/measurement/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortMeasurementDrilldown getCohortMeasurementResultsDrilldown( + @PathVariable("id") final int id, + @PathVariable("conceptId") final int conceptId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortMeasurementDrilldown drilldown = new CohortMeasurementDrilldown(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.MEASUREMENT_DRILLDOWN; @@ -1142,7 +1192,7 @@ public CohortMeasurementDrilldown getCohortMeasurementResultsDrilldown(final int /** * Queries for cohort analysis observation period for the given cohort * definition id - * + * * @summary Get observation period report * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -1151,11 +1201,13 @@ public CohortMeasurementDrilldown getCohortMeasurementResultsDrilldown(final int * @param refresh Boolean - refresh visualization data * @return CohortObservationPeriod */ - public CohortObservationPeriod getCohortObservationPeriod(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/observationperiod", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortObservationPeriod getCohortObservationPeriod( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortObservationPeriod obsPeriod = new CohortObservationPeriod(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.OBSERVATION_PERIOD; @@ -1176,7 +1228,7 @@ public CohortObservationPeriod getCohortObservationPeriod(final int id, /** * Queries for cohort analysis data density for the given cohort definition id - * + * * @summary Get data density report * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -1185,11 +1237,13 @@ public CohortObservationPeriod getCohortObservationPeriod(final int id, * @param refresh Boolean - refresh visualization data * @return CohortDataDensity */ - public CohortDataDensity getCohortDataDensity(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/datadensity", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortDataDensity getCohortDataDensity( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortDataDensity data = new CohortDataDensity(); Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1212,7 +1266,7 @@ public CohortDataDensity getCohortDataDensity(final int id, /** * Queries for cohort analysis procedure treemap results for the given cohort * definition id - * + * * @summary Get procedure treemap report * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -1221,11 +1275,13 @@ public CohortDataDensity getCohortDataDensity(final int id, * @param refresh Boolean - refresh visualization data * @return List */ - public List getProcedureTreemap(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/procedure/", produces = MediaType.APPLICATION_JSON_VALUE) + public List getProcedureTreemap( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { List res = null; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1249,7 +1305,7 @@ public List getProcedureTreemap(final int id, /** * Queries for cohort analysis procedures for the given cohort definition id * and concept id - * + * * @summary Get procedure drilldown report by concept ID * @param id The cohort ID * @param conceptId The procedure concept ID @@ -1259,12 +1315,14 @@ public List getProcedureTreemap(final int id, * @param refresh Boolean - refresh visualization data * @return CohortProceduresDrillDown */ - public CohortProceduresDrillDown getCohortProceduresDrilldown(final int id, - final int conceptId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/procedure/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortProceduresDrillDown getCohortProceduresDrilldown( + @PathVariable("id") final int id, + @PathVariable("conceptId") final int conceptId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortProceduresDrillDown drilldown = new CohortProceduresDrillDown(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.PROCEDURE_DRILLDOWN; @@ -1286,7 +1344,7 @@ public CohortProceduresDrillDown getCohortProceduresDrilldown(final int id, /** * Queries for cohort analysis visit treemap results for the given cohort * definition id - * + * * @summary Get visit treemap report * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -1295,11 +1353,13 @@ public CohortProceduresDrillDown getCohortProceduresDrilldown(final int id, * @param refresh Boolean - refresh visualization data * @return List */ - public List getVisitTreemap(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/visit/", produces = MediaType.APPLICATION_JSON_VALUE) + public List getVisitTreemap( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { List res = null; Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1323,7 +1383,7 @@ public List getVisitTreemap(final int id, /** * Queries for cohort analysis visits for the given cohort definition id and * concept id - * + * * @summary Get visit drilldown for a visit concept ID * @param id The cohort ID * @param conceptId The visit concept iD @@ -1331,14 +1391,16 @@ public List getVisitTreemap(final int id, * @param minIntervalPersonCountParam The minimum interval person count * @param sourceKey The source key * @param refresh Boolean - refresh visualization data - * @return + * @return CohortVisitsDrilldown */ - public CohortVisitsDrilldown getCohortVisitsDrilldown(final int id, - final int conceptId, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/visit/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortVisitsDrilldown getCohortVisitsDrilldown( + @PathVariable("id") final int id, + @PathVariable("conceptId") final int conceptId, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortVisitsDrilldown drilldown = new CohortVisitsDrilldown(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.VISIT_DRILLDOWN; @@ -1358,14 +1420,16 @@ public CohortVisitsDrilldown getCohortVisitsDrilldown(final int id, /** * Returns the summary for the cohort - * + * * @summary Get cohort summary * @param id The cohort ID * @param sourceKey The source key * @return CohortSummary */ - public CohortSummary getCohortSummaryData(final int id, - String sourceKey) { + @GetMapping(value = "/{sourceKey}/{id}/summarydata", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortSummary getCohortSummaryData( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey) { CohortSummary summary = new CohortSummary(); @@ -1395,7 +1459,7 @@ public CohortSummary getCohortSummaryData(final int id, /** * Queries for cohort analysis death data for the given cohort definition id - * + * * @summary Get death report * @param id The cohort ID * @param minCovariatePersonCountParam The minimum number of covariates per person @@ -1404,11 +1468,13 @@ public CohortSummary getCohortSummaryData(final int id, * @param refresh Boolean - refresh visualization data * @return CohortDeathData */ - public CohortDeathData getCohortDeathData(final int id, - final Integer minCovariatePersonCountParam, - final Integer minIntervalPersonCountParam, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/death", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortDeathData getCohortDeathData( + @PathVariable("id") final int id, + @RequestParam(value = "min_covariate_person_count", required = false) final Integer minCovariatePersonCountParam, + @RequestParam(value = "min_interval_person_count", required = false) final Integer minIntervalPersonCountParam, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { CohortDeathData data = new CohortDeathData(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.DEATH; @@ -1429,12 +1495,16 @@ public CohortDeathData getCohortDeathData(final int id, /** * Returns the summary for the cohort - * + * + * @summary Get cohort summary analyses * @param id The cohort ID * @param sourceKey The source key * @return CohortSummary */ - public CohortSummary getCohortSummaryAnalyses(final int id, String sourceKey) { + @GetMapping(value = "/{sourceKey}/{id}/summaryanalyses", produces = MediaType.APPLICATION_JSON_VALUE) + public CohortSummary getCohortSummaryAnalyses( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey) { CohortSummary summary = new CohortSummary(); try { @@ -1449,13 +1519,16 @@ public CohortSummary getCohortSummaryAnalyses(final int id, String sourceKey) { /** * Returns breakdown with counts about people in cohort - * + * * @summary Get cohort breakdown report * @param id The cohort ID * @param sourceKey The source key * @return Collection */ - public Collection getCohortBreakdown(final int id, String sourceKey) { + @GetMapping(value = "/{sourceKey}/{id}/breakdown", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getCohortBreakdown( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/cohortresults/sql/raw/getCohortBreakdown.sql"; String resultsTqName = "resultsTableQualifier"; @@ -1472,13 +1545,16 @@ public Collection getCohortBreakdown(final int id, String sourc /** * Returns the count of all members of a generated cohort * definition identifier - * + * * @summary Get cohort member count * @param id The cohort ID * @param sourceKey The source key * @return The cohort count */ - public Long getCohortMemberCount(final int id, String sourceKey) { + @GetMapping(value = "/{sourceKey}/{id}/members/count", produces = MediaType.APPLICATION_JSON_VALUE) + public Long getCohortMemberCount( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/cohortresults/sql/raw/getMemberCount.sql"; String tqName = "tableQualifier"; @@ -1490,7 +1566,7 @@ public Long getCohortMemberCount(final int id, String sourceKey) { /** * Returns all cohort analyses in the results/OHDSI schema for the given * cohort_definition_id - * + * * @summary Get the cohort analysis list for a cohort * @param id The cohort ID * @param sourceKey The source key @@ -1498,9 +1574,11 @@ public Long getCohortMemberCount(final int id, String sourceKey) { * @return List of all cohort analyses and their statuses for the given * cohort_defintion_id */ - public List getCohortAnalysesForCohortDefinition(final int id, - String sourceKey, - boolean retrieveFullDetail) { + @GetMapping(value = "/{sourceKey}/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public List getCohortAnalysesForCohortDefinition( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "fullDetail", defaultValue = "true") boolean retrieveFullDetail) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sql; @@ -1518,17 +1596,21 @@ public List getCohortAnalysesForCohortDefinition(final int id, } /** - * Get the exposure cohort incidence rates. This function is not using a + * Get the exposure cohort incidence rates. This function is not using a * proper incidence rate so this should be viewed as informational only * and not as a report - * + * * @summary DO NOT USE * @deprecated * @param sourceKey The source key * @param search The exposure cohort search * @return List */ - public List getExposureOutcomeCohortRates(String sourceKey, ExposureCohortSearch search) { + @PostMapping(value = "/{sourceKey}/exposurecohortrates", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Deprecated + public List getExposureOutcomeCohortRates( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ExposureCohortSearch search) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetExposureOutcomeCohortRates(search, source); @@ -1562,14 +1644,18 @@ protected PreparedStatementRenderer prepareGetExposureOutcomeCohortRates( /** * Provides a time to event calculation but it is unclear how this works. - * + * * @summary DO NOT USE * @deprecated * @param sourceKey The source key * @param search The exposure cohort search * @return List */ - public List getTimeToEventDrilldown(String sourceKey, ExposureCohortSearch search) { + @PostMapping(value = "/{sourceKey}/timetoevent", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Deprecated + public List getTimeToEventDrilldown( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ExposureCohortSearch search) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetTimeToEventDrilldown(search, source); @@ -1601,14 +1687,18 @@ protected PreparedStatementRenderer prepareGetTimeToEventDrilldown( /** * Provides a predictor calculation but it is unclear how this works. - * + * * @summary DO NOT USE * @deprecated * @param sourceKey The source key * @param search The exposure cohort search * @return List */ - public List getExposureOutcomeCohortPredictors(String sourceKey, ExposureCohortSearch search) { + @PostMapping(value = "/{sourceKey}/predictors", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Deprecated + public List getExposureOutcomeCohortPredictors( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ExposureCohortSearch search) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetExposureOutcomeCohortPredictors(search, source); @@ -1629,24 +1719,21 @@ public List getExposureOutcomeCohortPredictors(String sourceKey }); } - /** - * - * @param id cohort definition id - * @return List - */ /** * Returns heracles heel results (data quality issues) for the given cohort * definition id - * + * * @summary Get HERACLES heel report * @param id The cohort iD * @param sourceKey The source key * @param refresh Boolean - refresh visualization data * @return List */ - public List getHeraclesHeel(final int id, - final String sourceKey, - boolean refresh) { + @GetMapping(value = "/{sourceKey}/{id}/heraclesheel", produces = MediaType.APPLICATION_JSON_VALUE) + public List getHeraclesHeel( + @PathVariable("id") final int id, + @PathVariable("sourceKey") final String sourceKey, + @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { List attrs = new ArrayList(); Source source = getSourceRepository().findBySourceKey(sourceKey); final String key = CohortResultsAnalysisRunner.HERACLES_HEEL; @@ -1679,14 +1766,16 @@ public List getCohortAnalysesForDataCompleteness(final int id, /** * Provides a data completeness report for a cohort - * + * * @summary Get data completeness report * @param id The cohort ID * @param sourceKey The source key * @return List */ - public List getDataCompleteness(final int id, - String sourceKey) { + @GetMapping(value = "/{sourceKey}/{id}/datacompleteness", produces = MediaType.APPLICATION_JSON_VALUE) + public List getDataCompleteness( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey) { List arl = this.getCohortAnalysesForDataCompleteness(id, sourceKey); List dcal = new ArrayList<>(); @@ -1768,13 +1857,16 @@ public List getCohortAnalysesEntropy(final int id, String sourc /** * Provide an entropy report for a cohort - * + * * @summary Get entropy report * @param id The cohort ID * @param sourceKey The source key * @return List */ - public List getEntropy(final int id, String sourceKey) { + @GetMapping(value = "/{sourceKey}/{id}/entropy", produces = MediaType.APPLICATION_JSON_VALUE) + public List getEntropy( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey) { List arl = this.getCohortAnalysesEntropy(id, sourceKey, 2031); List el = new ArrayList<>(); @@ -1791,13 +1883,16 @@ public List getEntropy(final int id, String sourceKey) { /** * Provide a full entropy report for a cohort - * + * * @summary Get full entropy report * @param id The cohort ID * @param sourceKey The source key * @return List */ - public List getAllEntropy(final int id, String sourceKey) { + @GetMapping(value = "/{sourceKey}/{id}/allentropy", produces = MediaType.APPLICATION_JSON_VALUE) + public List getAllEntropy( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey) { List arl = this.getCohortAnalysesEntropy(id, sourceKey, 2031); List el = new ArrayList(); @@ -1827,7 +1922,7 @@ public List getAllEntropy(final int id, String sourceKey) { /** * Get the healthcare utilization exposure report for a specific window - * + * * @summary Get healthcare utilization report for selected time window * @param id The cohort ID * @param sourceKey The source key @@ -1835,9 +1930,12 @@ public List getAllEntropy(final int id, String sourceKey) { * @param periodType The period type * @return HealthcareExposureReport */ - public HealthcareExposureReport getHealthcareUtilizationExposureReport(final int id, String sourceKey - , final WindowType window - , final PeriodType periodType) { + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/exposure/{window}", produces = MediaType.APPLICATION_JSON_VALUE) + public HealthcareExposureReport getHealthcareUtilizationExposureReport( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("window") final WindowType window, + @RequestParam(value = "periodType", defaultValue = "ww") final PeriodType periodType) { Source source = getSourceRepository().findBySourceKey(sourceKey); HealthcareExposureReport exposureReport = queryRunner.getHealthcareExposureReport(getSourceJdbcTemplate(source), id, window, periodType, source); return exposureReport; @@ -1845,17 +1943,18 @@ public HealthcareExposureReport getHealthcareUtilizationExposureReport(final int /** * Get the healthcare utilization periods - * + * * @summary Get healthcare utilization periods * @param id The cohort ID * @param sourceKey The source key * @param window The time window * @return A list of the periods */ + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/periods/{window}", produces = MediaType.APPLICATION_JSON_VALUE) public List getHealthcareUtilizationPeriods( - final int id - , final String sourceKey - , final WindowType window) { + @PathVariable("id") final int id, + @PathVariable("sourceKey") final String sourceKey, + @PathVariable("window") final WindowType window) { final Source source = getSourceRepository().findBySourceKey(sourceKey); final List periodTypes = queryRunner.getHealthcarePeriodTypes(getSourceJdbcTemplate(source), id, window, source); return periodTypes; @@ -1864,7 +1963,7 @@ public List getHealthcareUtilizationPeriods( /** * Get the healthcare utilization report by window, visit status, * period type, visit concept, visit type concept and cost type concept. - * + * * @summary Get healthcare utilization visit report * @param id The cohort ID * @param sourceKey The source key @@ -1876,23 +1975,25 @@ public List getHealthcareUtilizationPeriods( * @param costTypeConcept The cost type concept ID * @return HealthcareVisitUtilizationReport */ - public HealthcareVisitUtilizationReport getHealthcareUtilizationVisitReport(final int id - , String sourceKey - , final WindowType window - , final VisitStatType visitStat - , final PeriodType periodType - , final Long visitConcept - , final Long visitTypeConcept - , final Long costTypeConcept) { + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/visit/{window}/{visitStat}", produces = MediaType.APPLICATION_JSON_VALUE) + public HealthcareVisitUtilizationReport getHealthcareUtilizationVisitReport( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("window") final WindowType window, + @PathVariable("visitStat") final VisitStatType visitStat, + @RequestParam(value = "periodType", defaultValue = "ww") final PeriodType periodType, + @RequestParam(value = "visitConcept", required = false) final Long visitConcept, + @RequestParam(value = "visitTypeConcept", required = false) final Long visitTypeConcept, + @RequestParam(value = "costTypeConcept", defaultValue = "31968") final Long costTypeConcept) { Source source = getSourceRepository().findBySourceKey(sourceKey); HealthcareVisitUtilizationReport visitUtilizationReport = queryRunner.getHealthcareVisitReport(getSourceJdbcTemplate(source), id, window, visitStat, periodType, visitConcept, visitTypeConcept, costTypeConcept, source); return visitUtilizationReport; } /** - * Get the healthcare utilization summary report by drug and + * Get the healthcare utilization summary report by drug and * cost type concept - * + * * @summary Get healthcare utilization drug summary report * @param id The cohort ID * @param sourceKey The source key @@ -1901,22 +2002,22 @@ public HealthcareVisitUtilizationReport getHealthcareUtilizationVisitReport(fina * @param costTypeConceptId The cost type concept ID * @return HealthcareDrugUtilizationSummary */ - public HealthcareDrugUtilizationSummary getHealthcareUtilizationDrugSummaryReport(final int id - , String sourceKey - , final WindowType window - , final Long drugTypeConceptId - , final Long costTypeConceptId - - ) { + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/drug/{window}", produces = MediaType.APPLICATION_JSON_VALUE) + public HealthcareDrugUtilizationSummary getHealthcareUtilizationDrugSummaryReport( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("window") final WindowType window, + @RequestParam(value = "drugType", required = false) final Long drugTypeConceptId, + @RequestParam(value = "costType", defaultValue = "31968") final Long costTypeConceptId) { Source source = getSourceRepository().findBySourceKey(sourceKey); HealthcareDrugUtilizationSummary report = queryRunner.getHealthcareDrugUtilizationSummary(getSourceJdbcTemplate(source), id, window, drugTypeConceptId, costTypeConceptId, source); return report; } /** - * Get the healthcare utilization detail report by drug and + * Get the healthcare utilization detail report by drug and * cost type concept - * + * * @summary Get healthcare utilization drug detail report * @param id The cohort ID * @param sourceKey The source key @@ -1927,14 +2028,15 @@ public HealthcareDrugUtilizationSummary getHealthcareUtilizationDrugSummaryRepor * @param costTypeConceptId The cost type concept ID * @return HealthcareDrugUtilizationDetail */ - public HealthcareDrugUtilizationDetail getHealthcareUtilizationDrugDetailReport(final int id - , String sourceKey - , final WindowType window - , final Long drugConceptId - , final PeriodType periodType - , final Long drugTypeConceptId - , final Long costTypeConceptId - ) { + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/drug/{window}/{drugConceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public HealthcareDrugUtilizationDetail getHealthcareUtilizationDrugDetailReport( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("window") final WindowType window, + @PathVariable("drugConceptId") final Long drugConceptId, + @RequestParam(value = "periodType", defaultValue = "ww") final PeriodType periodType, + @RequestParam(value = "drugType", required = false) final Long drugTypeConceptId, + @RequestParam(value = "costType", defaultValue = "31968") final Long costTypeConceptId) { Source source = getSourceRepository().findBySourceKey(sourceKey); HealthcareDrugUtilizationDetail report = queryRunner.getHealthcareDrugUtilizationReport(getSourceJdbcTemplate(source), id, window, drugConceptId, drugTypeConceptId, periodType, costTypeConceptId, source); return report; @@ -1942,17 +2044,18 @@ public HealthcareDrugUtilizationDetail getHealthcareUtilizationDrugDetailReport( /** * Get the drug type concepts for the selected drug concept ID - * + * * @summary Get drug types for healthcare utilization report * @param id The cohort ID * @param sourceKey The source key * @param drugConceptId The drug concept ID * @return A list of concepts of drug types */ - public List getDrugTypes(final int id - , String sourceKey - , final Long drugConceptId) - { + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/drugtypes", produces = MediaType.APPLICATION_JSON_VALUE) + public List getDrugTypes( + @PathVariable("id") final int id, + @PathVariable("sourceKey") String sourceKey, + @RequestParam(value = "drugConceptId", required = false) final Long drugConceptId) { Source source = getSourceRepository().findBySourceKey(sourceKey); return queryRunner.getDrugTypes(getSourceJdbcTemplate(source), id, drugConceptId, source); } diff --git a/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java b/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java index 92a41b148c..7189c08aee 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java @@ -12,17 +12,16 @@ import org.ohdsi.webapi.source.Source; import org.ohdsi.webapi.source.SourceRepository; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.*; -import java.util.Optional; -import org.springframework.http.ResponseEntity; -import org.springframework.web.server.ResponseStatusException; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; -import org.springframework.http.HttpStatus; -@Component +import java.util.*; + +@RestController +@RequestMapping("/cohortsample") public class CohortSampleService { private final CohortDefinitionRepository cohortDefinitionRepository; private final CohortGenerationInfoRepository generationInfoRepository; @@ -49,9 +48,10 @@ public CohortSampleService( * @param sourceKey * @return JSON containing information about cohort samples */ + @GetMapping(value = "/{cohortDefinitionId}/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) public CohortSampleListDTO listCohortSamples( - int cohortDefinitionId, - String sourceKey + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @PathVariable("sourceKey") String sourceKey ) { Source source = getSource(sourceKey); CohortSampleListDTO result = new CohortSampleListDTO(); @@ -77,11 +77,12 @@ public CohortSampleListDTO listCohortSamples( * @param fields * @return personId, gender, age of each person in the cohort sample */ + @GetMapping(value = "/{cohortDefinitionId}/{sourceKey}/{sampleId}", produces = MediaType.APPLICATION_JSON_VALUE) public CohortSampleDTO getCohortSample( - int cohortDefinitionId, - String sourceKey, - Integer sampleId, - String fields + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("sampleId") Integer sampleId, + @RequestParam(defaultValue = "") String fields ) { List returnFields = Arrays.asList(fields.split(",")); boolean withRecordCounts = returnFields.contains("recordCount"); @@ -97,11 +98,12 @@ public CohortSampleDTO getCohortSample( * @param fields * @return A sample of persons from a cohort */ + @PostMapping(value = "/{cohortDefinitionId}/{sourceKey}/{sampleId}/refresh", produces = MediaType.APPLICATION_JSON_VALUE) public CohortSampleDTO refreshCohortSample( - int cohortDefinitionId, - String sourceKey, - Integer sampleId, - String fields + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @PathVariable("sourceKey") String sourceKey, + @PathVariable("sampleId") Integer sampleId, + @RequestParam(defaultValue = "") String fields ) { List returnFields = Arrays.asList(fields.split(",")); boolean withRecordCounts = returnFields.contains("recordCount"); @@ -114,8 +116,9 @@ public CohortSampleDTO refreshCohortSample( * @param cohortDefinitionId * @return true or false */ + @GetMapping(value = "/has-samples/{cohortDefinitionId}", produces = MediaType.APPLICATION_JSON_VALUE) public Map hasSamples( - int cohortDefinitionId + @PathVariable("cohortDefinitionId") int cohortDefinitionId ) { int nSamples = this.samplingService.countSamples(cohortDefinitionId); return Collections.singletonMap("hasSamples", nSamples > 0); @@ -127,9 +130,10 @@ public Map hasSamples( * @param cohortDefinitionId * @return true or false */ - public Map hasSamples( - String sourceKey, - int cohortDefinitionId + @GetMapping(value = "/has-samples/{cohortDefinitionId}/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) + public Map hasSamplesForSource( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("cohortDefinitionId") int cohortDefinitionId ) { Source source = getSource(sourceKey); int nSamples = this.samplingService.countSamples(cohortDefinitionId, source.getId()); @@ -143,10 +147,11 @@ public Map hasSamples( * @param sampleParameters * @return */ + @PostMapping(value = "/{cohortDefinitionId}/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public CohortSampleDTO createCohortSample( - String sourceKey, - int cohortDefinitionId, - SampleParametersDTO sampleParameters + @PathVariable("sourceKey") String sourceKey, + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @RequestBody SampleParametersDTO sampleParameters ) { sampleParameters.validate(); Source source = getSource(sourceKey); @@ -168,17 +173,18 @@ public CohortSampleDTO createCohortSample( * @param sampleId * @return */ - public ResponseEntity deleteCohortSample( - String sourceKey, - int cohortDefinitionId, - int sampleId + @DeleteMapping("/{cohortDefinitionId}/{sourceKey}/{sampleId}") + public ResponseEntity deleteCohortSample( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @PathVariable("sampleId") int sampleId ) { Source source = getSource(sourceKey); if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Cohort definition " + cohortDefinitionId + " does not exist."); } samplingService.deleteSample(cohortDefinitionId, source, sampleId); - return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + return ResponseEntity.noContent().build(); } /** @@ -187,9 +193,10 @@ public ResponseEntity deleteCohortSample( * @param cohortDefinitionId * @return */ - public ResponseEntity deleteCohortSamples( - String sourceKey, - int cohortDefinitionId + @DeleteMapping("/{cohortDefinitionId}/{sourceKey}") + public ResponseEntity deleteCohortSamples( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("cohortDefinitionId") int cohortDefinitionId ) { Source source = getSource(sourceKey); if (cohortDefinitionRepository.findById(cohortDefinitionId).orElse(null) == null) { diff --git a/src/main/java/org/ohdsi/webapi/service/CohortService.java b/src/main/java/org/ohdsi/webapi/service/CohortService.java index a6d28fdc4e..bd766e0d00 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortService.java @@ -7,15 +7,22 @@ import org.ohdsi.webapi.cohort.CohortEntity; import org.ohdsi.webapi.cohort.CohortRepository; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; +import org.springframework.http.MediaType; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; /** * Service to read/write to the Cohort table */ -@Component +@RestController +@RequestMapping("/cohort") public class CohortService { @Autowired @@ -28,13 +35,14 @@ public class CohortService { private EntityManager em; /** - * Retrieves all cohort entities for the given cohort definition id + * Retrieves all cohort entities for the given cohort definition id * from the COHORT table - * + * * @param id Cohort Definition id * @return List of CohortEntity */ - public List getCohortListById(final long id) { + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public List getCohortListById(@PathVariable("id") final long id) { List d = this.cohortRepository.getAllCohortsForId(id); return d; @@ -42,11 +50,12 @@ public List getCohortListById(final long id) { /** * Imports a List of CohortEntity into the COHORT table - * + * * @param cohort List of CohortEntity * @return status */ - public String saveCohortListToCDM(final List cohort) { + @PostMapping(value = "/import", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) + public String saveCohortListToCDM(@RequestBody final List cohort) { this.transactionTemplate.execute(new TransactionCallback() { @Override diff --git a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java index 46b998d230..0641d9e827 100644 --- a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java +++ b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java @@ -20,7 +20,7 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import jakarta.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -74,11 +74,13 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.stereotype.Component; -import org.springframework.http.ResponseEntity; -import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; /** * Provides REST services for working with @@ -86,7 +88,8 @@ * * @summary Concept Set */ -@Component +@RestController +@RequestMapping("/conceptset") @Transactional public class ConceptSetService extends AbstractDaoService implements HasTags { //create cache @@ -158,7 +161,8 @@ public void customize(CacheManager cacheManager) { * @param id The concept set ID * @return The concept set definition */ - public ConceptSetDTO getConceptSet(final int id) { + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ConceptSetDTO getConceptSet(@PathVariable("id") final int id) { ConceptSet conceptSet = getConceptSetRepository().findById(id).orElse(null); ExceptionUtils.throwNotFoundExceptionIfNull(conceptSet, String.format("There is no concept set with id = %d.", id)); return conversionService.convert(conceptSet, ConceptSetDTO.class); @@ -170,8 +174,9 @@ public ConceptSetDTO getConceptSet(final int id) { * @summary Get all concept sets * @return A list of all concept sets in the WebAPI database */ + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) @Cacheable(cacheNames = ConceptSetService.CachingSetup.CONCEPT_SET_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()") - public Collection getConceptSets() { + public Collection getConceptSets() { return getTransactionTemplate().execute( transactionStatus -> StreamSupport.stream(getConceptSetRepository().findAll().spliterator(), false) .filter(!defaultGlobalReadPermissions ? entity -> permissionService.hasReadAccess(entity) : entity -> true) @@ -192,7 +197,8 @@ public Collection getConceptSets() { * @param id The concept set identifier * @return A list of concept set items */ - public Iterable getConceptSetItems(final int id) { + @GetMapping(value = "/{id}/items", produces = MediaType.APPLICATION_JSON_VALUE) + public Iterable getConceptSetItems(@PathVariable("id") final int id) { return getConceptSetItemRepository().findAllByConceptSetId(id); } @@ -204,13 +210,15 @@ public Iterable getConceptSetItems(final int id) { * @param version The version identifier * @return The concept set expression */ - public ConceptSetExpression getConceptSetExpression(final int id, - final int version) { + @GetMapping(value = "/{id}/version/{version}/expression", produces = MediaType.APPLICATION_JSON_VALUE) + public ConceptSetExpression getConceptSetExpressionByVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version) { SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); if (sourceInfo == null) { throw new UnauthorizedException(); } - return getConceptSetExpression(id, version, sourceInfo); + return getConceptSetExpressionInternal(id, version, sourceInfo); } /** @@ -225,14 +233,16 @@ public ConceptSetExpression getConceptSetExpression(final int id, * @param sourceKey The source key * @return The concept set expression for the selected version */ - public ConceptSetExpression getConceptSetExpression(final int id, - final int version, - final String sourceKey) { + @GetMapping(value = "/{id}/version/{version}/expression/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) + public ConceptSetExpression getConceptSetExpressionByVersionAndSource( + @PathVariable("id") final int id, + @PathVariable("version") final int version, + @PathVariable("sourceKey") final String sourceKey) { SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); if (sourceInfo == null) { throw new UnauthorizedException(); } - return getConceptSetExpression(id, version, sourceInfo); + return getConceptSetExpressionInternal(id, version, sourceInfo); } /** @@ -242,12 +252,13 @@ public ConceptSetExpression getConceptSetExpression(final int id, * @param id The concept set identifier * @return The concept set expression */ - public ConceptSetExpression getConceptSetExpression(final int id) { + @GetMapping(value = "/{id}/expression", produces = MediaType.APPLICATION_JSON_VALUE) + public ConceptSetExpression getConceptSetExpressionById(@PathVariable("id") final int id) { SourceInfo sourceInfo = sourceService.getPriorityVocabularySourceInfo(); if (sourceInfo == null) { throw new UnauthorizedException(); } - return getConceptSetExpression(id, null, sourceInfo); + return getConceptSetExpressionInternal(id, null, sourceInfo); } /** @@ -258,14 +269,16 @@ public ConceptSetExpression getConceptSetExpression(final int id) { * @param sourceKey The source key * @return The concept set expression */ - public ConceptSetExpression getConceptSetExpression(final int id, final String sourceKey) { - + @GetMapping(value = "/{id}/expression/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) + public ConceptSetExpression getConceptSetExpressionByIdAndSource( + @PathVariable("id") final int id, + @PathVariable("sourceKey") final String sourceKey) { Source source = sourceService.findBySourceKey(sourceKey); sourceAccessor.checkAccess(source); - return getConceptSetExpression(id, null, source.getSourceInfo()); + return getConceptSetExpressionInternal(id, null, source.getSourceInfo()); } - private ConceptSetExpression getConceptSetExpression(int id, Integer version, SourceInfo sourceInfo) { + private ConceptSetExpression getConceptSetExpressionInternal(int id, Integer version, SourceInfo sourceInfo) { HashMap map = new HashMap<>(); // create our expression to return @@ -332,11 +345,14 @@ private ConceptSetExpression getConceptSetExpression(int id, Integer version, So * @summary DO NOT USE * @deprecated * @param id The concept set ID - * @param sourceKey The source key + * @param name The concept set name * @return The concept set expression */ @Deprecated - public ResponseEntity getConceptSetExistsDeprecated(final int id, String name) { + @GetMapping(value = "/{id}/{name}/exists", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getConceptSetExistsDeprecated( + @PathVariable("id") final int id, + @PathVariable("name") String name) { String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: conceptset/{id}/exists?name={name}"; Collection cs = getConceptSetRepository().conceptSetExists(id, name); return ResponseEntity.ok() @@ -355,7 +371,10 @@ public ResponseEntity getConceptSetExistsDeprecated(final int id, String name) { * @return The count of concept sets with the name, excluding the * specified concept set ID. */ - public int getCountCSetWithSameName(final int id, String name) { + @GetMapping(value = "/{id}/exists", produces = MediaType.APPLICATION_JSON_VALUE) + public int getCountCSetWithSameName( + @PathVariable("id") final int id, + @RequestParam(value = "name", required = false) String name) { return getConceptSetRepository().getCountCSetWithSameName(id, name); } @@ -372,8 +391,11 @@ public int getCountCSetWithSameName(final int id, String name) { * @param items An array of ConceptSetItems * @return Boolean: true if the save is successful */ + @PutMapping(value = "/{id}/items", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public boolean saveConceptSetItems(final int id, ConceptSetItem[] items) { + public boolean saveConceptSetItems( + @PathVariable("id") final int id, + @RequestBody ConceptSetItem[] items) { getConceptSetItemRepository().deleteByConceptSetId(id); for (ConceptSetItem csi : items) { @@ -397,7 +419,9 @@ public boolean saveConceptSetItems(final int id, ConceptSetItem[] items) { * @return * @throws Exception */ - public ResponseEntity exportConceptSetList(final String conceptSetList) throws Exception { + @GetMapping(value = "/exportlist") + public ResponseEntity exportConceptSetList( + @RequestParam("conceptsets") final String conceptSetList) throws Exception { ArrayList conceptSetIds = new ArrayList<>(); try { String[] conceptSetItems = conceptSetList.split("\\+"); @@ -414,7 +438,6 @@ public ResponseEntity exportConceptSetList(final String conceptSetList) throws E ByteArrayOutputStream baos; Source source = sourceService.getPriorityVocabularySource(); ArrayList cs = new ArrayList<>(); - ResponseEntity response = null; try { // Load all of the concept sets requested for (int i = 0; i < conceptSetIds.size(); i++) { @@ -424,16 +447,17 @@ public ResponseEntity exportConceptSetList(final String conceptSetList) throws E // Write Concept Set Expression to a CSV baos = ExportUtil.writeConceptSetExportToCSVAndZip(cs); - response = ResponseEntity - .ok() - .contentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM) - .header("Content-Disposition", "attachment; filename=\"conceptSetExport.zip\"") - .body(baos); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentDispositionFormData("attachment", "conceptSetExport.zip"); + + return ResponseEntity.ok() + .headers(headers) + .body(baos.toByteArray()); } catch (Exception ex) { throw ex; } - return response; } /** @@ -444,7 +468,8 @@ public ResponseEntity exportConceptSetList(final String conceptSetList) throws E * @return A zip file containing the exported concept set * @throws Exception */ - public ResponseEntity exportConceptSetToCSV(final String id) throws Exception { + @GetMapping(value = "/{id}/export") + public ResponseEntity exportConceptSetToCSV(@PathVariable("id") final String id) throws Exception { return this.exportConceptSetList(id); } @@ -455,8 +480,9 @@ public ResponseEntity exportConceptSetToCSV(final String id) throws Exception { * @param conceptSetDTO The concept set to save * @return The concept set saved with the concept set identifier */ + @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public ConceptSetDTO createConceptSet(ConceptSetDTO conceptSetDTO) { + public ConceptSetDTO createConceptSet(@RequestBody ConceptSetDTO conceptSetDTO) { UserEntity user = userRepository.findByLogin(security.getSubject()); ConceptSet conceptSet = conversionService.convert(conceptSetDTO, ConceptSet.class); @@ -464,7 +490,7 @@ public ConceptSetDTO createConceptSet(ConceptSetDTO conceptSetDTO) { updated.setCreatedBy(user); updated.setCreatedDate(new Date()); updated.setTags(null); - updateConceptSet(updated, conceptSet); + updateConceptSetInternal(updated, conceptSet); return conversionService.convert(updated, ConceptSetDTO.class); } @@ -474,12 +500,13 @@ public ConceptSetDTO createConceptSet(ConceptSetDTO conceptSetDTO) { * function is generally used in conjunction with the copy endpoint to * create a unique name and then save a copy of an existing concept set. * - * @sumamry Get concept set name suggestion for copying + * @summary Get concept set name suggestion for copying * @param id The concept set ID * @return A map of the new concept set name and the existing concept set * name */ - public Map getNameForCopy (final int id){ + @GetMapping(value = "/{id}/copy-name", produces = MediaType.APPLICATION_JSON_VALUE) + public Map getNameForCopy(@PathVariable("id") final int id) { ConceptSetDTO source = getConceptSet(id); String name = NameUtils.getNameForCopy(source.getName(), this::getNamesLike, getConceptSetRepository().findByName(source.getName())); return Collections.singletonMap(COPY_NAME, name); @@ -503,9 +530,12 @@ public List getNamesLike(String copyName) { * @return The * @throws Exception */ + @PutMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public ConceptSetDTO updateConceptSet(final int id, ConceptSetDTO conceptSetDTO) throws Exception { + @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) + public ConceptSetDTO updateConceptSet( + @PathVariable("id") final int id, + @RequestBody ConceptSetDTO conceptSetDTO) throws Exception { ConceptSet updated = getConceptSetRepository().findById(id).orElse(null); if (updated == null) { @@ -515,10 +545,10 @@ public ConceptSetDTO updateConceptSet(final int id, ConceptSetDTO conceptSetDTO) saveVersion(id); ConceptSet conceptSet = conversionService.convert(conceptSetDTO, ConceptSet.class); - return conversionService.convert(updateConceptSet(updated, conceptSet), ConceptSetDTO.class); + return conversionService.convert(updateConceptSetInternal(updated, conceptSet), ConceptSetDTO.class); } - private ConceptSet updateConceptSet(ConceptSet dst, ConceptSet src) { + private ConceptSet updateConceptSetInternal(ConceptSet dst, ConceptSet src) { UserEntity user = userRepository.findByLogin(security.getSubject()); dst.setName(src.getName()); @@ -538,7 +568,7 @@ private ConceptSetExport getConceptSetForExport(int conceptSetId, SourceInfo voc // Get the concept set information cs.ConceptSetName = this.getConceptSet(conceptSetId).getName(); // Get the concept set expression - cs.csExpression = this.getConceptSetExpression(conceptSetId); + cs.csExpression = this.getConceptSetExpressionById(conceptSetId); // Lookup the identifiers cs.identifierConcepts = vocabService.executeIncludedConceptLookup(vocabSource.sourceKey, cs.csExpression); //vocabService.executeIdentifierLookup(vocabSource.sourceKey, conceptIds); @@ -560,7 +590,8 @@ private ConceptSetExport getConceptSetForExport(int conceptSetId, SourceInfo voc * @param id The concept set identifier. * @return A collection of concept set generation info objects */ - public Collection getConceptSetGenerationInfo(final int id) { + @GetMapping(value = "/{id}/generationinfo", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getConceptSetGenerationInfo(@PathVariable("id") final int id) { return this.conceptSetGenerationInfoRepository.findAllByConceptSetId(id); } @@ -570,9 +601,10 @@ public Collection getConceptSetGenerationInfo(final in * @summary Delete concept set * @param id The concept set ID */ - @Transactional(rollbackOn = Exception.class, dontRollbackOn = EmptyResultDataAccessException.class) - @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public void deleteConceptSet(final int id) { + @DeleteMapping(value = "/{id}") + @Transactional(rollbackFor = Exception.class, noRollbackFor = EmptyResultDataAccessException.class) + @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) + public void deleteConceptSet(@PathVariable("id") final int id) { // Remove any generation info try { this.conceptSetGenerationInfoRepository.deleteByConceptSetId(id); @@ -615,9 +647,13 @@ public void deleteConceptSet(final int id) { * @param id The concept set ID * @param tagId The tag ID */ + @PostMapping(value = "/{id}/tag/", consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public void assignTag(final Integer id, final int tagId) { + @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) + @Override + public void assignTag( + @PathVariable("id") final Integer id, + @RequestBody int tagId) { ConceptSet entity = getConceptSetRepository().findById(id).orElse(null); assignTag(entity, tagId); } @@ -630,9 +666,13 @@ public void assignTag(final Integer id, final int tagId) { * @param id The concept set ID * @param tagId The tag ID */ + @DeleteMapping(value = "/{id}/tag/{tagId}") @Transactional - @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public void unassignTag(final Integer id, final int tagId) { + @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) + @Override + public void unassignTag( + @PathVariable("id") final Integer id, + @PathVariable("tagId") final int tagId) { ConceptSet entity = getConceptSetRepository().findById(id).orElse(null); unassignTag(entity, tagId); } @@ -645,8 +685,11 @@ public void unassignTag(final Integer id, final int tagId) { * @param id The concept set ID * @param tagId The tag ID */ + @PostMapping(value = "/{id}/protectedtag/", consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public void assignPermissionProtectedTag(final int id, final int tagId) { + public void assignPermissionProtectedTag( + @PathVariable("id") final int id, + @RequestBody final int tagId) { assignTag(id, tagId); } @@ -658,9 +701,12 @@ public void assignPermissionProtectedTag(final int id, final int tagId) { * @param id The concept set ID * @param tagId The tag ID */ + @DeleteMapping(value = "/{id}/protectedtag/{tagId}") @Transactional - @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public void unassignPermissionProtectedTag(final int id, final int tagId) { + @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) + public void unassignPermissionProtectedTag( + @PathVariable("id") final int id, + @PathVariable("tagId") final int tagId) { unassignTag(id, tagId); } @@ -674,8 +720,9 @@ public void unassignPermissionProtectedTag(final int id, final int tagId) { * @param conceptSetDTO The concept set * @return A check result */ + @PostMapping(value = "/check", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public CheckResult runDiagnostics(ConceptSetDTO conceptSetDTO) { + public CheckResult runDiagnostics(@RequestBody ConceptSetDTO conceptSetDTO) { return new CheckResult(checker.check(conceptSetDTO)); } @@ -687,8 +734,9 @@ public CheckResult runDiagnostics(ConceptSetDTO conceptSetDTO) { * @param id The concept set ID * @return A list of version information */ + @GetMapping(value = "/{id}/version/", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public List getVersions(final int id) { + public List getVersions(@PathVariable("id") final int id) { List versions = versionService.getVersions(VersionType.CONCEPT_SET, id); return versions.stream() .map(v -> conversionService.convert(v, VersionDTO.class)) @@ -704,8 +752,11 @@ public List getVersions(final int id) { * @param version The version ID * @return The concept set for the selected version */ + @GetMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public ConceptSetVersionFullDTO getVersion(final int id, final int version) { + public ConceptSetVersionFullDTO getVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version) { checkVersion(id, version, false); ConceptSetVersion conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version); @@ -722,9 +773,12 @@ public ConceptSetVersionFullDTO getVersion(final int id, final int version) { * @param updateDTO The version update * @return The version information */ + @PutMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public VersionDTO updateVersion(final int id, final int version, - VersionUpdateDTO updateDTO) { + public VersionDTO updateVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version, + @RequestBody VersionUpdateDTO updateDTO) { checkVersion(id, version); updateDTO.setAssetId(id); updateDTO.setVersion(version); @@ -739,10 +793,13 @@ public VersionDTO updateVersion(final int id, final int version, * @summary Delete a concept set version * @since v2.10.0 * @param id The concept ID - * @param version THe version ID + * @param version The version ID */ + @DeleteMapping(value = "/{id}/version/{version}") @Transactional - public void deleteVersion(final int id, final int version) { + public void deleteVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version) { checkVersion(id, version); versionService.delete(VersionType.CONCEPT_SET, id, version); } @@ -757,9 +814,12 @@ public void deleteVersion(final int id, final int version) { * @param version The version ID * @return The concept set copy */ + @PutMapping(value = "/{id}/version/{version}/createAsset", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public ConceptSetDTO copyAssetFromVersion(final int id, final int version) { + @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) + public ConceptSetDTO copyAssetFromVersion( + @PathVariable("id") final int id, + @PathVariable("version") final int version) { checkVersion(id, version, false); ConceptSetVersion conceptSetVersion = versionService.getById(VersionType.CONCEPT_SET, id, version); @@ -782,7 +842,8 @@ public ConceptSetDTO copyAssetFromVersion(final int id, final int version) { * @param requestDTO The tagNameListRequest * @return A list of concept sets with their assigned tags */ - public List listByTags(TagNameListRequestDTO requestDTO) { + @PostMapping(value = "/byTags", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public List listByTags(@RequestBody TagNameListRequestDTO requestDTO) { if (requestDTO == null || requestDTO.getNames() == null || requestDTO.getNames().isEmpty()) { return Collections.emptyList(); } @@ -830,8 +891,11 @@ private ConceptSetVersion saveVersion(int id) { * @return Boolean: true if the save is successful * @summary Create new or delete concept set annotation items */ + @PutMapping(value = "/{id}/annotation", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public boolean saveConceptSetAnnotation(final int conceptSetId, SaveConceptSetAnnotationsRequest request) { + public boolean saveConceptSetAnnotation( + @PathVariable("id") final int conceptSetId, + @RequestBody SaveConceptSetAnnotationsRequest request) { removeAnnotations(conceptSetId, request); if (request.getNewAnnotation() != null && !request.getNewAnnotation().isEmpty()) { List annotationList = request.getNewAnnotation() @@ -869,8 +933,9 @@ private void removeAnnotations(int id, SaveConceptSetAnnotationsRequest request) } } } + @PostMapping(value = "/copy-annotations", consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public void copyAnnotations(CopyAnnotationsRequest copyAnnotationsRequest ) { + public void copyAnnotations(@RequestBody CopyAnnotationsRequest copyAnnotationsRequest) { List sourceAnnotations = getConceptSetAnnotationRepository().findByConceptSetId(copyAnnotationsRequest.getSourceConceptSetId()); List copiedAnnotations= sourceAnnotations.stream() .map(sourceAnnotation -> copyAnnotation(sourceAnnotation, copyAnnotationsRequest.getSourceConceptSetId(), copyAnnotationsRequest.getTargetConceptSetId())) @@ -897,7 +962,8 @@ private String appendCopiedFromConceptSetId(String copiedFromConceptSetIds, int } return copiedFromConceptSetIds.concat(",").concat(Integer.toString(sourceConceptSetId)); } - public List getConceptSetAnnotation(final int id) { + @GetMapping(value = "/{id}/annotation", produces = MediaType.APPLICATION_JSON_VALUE) + public List getConceptSetAnnotation(@PathVariable("id") final int id) { List annotationList = getConceptSetAnnotationRepository().findByConceptSetId(id); return annotationList.stream() .map(this::convertAnnotationEntityToDTO) @@ -930,11 +996,16 @@ private AnnotationDTO convertAnnotationEntityToDTO(ConceptSetAnnotation conceptS annotationDTO.setCreatedDate(conceptSetAnnotation.getCreatedDate() != null ? conceptSetAnnotation.getCreatedDate().toString() : null); return annotationDTO; } - public ResponseEntity deleteConceptSetAnnotation(final int conceptSetId, final int annotationId) { + @DeleteMapping(value = "/{conceptSetId}/annotation/{annotationId}") + public ResponseEntity deleteConceptSetAnnotation( + @PathVariable("conceptSetId") final int conceptSetId, + @PathVariable("annotationId") final int annotationId) { ConceptSetAnnotation conceptSetAnnotation = getConceptSetAnnotationRepository().findById(annotationId); if (conceptSetAnnotation != null) { getConceptSetAnnotationRepository().deleteById(annotationId); return ResponseEntity.ok().build(); - } else throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Concept set annotation not found"); + } else { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Concept set annotation not found"); + } } } \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/service/DDLService.java b/src/main/java/org/ohdsi/webapi/service/DDLService.java index 0927c92dd1..85c51b4674 100644 --- a/src/main/java/org/ohdsi/webapi/service/DDLService.java +++ b/src/main/java/org/ohdsi/webapi/service/DDLService.java @@ -32,10 +32,14 @@ import org.ohdsi.circe.helper.ResourceHelper; import org.ohdsi.webapi.sqlrender.SourceStatement; import org.ohdsi.webapi.sqlrender.TranslatedStatement; -import org.ohdsi.webapi.util.SessionUtils; -import org.springframework.stereotype.Component; - -@Component +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/ddl") public class DDLService { public static final String VOCAB_SCHEMA = "vocab_schema"; @@ -126,12 +130,13 @@ public class DDLService { * @param tempSchema * @return SQL to create tables in results schema */ + @GetMapping(value = "/results", produces = MediaType.TEXT_PLAIN_VALUE) public String generateResultSQL( - String dialect, - String vocabSchema, - String resultSchema, - Boolean initConceptHierarchy, - String tempSchema) { + @RequestParam(value = "dialect", required = false) String dialect, + @RequestParam(value = "vocabSchema", defaultValue = "vocab") String vocabSchema, + @RequestParam(value = "schema", defaultValue = "results") String resultSchema, + @RequestParam(value = "initConceptHierarchy", defaultValue = "true") Boolean initConceptHierarchy, + @RequestParam(value = "tempSchema", required = false) String tempSchema) { Collection resultDDLFilePaths = new ArrayList<>(RESULT_DDL_FILE_PATHS); @@ -162,7 +167,10 @@ private Collection getResultInitFilePaths(String dialect) { * @param schema schema name * @return SQL */ - public String generateCemResultSQL(String dialect, String schema) { + @GetMapping(value = "/cemresults", produces = MediaType.TEXT_PLAIN_VALUE) + public String generateCemResultSQL( + @RequestParam(value = "dialect", required = false) String dialect, + @RequestParam(value = "schema", defaultValue = "cemresults") String schema) { Map params = new HashMap() {{ put(CEM_SCHEMA, schema); @@ -178,10 +186,11 @@ public String generateCemResultSQL(String dialect, String schema) { * @param resultSchema results schema * @return SQL */ + @GetMapping(value = "/achilles", produces = MediaType.TEXT_PLAIN_VALUE) public String generateAchillesSQL( - String dialect, - String vocabSchema, - String resultSchema) { + @RequestParam(value = "dialect", required = false) String dialect, + @RequestParam(value = "vocabSchema", defaultValue = "vocab") String vocabSchema, + @RequestParam(value = "schema", defaultValue = "results") String resultSchema) { final Collection achillesDDLFilePaths = new ArrayList<>(ACHILLES_DDL_FILE_PATHS); diff --git a/src/main/java/org/ohdsi/webapi/service/EvidenceService.java b/src/main/java/org/ohdsi/webapi/service/EvidenceService.java index fe3561b8ec..afa1264412 100644 --- a/src/main/java/org/ohdsi/webapi/service/EvidenceService.java +++ b/src/main/java/org/ohdsi/webapi/service/EvidenceService.java @@ -55,10 +55,12 @@ import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; -import org.springframework.stereotype.Component; -import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; /** * Provides REST services for querying the Common Evidence Model @@ -66,7 +68,8 @@ * @summary REST services for querying the Common Evidence Model See * https://github.com/OHDSI/CommonEvidenceModel */ -@Component +@RestController +@RequestMapping("/evidence") public class EvidenceService extends AbstractDaoService implements GeneratesNotification { private static final String NAME = "negativeControlsAnalysisJob"; @@ -142,7 +145,8 @@ public String getSourceIds() { * @param cohortId The cohort Id * @return A list of studies related to the cohort */ - public Collection getCohortStudyMapping(int cohortId) { + @GetMapping(value = "/study/{cohortId}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getCohortStudyMapping(@PathVariable("cohortId") int cohortId) { return cohortStudyMappingRepository.findByCohortDefinitionId(cohortId); } @@ -155,7 +159,8 @@ public Collection getCohortStudyMapping(int cohortId) { * @param conceptId The concept Id of interest * @return A list of cohorts for the specified conceptId */ - public Collection getConceptCohortMapping(int conceptId) { + @GetMapping(value = "/mapping/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getConceptCohortMapping(@PathVariable("conceptId") int conceptId) { return mappingRepository.findByConceptId(conceptId); } @@ -170,7 +175,8 @@ public Collection getConceptCohortMapping(int conceptId) { * @param conceptId The conceptId of interest * @return A list of concepts based on the conceptId of interest */ - public Collection getConceptOfInterest(int conceptId) { + @GetMapping(value = "/conceptofinterest/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getConceptOfInterest(@PathVariable("conceptId") int conceptId) { return conceptOfInterestMappingRepository.findAllByConceptId(conceptId); } @@ -186,7 +192,8 @@ public Collection getConceptOfInterest(int conceptId) * @param setid The drug label setId * @return The set of drug labels that match the setId specified. */ - public Collection getDrugLabel(String setid) { + @GetMapping(value = "/label/{setid}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getDrugLabel(@PathVariable("setid") String setid) { return drugLabelRepository.findAllBySetid(setid); } @@ -199,7 +206,8 @@ public Collection getDrugLabel(String setid) { * @param searchTerm The search term * @return A list of drug labels matching the search term */ - public Collection searchDrugLabels(String searchTerm) { + @GetMapping(value = "/labelsearch/{searchTerm}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection searchDrugLabels(@PathVariable("searchTerm") String searchTerm) { return drugLabelRepository.searchNameContainsTerm(searchTerm); } @@ -211,7 +219,8 @@ public Collection searchDrugLabels(String searchTerm) { * @param sourceKey The source key containing the CEM daimon * @return A collection of evidence information stored in CEM */ - public Collection getInfo(String sourceKey) { + @GetMapping(value = "/{sourceKey}/info", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getInfo(@PathVariable("sourceKey") String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/evidence/sql/getInfo.sql"; String tqName = "cem_schema"; @@ -241,7 +250,8 @@ public Collection getInfo(String sourceKey) { * @param searchParams * @return */ - public Collection getDrugConditionPairs(String sourceKey, DrugConditionSourceSearchParams searchParams) { + @PostMapping(value = "/{sourceKey}/drugconditionpairs", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getDrugConditionPairs(@PathVariable("sourceKey") String sourceKey, @RequestBody DrugConditionSourceSearchParams searchParams) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sql = getDrugHoiEvidenceSQL(source, searchParams); return getSourceJdbcTemplate(source).query(sql, (rs, rowNum) -> { @@ -274,7 +284,8 @@ public Collection getDrugConditionPairs(String sourceKey, DrugC * @param id - An RxNorm Drug Concept Id * @return A list of evidence */ - public Collection getDrugEvidence(String sourceKey, final Long id) { + @GetMapping(value = "/{sourceKey}/drug/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getDrugEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); return getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { @@ -310,7 +321,8 @@ public Collection getDrugEvidence(String sourceKey, final Long id) * @param id The conceptId for the health outcome of interest * @return A list of evidence */ - public Collection getHoiEvidence(String sourceKey, final Long id) { + @GetMapping(value = "/{sourceKey}/hoi/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getHoiEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); return getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { @@ -346,7 +358,8 @@ public Collection getHoiEvidence(String sourceKey, final Long id) { * @param identifiers The list of RxNorm Ingredients concepts or ancestors * @return A list of evidence for the drug and HOI */ - public Collection getDrugIngredientLabel(String sourceKey, long[] identifiers) { + @PostMapping(value = "/{sourceKey}/druglabel", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getDrugIngredientLabel(@PathVariable("sourceKey") String sourceKey, @RequestBody long[] identifiers) { Source source = getSourceRepository().findBySourceKey(sourceKey); return executeGetDrugLabels(identifiers, source); } @@ -360,7 +373,8 @@ public Collection getDrugIngredientLabel(String sourceKey, long[] * @param key The key must be structured as {drugConceptId}-{hoiConceptId} * @return A list of evidence for the drug and HOI */ - public List getDrugHoiEvidence(String sourceKey, final String key) { + @GetMapping(value = "/{sourceKey}/drughoi/{key}", produces = MediaType.APPLICATION_JSON_VALUE) + public List getDrugHoiEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("key") final String key) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetDrugHoiEvidence(key, source); return getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { @@ -402,10 +416,13 @@ public List getDrugHoiEvidence(String sourceKey, final String k * drug, branded drug) * @return A list of evidence rolled up */ - public ResponseEntity getDrugRollupIngredientEvidence(String sourceKey, final Long id, final String filter) { + @GetMapping(value = "/{sourceKey}/drugrollup/{filter}/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getDrugRollupIngredientEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id, @PathVariable("filter") final String filter) { String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; ArrayList evidence = new ArrayList<>(); - return ResponseEntity.ok().header("Warning: 299", warningMessage).body(evidence); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(evidence); } /** @@ -417,7 +434,8 @@ public ResponseEntity getDrugRollupIngredientEvidence(String sourceKey, final Lo * @param id The conceptId of interest * @return A list of evidence matching the conceptId of interest */ - public Collection getEvidence(String sourceKey, final Long id) { + @GetMapping(value = "/{sourceKey}/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getEvidence(@PathVariable("sourceKey") String sourceKey, @PathVariable("id") final Long id) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetEvidenceForConcept(source, id); return getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), (rs, rowNum) -> { @@ -459,10 +477,13 @@ public Collection getEvidence(String sourceKey, final Long id) { * @param evidenceGroup The evidence group * @return A summary of evidence */ - public ResponseEntity getEvidenceSummaryBySource(String sourceKey, String conditionID, String drugID, String evidenceGroup) { + @GetMapping(value = "/{sourceKey}/evidencesummary", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getEvidenceSummaryBySource(@PathVariable("sourceKey") String sourceKey, @RequestParam(required = false) String conditionID, @RequestParam(required = false) String drugID, @RequestParam(required = false) String evidenceGroup) { String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; ArrayList evidenceSummary = new ArrayList<>(); - return ResponseEntity.ok().header("Warning: 299", warningMessage).body(evidenceSummary); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(evidenceSummary); } /** @@ -478,14 +499,17 @@ public ResponseEntity getEvidenceSummaryBySource(String sourceKey, String condit * @throws org.codehaus.jettison.json.JSONException * @throws java.io.IOException */ - public ResponseEntity getEvidenceDetails(String sourceKey, - String conditionID, - String drugID, - String evidenceType) + @GetMapping(value = "/{sourceKey}/evidencedetails", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getEvidenceDetails(@PathVariable("sourceKey") String sourceKey, + @RequestParam(required = false) String conditionID, + @RequestParam(required = false) String drugID, + @RequestParam(required = false) String evidenceType) throws JSONException, IOException { String warningMessage = "This method will be deprecated in the next release. Instead, please use the new REST endpoint: evidence/{sourceKey}/drug/{id}"; ArrayList evidenceDetails = new ArrayList<>(); - return ResponseEntity.ok().header("Warning: 299", warningMessage).body(evidenceDetails); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(evidenceDetails); } /** @@ -499,10 +523,13 @@ public ResponseEntity getEvidenceDetails(String sourceKey, * @throws JSONException * @throws IOException */ - public ResponseEntity getSpontaneousReports(String sourceKey, EvidenceSearch search) throws JSONException, IOException { + @PostMapping(value = "/{sourceKey}/spontaneousreports", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getSpontaneousReports(@PathVariable("sourceKey") String sourceKey, @RequestBody EvidenceSearch search) throws JSONException, IOException { String warningMessage = "This method will be deprecated in the next release."; ArrayList returnVal = new ArrayList<>(); - return ResponseEntity.ok().header("Warning: 299", warningMessage).body(returnVal); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(returnVal); } /** @@ -516,10 +543,13 @@ public ResponseEntity getSpontaneousReports(String sourceKey, EvidenceSearch sea * @throws JSONException * @throws IOException */ - public ResponseEntity evidenceSearch(String sourceKey, EvidenceSearch search) throws JSONException, IOException { + @PostMapping(value = "/{sourceKey}/evidencesearch", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> evidenceSearch(@PathVariable("sourceKey") String sourceKey, @RequestBody EvidenceSearch search) throws JSONException, IOException { String warningMessage = "This method will be deprecated in the next release."; ArrayList returnVal = new ArrayList<>(); - return ResponseEntity.ok().header("Warning: 299", warningMessage).body(returnVal); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(returnVal); } /** @@ -533,10 +563,13 @@ public ResponseEntity evidenceSearch(String sourceKey, EvidenceSearch search) th * @throws JSONException * @throws IOException */ - public ResponseEntity labelEvidence(String sourceKey, EvidenceSearch search) throws JSONException, IOException { + @PostMapping(value = "/{sourceKey}/labelevidence", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> labelEvidence(@PathVariable("sourceKey") String sourceKey, @RequestBody EvidenceSearch search) throws JSONException, IOException { String warningMessage = "This method will be deprecated in the next release."; ArrayList returnVal = new ArrayList<>(); - return ResponseEntity.ok().header("Warning: 299", warningMessage).body(returnVal); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(returnVal); } /** @@ -549,7 +582,8 @@ public ResponseEntity labelEvidence(String sourceKey, EvidenceSearch search) thr * @return information about the negative control job * @throws Exception */ - public JobExecutionResource queueNegativeControlsJob(String sourceKey, NegativeControlTaskParameters task) throws Exception { + @PostMapping(value = "/{sourceKey}/negativecontrols", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public JobExecutionResource queueNegativeControlsJob(@PathVariable("sourceKey") String sourceKey, @RequestBody NegativeControlTaskParameters task) throws Exception { if (task == null) { return null; } @@ -610,7 +644,7 @@ public JobExecutionResource queueNegativeControlsJob(String sourceKey, NegativeC String csSQL = ""; if (task.getCsToInclude() > 0) { try { - csExpression = conceptSetService.getConceptSetExpression(task.getCsToInclude()); + csExpression = conceptSetService.getConceptSetExpressionById(task.getCsToInclude()); csSQL = csBuilder.buildExpressionQuery(csExpression); } catch (Exception e) { log.warn("Failed to build Inclusion expression query", e); @@ -620,7 +654,7 @@ public JobExecutionResource queueNegativeControlsJob(String sourceKey, NegativeC csSQL = ""; if (task.getCsToExclude() > 0) { try { - csExpression = conceptSetService.getConceptSetExpression(task.getCsToExclude()); + csExpression = conceptSetService.getConceptSetExpressionById(task.getCsToExclude()); csSQL = csBuilder.buildExpressionQuery(csExpression); } catch (Exception e) { log.warn("Failed to build Exclusion expression query", e); @@ -646,7 +680,8 @@ public JobExecutionResource queueNegativeControlsJob(String sourceKey, NegativeC * @param conceptSetId The concept set id * @return The list of negative controls */ - public Collection getNegativeControls(String sourceKey, int conceptSetId) throws Exception { + @GetMapping(value = "/{sourceKey}/negativecontrols/{conceptsetid}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getNegativeControls(@PathVariable("sourceKey") String sourceKey, @PathVariable("conceptsetid") int conceptSetId) throws Exception { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = this.prepareGetNegativeControls(source, conceptSetId); final List recs = getSourceJdbcTemplate(source).query(psr.getSql(), psr.getSetter(), new NegativeControlMapper()); @@ -660,10 +695,11 @@ public Collection getNegativeControls(String sourceKey, int * @param sourceKey The source key of the CEM daimon * @return The list of negative controls */ - public String getNegativeControlsSqlStatement(String sourceKey, - String conceptDomain, - String targetDomain, - String conceptOfInterest) { + @GetMapping(value = "/{sourceKey}/negativecontrols/sql", produces = MediaType.TEXT_PLAIN_VALUE) + public String getNegativeControlsSqlStatement(@PathVariable("sourceKey") String sourceKey, + @RequestParam(defaultValue = "CONDITION") String conceptDomain, + @RequestParam(defaultValue = "DRUG") String targetDomain, + @RequestParam(defaultValue = "192671") String conceptOfInterest) { NegativeControlTaskParameters task = new NegativeControlTaskParameters(); Source source = getSourceRepository().findBySourceKey(sourceKey); task.setSource(source); diff --git a/src/main/java/org/ohdsi/webapi/service/FeasibilityService.java b/src/main/java/org/ohdsi/webapi/service/FeasibilityService.java index f81e260c5c..29c67ccea0 100644 --- a/src/main/java/org/ohdsi/webapi/service/FeasibilityService.java +++ b/src/main/java/org/ohdsi/webapi/service/FeasibilityService.java @@ -75,24 +75,26 @@ import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.jdbc.core.RowMapper; -import org.springframework.stereotype.Component; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.web.bind.annotation.*; import java.util.Optional; /** - * REST Services related to performing a feasibility analysis but + * REST Services related to performing a feasibility analysis but * the implementation appears to be subsumed by the cohort definition * services. Marking the REST methods of this * class as deprecated. - * + * * @summary Feasibility analysis (DO NOT USE) */ -@Component +@RestController +@RequestMapping("/feasibility") public class FeasibilityService extends AbstractDaoService { @Autowired @@ -373,6 +375,8 @@ public FeasibilityStudyDTO feasibilityStudyToDTO(FeasibilityStudy study) { * @deprecated * @return List */ + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @Deprecated public List getFeasibilityStudyList() { return getTransactionTemplate().execute(transactionStatus -> { @@ -401,8 +405,13 @@ public List getFeasibilityStudyList * @param study The feasibility study * @return Feasibility study */ + @PutMapping( + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) @Transactional - public FeasibilityService.FeasibilityStudyDTO createStudy(FeasibilityService.FeasibilityStudyDTO study) { + @Deprecated + public FeasibilityService.FeasibilityStudyDTO createStudy(@RequestBody FeasibilityService.FeasibilityStudyDTO study) { return getTransactionTemplate().execute(transactionStatus -> { Date currentTime = Calendar.getInstance().getTime(); @@ -455,14 +464,20 @@ public FeasibilityService.FeasibilityStudyDTO createStudy(FeasibilityService.Fea /** * Get the feasibility study by ID - * + * * @summary DO NOT USE * @deprecated * @param id The study ID * @return Feasibility study */ + @GetMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) @Transactional(readOnly = true) - public FeasibilityService.FeasibilityStudyDTO getStudy(final int id) { + @Deprecated + public FeasibilityService.FeasibilityStudyDTO getStudy(@PathVariable("id") final int id) { return getTransactionTemplate().execute(transactionStatus -> { FeasibilityStudy s = this.feasibilityStudyRepository.findOneWithDetail(id); @@ -472,15 +487,20 @@ public FeasibilityService.FeasibilityStudyDTO getStudy(final int id) { /** * Update the feasibility study - * + * * @summary DO NOT USE * @deprecated * @param id The study ID * @param study The study information * @return The updated study information */ + @PutMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE + ) @Transactional - public FeasibilityService.FeasibilityStudyDTO saveStudy(final int id, FeasibilityStudyDTO study) { + @Deprecated + public FeasibilityService.FeasibilityStudyDTO saveStudy(@PathVariable("id") final int id, @RequestBody FeasibilityStudyDTO study) { Date currentTime = Calendar.getInstance().getTime(); UserEntity user = userRepository.findByLogin(security.getSubject()); @@ -532,14 +552,20 @@ public FeasibilityService.FeasibilityStudyDTO saveStudy(final int id, Feasibilit /** * Generate the feasibility study - * + * * @summary DO NOT USE * @deprecated * @param study_id The study ID * @param sourceKey The source key * @return JobExecutionResource */ - public JobExecutionResource performStudy(final int study_id, final String sourceKey) { + @GetMapping( + value = "/{study_id}/generate/{sourceKey}", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + @Deprecated + public JobExecutionResource performStudy(@PathVariable("study_id") final int study_id, @PathVariable("sourceKey") final String sourceKey) { Date startTime = Calendar.getInstance().getTime(); Source source = this.getSourceRepository().findBySourceKey(sourceKey); @@ -636,14 +662,19 @@ public JobExecutionResource performStudy(final int study_id, final String source /** * Get simulation information - * + * * @summary DO NOT USE * @deprecated * @param id The study ID * @return List */ + @GetMapping( + value = "/{id}/info", + produces = MediaType.APPLICATION_JSON_VALUE + ) @Transactional(readOnly = true) - public List getSimulationInfo(final int id) { + @Deprecated + public List getSimulationInfo(@PathVariable("id") final int id) { FeasibilityStudy study = this.feasibilityStudyRepository.findById(id).orElse(null); List result = new ArrayList<>(); @@ -658,15 +689,20 @@ public List getSimulationInfo(final int id) { /** * Get simulation report - * + * * @summary DO NOT USE * @deprecated * @param id The study ID * @param sourceKey The source key * @return FeasibilityReport */ + @GetMapping( + value = "/{id}/report/{sourceKey}", + produces = MediaType.APPLICATION_JSON_VALUE + ) @Transactional - public FeasibilityReport getSimulationReport(final int id, final String sourceKey) { + @Deprecated + public FeasibilityReport getSimulationReport(@PathVariable("id") final int id, @PathVariable("sourceKey") final String sourceKey) { Source source = this.getSourceRepository().findBySourceKey(sourceKey); @@ -690,8 +726,13 @@ public FeasibilityReport getSimulationReport(final int id, final String sourceKe * @param id - the Cohort Definition ID to copy * @return the copied feasibility study as a FeasibilityStudyDTO */ + @GetMapping( + value = "/{id}/copy", + produces = MediaType.APPLICATION_JSON_VALUE + ) @jakarta.transaction.Transactional - public FeasibilityStudyDTO copy(final int id) { + @Deprecated + public FeasibilityStudyDTO copy(@PathVariable("id") final int id) { FeasibilityStudyDTO sourceStudy = getStudy(id); sourceStudy.id = null; // clear the ID sourceStudy.name = String.format(Constants.Templates.ENTITY_COPY_PREFIX, sourceStudy.name); @@ -701,25 +742,35 @@ public FeasibilityStudyDTO copy(final int id) { /** * Deletes the specified feasibility study - * + * * @summary DO NOT USE * @deprecated * @param id The study ID */ - public void delete(final int id) { + @DeleteMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @Deprecated + public void delete(@PathVariable("id") final int id) { feasibilityStudyRepository.deleteById(id); } /** * Deletes the specified study for the selected source - * + * * @summary DO NOT USE * @deprecated * @param id The study ID * @param sourceKey The source key */ - @Transactional - public void deleteInfo(final int id, final String sourceKey) { + @DeleteMapping( + value = "/{id}/info/{sourceKey}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @Transactional + @Deprecated + public void deleteInfo(@PathVariable("id") final int id, @PathVariable("sourceKey") final String sourceKey) { FeasibilityStudy study = feasibilityStudyRepository.findById(id).orElse(null); StudyGenerationInfo itemToRemove = null; for (StudyGenerationInfo info : study.getStudyGenerationInfoList()) diff --git a/src/main/java/org/ohdsi/webapi/service/JobService.java b/src/main/java/org/ohdsi/webapi/service/JobService.java index ff9bc0c719..7b07f77dd1 100644 --- a/src/main/java/org/ohdsi/webapi/service/JobService.java +++ b/src/main/java/org/ohdsi/webapi/service/JobService.java @@ -25,9 +25,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; +import org.springframework.http.MediaType; import org.springframework.jdbc.core.ResultSetExtractor; -import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.sql.ResultSet; import java.sql.SQLException; @@ -41,10 +46,11 @@ /** * REST Services related to working with the Spring Batch jobs - * + * * @summary Jobs */ -@Component +@RestController +@RequestMapping("/job") public class JobService extends AbstractDaoService { private final JobExplorer jobExplorer; @@ -67,12 +73,13 @@ public JobService(JobExplorer jobExplorer, SearchableJobExecutionDao jobExecutio /** * Get the job information by job ID - * + * * @summary Get job by ID * @param jobId The job ID * @return The job information */ - public JobInstanceResource findJob(final Long jobId) { + @GetMapping(value = "/{jobId}", produces = MediaType.APPLICATION_JSON_VALUE) + public JobInstanceResource findJob(@PathVariable("jobId") final Long jobId) { final JobInstance job = this.jobExplorer.getJobInstance(jobId); if (job == null) { return null;//TODO #8 conventions under review @@ -82,29 +89,34 @@ public JobInstanceResource findJob(final Long jobId) { /** * Get the job execution information by job type and name - * + * * @summary Get job by name and type * @param jobName The job name * @param jobType The job type * @return JobExecutionResource */ - public JobExecutionResource findJobByName(final String jobName, final String jobType) { + @GetMapping(value = "/type/{jobType}/name/{jobName}", produces = MediaType.APPLICATION_JSON_VALUE) + public JobExecutionResource findJobByName( + @PathVariable("jobName") final String jobName, + @PathVariable("jobType") final String jobType) { final Optional jobExecution = jobExplorer.findRunningJobExecutions(jobType).stream() .filter(job -> jobName.equals(job.getJobParameters().getString(Constants.Params.JOB_NAME))) .findFirst(); return jobExecution.isPresent() ? JobUtils.toJobExecutionResource(jobExecution.get()) : null; } - /** - * Get the job execution information by execution ID and job ID - * - * @summary Get job by job ID and execution ID - * @param jobId The job ID - * @param executionId The execution ID - * @return JobExecutionResource - */ - public JobExecutionResource findJobExecution(final Long jobId, - final Long executionId) { + /** + * Get the job execution information by execution ID and job ID + * + * @summary Get job by job ID and execution ID + * @param jobId The job ID + * @param executionId The execution ID + * @return JobExecutionResource + */ + @GetMapping(value = "/{jobId}/execution/{executionId}", produces = MediaType.APPLICATION_JSON_VALUE) + public JobExecutionResource findJobExecution( + @PathVariable("jobId") final Long jobId, + @PathVariable("executionId") final Long executionId) { return service(jobId, executionId); } @@ -115,7 +127,8 @@ public JobExecutionResource findJobExecution(final Long jobId, * @param executionId The job execution ID * @return JobExecutionResource */ - public JobExecutionResource findJobExecution(final Long executionId) { + @GetMapping(value = "/execution/{executionId}", produces = MediaType.APPLICATION_JSON_VALUE) + public JobExecutionResource findJobExecutionById(@PathVariable("executionId") final Long executionId) { return service(null, executionId); } @@ -130,12 +143,13 @@ private JobExecutionResource service(final Long jobId, final Long executionId) { /** * Get job names (unique names). Note: this path (GET /job) should really * return pages of job instances. This could be implemented should the need - * arise. See {@link JobService#list(String, Integer, Integer)} to obtain + * arise. See {@link JobService#list(String, Integer, Integer, boolean)} to obtain * executions and filter by job name. * * @summary Get list of jobs * @return A list of jobs */ + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public List findJobNames() { return this.jobExplorer.getJobNames(); } @@ -156,10 +170,12 @@ public List findJobNames() { * @return collection of JobExecutionInfo * @throws NoSuchJobException */ - public Page list(final String jobName, - final Integer pageIndex, - final Integer pageSize, - boolean comprehensivePage) + @GetMapping(value = "/execution", produces = MediaType.APPLICATION_JSON_VALUE) + public Page list( + @RequestParam(value = "jobName", required = false) final String jobName, + @RequestParam(value = "pageIndex", defaultValue = "0") final Integer pageIndex, + @RequestParam(value = "pageSize", defaultValue = "20") final Integer pageSize, + @RequestParam(value = "comprehensivePage", required = false, defaultValue = "false") boolean comprehensivePage) throws NoSuchJobException { List resources = null; diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/SSOMvcController.java b/src/main/java/org/ohdsi/webapi/service/SSOService.java similarity index 80% rename from src/main/java/org/ohdsi/webapi/mvc/controller/SSOMvcController.java rename to src/main/java/org/ohdsi/webapi/service/SSOService.java index 9be94b134e..a634c4e947 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/SSOMvcController.java +++ b/src/main/java/org/ohdsi/webapi/service/SSOService.java @@ -1,8 +1,7 @@ -package org.ohdsi.webapi.mvc.controller; +package org.ohdsi.webapi.service; import com.google.common.net.HttpHeaders; import org.apache.commons.io.IOUtils; -import org.ohdsi.webapi.mvc.AbstractMvcController; import org.pac4j.core.context.HttpConstants; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; @@ -19,15 +18,11 @@ import java.net.URISyntaxException; /** - * Spring MVC version of SSOController - * - * Migration Status: Replaces /security/SSOController.java (Jersey) - * Endpoints: 2 GET endpoints - * Complexity: Low - SAML metadata and logout redirect + * SSO Service providing SAML metadata and logout functionality */ @RestController @RequestMapping("/saml") -public class SSOMvcController extends AbstractMvcController { +public class SSOService { @Value("${security.saml.metadataLocation}") private String metadataLocation; @@ -41,9 +36,6 @@ public class SSOMvcController extends AbstractMvcController { /** * Get the SAML metadata * - * Jersey: GET /WebAPI/saml/saml-metadata - * Spring MVC: GET /WebAPI/v2/saml/saml-metadata - * * @summary Get metadata * @param response The response context * @throws IOException @@ -64,9 +56,6 @@ public void samlMetadata(HttpServletResponse response) throws IOException { /** * Log out of the service * - * Jersey: GET /WebAPI/saml/slo - * Spring MVC: GET /WebAPI/v2/saml/slo - * * @summary Log out * @return Response * @throws URISyntaxException diff --git a/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java b/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java index 3cf0005dc7..882bce8c83 100644 --- a/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java +++ b/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java @@ -11,18 +11,30 @@ import org.ohdsi.webapi.sqlrender.SourceStatement; import org.ohdsi.webapi.sqlrender.TranslatedStatement; import org.ohdsi.webapi.util.SessionUtils; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; /** * * @author Lee Evans */ +@RestController +@RequestMapping("/sqlrender") public class SqlRenderService { /** * Translate an OHDSI SQL to a supported target SQL dialect * @param sourceStatement JSON with parameters, source SQL, and target dialect * @return rendered and translated SQL */ - public TranslatedStatement translateSQLFromSourceStatement(SourceStatement sourceStatement) { + @PostMapping( + value = "/translate", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public TranslatedStatement translateSQLFromSourceStatement(@RequestBody SourceStatement sourceStatement) { if (sourceStatement == null) { return new TranslatedStatement(); } diff --git a/src/main/java/org/ohdsi/webapi/service/UserService.java b/src/main/java/org/ohdsi/webapi/service/UserService.java index 74fe352d24..17566f525c 100644 --- a/src/main/java/org/ohdsi/webapi/service/UserService.java +++ b/src/main/java/org/ohdsi/webapi/service/UserService.java @@ -1,6 +1,5 @@ package org.ohdsi.webapi.service; -import com.fasterxml.jackson.databind.JsonNode; import org.ohdsi.webapi.arachne.logging.event.*; import org.ohdsi.webapi.shiro.Entities.PermissionEntity; import org.ohdsi.webapi.shiro.Entities.RoleEntity; @@ -10,7 +9,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.stereotype.Component; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; import java.util.*; import java.util.stream.Collectors; @@ -21,7 +21,8 @@ * @author gennadiy.anisimov */ -@Component +@RestController +@RequestMapping("") public class UserService { @Autowired @@ -93,11 +94,15 @@ public int compareTo(Permission o) { return c.compare(this.id, o.id); } } + + @GetMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE) public ArrayList getUsers() { Iterable userEntities = this.authorizer.getUsers(); ArrayList users = convertUsers(userEntities); return users; } + + @GetMapping(value = "/user/me", produces = MediaType.APPLICATION_JSON_VALUE) public User getCurrentUser() throws Exception { UserEntity currentUser = this.authorizer.getCurrentUser(); @@ -113,19 +118,25 @@ public User getCurrentUser() throws Exception { return user; } - public List getUsersPermissions(Long userId) throws Exception { + + @GetMapping(value = "/user/{userId}/permissions", produces = MediaType.APPLICATION_JSON_VALUE) + public List getUsersPermissions(@PathVariable("userId") Long userId) throws Exception { Set permissionEntities = this.authorizer.getUserPermissions(userId); List permissions = convertPermissions(permissionEntities); Collections.sort(permissions); return permissions; } - public ArrayList getUserRoles(Long userId) throws Exception { + + @GetMapping(value = "/user/{userId}/roles", produces = MediaType.APPLICATION_JSON_VALUE) + public ArrayList getUserRoles(@PathVariable("userId") Long userId) throws Exception { Set roleEntities = this.authorizer.getUserRoles(userId); ArrayList roles = convertRoles(roleEntities); Collections.sort(roles); return roles; } - public Role createRole(Role role) throws Exception { + + @PostMapping(value = "/role", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public Role createRole(@RequestBody Role role) throws Exception { RoleEntity roleEntity = this.authorizer.addRole(role.role, true); RoleEntity personalRole = this.authorizer.getCurrentUserPersonalRole(); this.authorizer.addPermissionsFromTemplate( @@ -136,7 +147,9 @@ public Role createRole(Role role) throws Exception { eventPublisher.publishEvent(new AddRoleEvent(this, newRole.id, newRole.role)); return newRole; } - public Role updateRole(Long id, Role role) throws Exception { + + @PutMapping(value = "/role/{roleId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public Role updateRole(@PathVariable("roleId") Long id, @RequestBody Role role) throws Exception { RoleEntity roleEntity = this.authorizer.getRole(id); if (roleEntity == null) { throw new Exception("Role doesn't exist"); @@ -146,28 +159,40 @@ public Role updateRole(Long id, Role role) throws Exception { eventPublisher.publishEvent(new ChangeRoleEvent(this, id, role.role)); return new Role(roleEntity); } + + @GetMapping(value = "/role", produces = MediaType.APPLICATION_JSON_VALUE) public ArrayList getRoles( - boolean includePersonalRoles) { + @RequestParam(value = "include_personal", defaultValue = "false") boolean includePersonalRoles) { Iterable roleEntities = this.authorizer.getRoles(includePersonalRoles); ArrayList roles = convertRoles(roleEntities); return roles; } - public Role getRole(Long id) { + + @GetMapping(value = "/role/{roleId}", produces = MediaType.APPLICATION_JSON_VALUE) + public Role getRole(@PathVariable("roleId") Long id) { RoleEntity roleEntity = this.authorizer.getRole(id); Role role = new Role(roleEntity); return role; } - public void removeRole(Long roleId) { + + @DeleteMapping(value = "/role/{roleId}") + public void removeRole(@PathVariable("roleId") Long roleId) { this.authorizer.removeRole(roleId); this.authorizer.removePermissionsFromTemplate(this.roleCreatorPermissionsTemplate, String.valueOf(roleId)); } - public List getRolePermissions(Long roleId) throws Exception { + + @GetMapping(value = "/role/{roleId}/permissions", produces = MediaType.APPLICATION_JSON_VALUE) + public List getRolePermissions(@PathVariable("roleId") Long roleId) throws Exception { Set permissionEntities = this.authorizer.getRolePermissions(roleId); List permissions = convertPermissions(permissionEntities); Collections.sort(permissions); return permissions; } - public void addPermissionToRole(Long roleId, String permissionIdList) throws Exception { + + @PutMapping(value = "/role/{roleId}/permissions/{permissionIdList}") + public void addPermissionToRole( + @PathVariable("roleId") Long roleId, + @PathVariable("permissionIdList") String permissionIdList) throws Exception { String[] ids = permissionIdList.split("\\+"); for (String permissionIdString : ids) { Long permissionId = Long.parseLong(permissionIdString); @@ -175,7 +200,11 @@ public void addPermissionToRole(Long roleId, String permissionIdList) throws Exc eventPublisher.publishEvent(new AddPermissionEvent(this, permissionId, roleId)); } } - public void removePermissionFromRole(Long roleId, String permissionIdList) { + + @DeleteMapping(value = "/role/{roleId}/permissions/{permissionIdList}") + public void removePermissionFromRole( + @PathVariable("roleId") Long roleId, + @PathVariable("permissionIdList") String permissionIdList) { String[] ids = permissionIdList.split("\\+"); for (String permissionIdString : ids) { Long permissionId = Long.parseLong(permissionIdString); @@ -183,13 +212,19 @@ public void removePermissionFromRole(Long roleId, String permissionIdList) { eventPublisher.publishEvent(new DeletePermissionEvent(this, permissionId, roleId)); } } - public ArrayList getRoleUsers(Long roleId) throws Exception { + + @GetMapping(value = "/role/{roleId}/users", produces = MediaType.APPLICATION_JSON_VALUE) + public ArrayList getRoleUsers(@PathVariable("roleId") Long roleId) throws Exception { Set userEntities = this.authorizer.getRoleUsers(roleId); ArrayList users = this.convertUsers(userEntities); Collections.sort(users); return users; } - public void addUserToRole(Long roleId, String userIdList) throws Exception { + + @PutMapping(value = "/role/{roleId}/users/{userIdList}") + public void addUserToRole( + @PathVariable("roleId") Long roleId, + @PathVariable("userIdList") String userIdList) throws Exception { String[] ids = userIdList.split("\\+"); for (String userIdString : ids) { Long userId = Long.parseLong(userIdString); @@ -197,7 +232,11 @@ public void addUserToRole(Long roleId, String userIdList) throws Exception { eventPublisher.publishEvent(new AssignRoleEvent(this, roleId, userId)); } } - public void removeUserFromRole(Long roleId, String userIdList) { + + @DeleteMapping(value = "/role/{roleId}/users/{userIdList}") + public void removeUserFromRole( + @PathVariable("roleId") Long roleId, + @PathVariable("userIdList") String userIdList) { String[] ids = userIdList.split("\\+"); for (String userIdString : ids) { Long userId = Long.parseLong(userIdString); diff --git a/src/main/java/org/ohdsi/webapi/service/VocabularyService.java b/src/main/java/org/ohdsi/webapi/service/VocabularyService.java index 8e262395aa..93ee2a02dc 100644 --- a/src/main/java/org/ohdsi/webapi/service/VocabularyService.java +++ b/src/main/java/org/ohdsi/webapi/service/VocabularyService.java @@ -66,25 +66,23 @@ import org.springframework.cache.annotation.Caching; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Component; -import org.ohdsi.webapi.vocabulary.MappedRelatedConcept; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; -import org.springframework.http.HttpStatus; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.http.HttpStatus; +import org.ohdsi.webapi.vocabulary.MappedRelatedConcept; /** * Provides REST services for working with * the OMOP standardized vocabularies - * + * * @summary Vocabulary */ -@Component +@RestController +@RequestMapping("/vocabulary") public class VocabularyService extends AbstractDaoService { //create cache @@ -188,18 +186,23 @@ public ConceptSetExport exportConceptSet(ConceptSet conceptSet, SourceInfo vocab } /** - * Calculates the full set of ancestor and descendant concepts for a list of + * Calculates the full set of ancestor and descendant concepts for a list of * ancestor and descendant concepts specified. This is used by ATLAS when * navigating the list of included concepts in a concept set - the full list * of ancestors (as defined in the concept set) and the descendants (those - * concepts included when resolving the concept set) are used to determine + * concepts included when resolving the concept set) are used to determine * which descendant concepts share one or more ancestors. - * + * * @summary Calculates ancestors for a list of concepts * @param ids Concepts identifiers from concept set * @return A map of the form: {id -> List} */ - public Map> calculateAscendants(String sourceKey, Ids ids) { + @PostMapping(value = "/{sourceKey}/lookup/identifiers/ancestors", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Map> calculateAscendants( + @PathVariable("sourceKey") String sourceKey, + @RequestBody Ids ids) { Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -243,7 +246,7 @@ public Map> calculateAscendants(String sourceKey, Ids ids) { ); } - private static class Ids { + public static class Ids { public List ancestors; public List descendants; } @@ -261,7 +264,7 @@ protected PreparedStatementRenderer prepareAscendantsCalculating(Long[] identifi /** * Get concepts from concept identifiers (IDs) from a specific source - * + * * @summary Perform a lookup of an array of concept identifiers returning the * matching concepts with their detailed properties. * @param sourceKey path parameter specifying the source key identifying the @@ -269,7 +272,12 @@ protected PreparedStatementRenderer prepareAscendantsCalculating(Long[] identifi * @param identifiers an array of concept identifiers * @return A collection of concepts */ - public Collection executeIdentifierLookup(String sourceKey, long[] identifiers) { + @PostMapping(value = "/{sourceKey}/lookup/identifiers", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection executeIdentifierLookup( + @PathVariable("sourceKey") String sourceKey, + @RequestBody long[] identifiers) { Source source = getSourceRepository().findBySourceKey(sourceKey); return executeIdentifierLookup(source, identifiers); } @@ -303,15 +311,18 @@ protected PreparedStatementRenderer prepareExecuteIdentifierLookup(long[] identi } /** - * Get concepts from concept identifiers (IDs) from the default vocabulary + * Get concepts from concept identifiers (IDs) from the default vocabulary * source - * + * * @summary Perform a lookup of an array of concept identifiers returning the * matching concepts with their detailed properties, using the default source. * @param identifiers an array of concept identifiers * @return A collection of concepts */ - public Collection executeIdentifierLookup(long[] identifiers) { + @PostMapping(value = "/lookup/identifiers", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection executeIdentifierLookup(@RequestBody long[] identifiers) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) @@ -336,7 +347,7 @@ public Collection executeIncludedConceptLookup(String sourceKey, Concep /** * Get concepts from source codes from a specific source - * + * * @summary Lookup source codes from the concept CONCEPT_CODE field * in the specified vocabulary * @param sourceKey path parameter specifying the source key identifying the @@ -344,7 +355,12 @@ public Collection executeIncludedConceptLookup(String sourceKey, Concep * @param sourcecodes array of source codes * @return A collection of concepts */ - public Collection executeSourcecodeLookup(String sourceKey, String[] sourcecodes) { + @PostMapping(value = "/{sourceKey}/lookup/sourcecodes", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection executeSourcecodeLookup( + @PathVariable("sourceKey") String sourceKey, + @RequestBody String[] sourcecodes) { if (sourcecodes.length == 0) { return new ArrayList<>(); } @@ -365,13 +381,16 @@ protected PreparedStatementRenderer prepareExecuteSourcecodeLookup(String[] sour /** * Get concepts from source codes from the default vocabulary source - * + * * @summary Lookup source codes from the concept CONCEPT_CODE field * in the specified vocabulary * @param sourcecodes array of source codes * @return A collection of concepts */ - public Collection executeSourcecodeLookup(String[] sourcecodes) { + @PostMapping(value = "/lookup/sourcecodes", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection executeSourcecodeLookup(@RequestBody String[] sourcecodes) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) @@ -381,18 +400,23 @@ public Collection executeSourcecodeLookup(String[] sourcecodes) { } /** - * Get concepts mapped to the selected concept identifiers from a - * specific source. Find all concepts mapped to the concept identifiers + * Get concepts mapped to the selected concept identifiers from a + * specific source. Find all concepts mapped to the concept identifiers * provided. This end-point will check the CONCEPT, CONCEPT_RELATIONSHIP and * SOURCE_TO_CONCEPT_MAP tables. - * + * * @summary Concepts mapped to other concepts * @param sourceKey path parameter specifying the source key identifying the * source to use for access to the set of vocabulary tables * @param identifiers an array of concept identifiers * @return A collection of concepts */ - public Collection executeMappedLookup(String sourceKey, long[] identifiers) { + @PostMapping(value = "/{sourceKey}/lookup/mapped", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection executeMappedLookup( + @PathVariable("sourceKey") String sourceKey, + @RequestBody long[] identifiers) { Source source = getSourceRepository().findBySourceKey(sourceKey); return executeMappedLookup(source, identifiers); } @@ -427,16 +451,19 @@ protected PreparedStatementRenderer prepareExecuteMappedLookup(long[] identifier } /** - * Get concepts mapped to the selected concept identifiers from a - * specific source. Find all concepts mapped to the concept identifiers + * Get concepts mapped to the selected concept identifiers from a + * specific source. Find all concepts mapped to the concept identifiers * provided. This end-point will check the CONCEPT, CONCEPT_RELATIONSHIP and * SOURCE_TO_CONCEPT_MAP tables. - * + * * @summary Concepts mapped to other concepts * @param identifiers an array of concept identifiers * @return A collection of concepts */ - public Collection executeMappedLookup(long[] identifiers) { + @PostMapping(value = "/lookup/mapped", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection executeMappedLookup(@RequestBody long[] identifiers) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (defaultSourceKey == null) @@ -461,13 +488,18 @@ public Collection executeMappedLookup(String sourceKey, ConceptSetExpre /** * Search for a concept on the selected source. - * + * * @summary Search for a concept on the selected source * @param sourceKey The source key for the concept search * @param search The ConceptSearch parameters * @return A collection of concepts */ - public Collection executeSearch(String sourceKey, ConceptSearch search) { + @PostMapping(value = "/{sourceKey}/search", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection executeSearch( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptSearch search) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareExecuteSearch(search, source); @@ -618,43 +650,56 @@ protected PreparedStatementRenderer prepareExecuteSearch(ConceptSearch search, S /** * Search for a concept on the default vocabulary source. - * + * * @summary Search for a concept (default vocabulary source) * @param search The ConceptSearch parameters * @return A collection of concepts */ - public Collection executeSearch(ConceptSearch search) { + @PostMapping(value = "/search", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection executeSearch(@RequestBody ConceptSearch search) { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 - return executeSearch(defaultSourceKey, search); + return executeSearch(defaultSourceKey, search); } /** * Search for a concept based on a query using the selected vocabulary source. - * + * * @summary Search for a concept using a query * @param sourceKey The source key holding the OMOP vocabulary * @param query The query to use to search for concepts * @return A collection of concepts */ - public Collection executeSearch(String sourceKey, String query) { + @GetMapping(value = "/{sourceKey}/search/{query}", + produces = MediaType.APPLICATION_JSON_VALUE) + public Collection executeSearch( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("query") String query) { return this.executeSearch(sourceKey, query, DEFAULT_SEARCH_ROWS); } /** * Search for a concept based on a query using the default vocabulary source. * NOTE: This method uses the query as part of the URL query string - * + * * @summary Search for a concept using a query (default vocabulary) * @param sourceKey The source key holding the OMOP vocabulary * @param query The query to use to search for concepts * @param rows The number of rows to return. * @return A collection of concepts */ - public Collection executeSearch(String sourceKey, String query, String rows) { + @GetMapping(value = "/{sourceKey}/searchByQuery", + produces = MediaType.APPLICATION_JSON_VALUE, + params = "query") + public Collection executeSearch( + @PathVariable("sourceKey") String sourceKey, + @RequestParam("query") String query, + @RequestParam(value = "rows", defaultValue = DEFAULT_SEARCH_ROWS) String rows) { // Verify that the rows parameter contains an integer and is > 0 try { Integer r = Integer.parseInt(rows); @@ -687,34 +732,40 @@ public PreparedStatementRenderer prepareExecuteSearchWithQuery(String query, Sou /** * Search for a concept based on a query using the default vocabulary source. - * NOTE: This method uses the query as part of the URL and not the + * NOTE: This method uses the query as part of the URL and not the * query string - * + * * @summary Search for a concept using a query (default vocabulary) * @param query The query to use to search for concepts * @return A collection of concepts */ - public Collection executeSearch(String query) { - + @GetMapping(value = "/search/{query}", + produces = MediaType.APPLICATION_JSON_VALUE) + public Collection executeSearch(@PathVariable("query") String query) { + String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return executeSearch(defaultSourceKey, query); } /** - * Get a concept based on the concept identifier from the specified + * Get a concept based on the concept identifier from the specified * source - * + * * @summary Get concept details * @param sourceKey The source containing the vocabulary * @param id The concept ID to find * @return The concept details */ - @Cacheable(cacheNames = CachingSetup.CONCEPT_DETAIL_CACHE, key = "#sourceKey.concat('/').concat(#id)") - public Concept getConcept(final String sourceKey, final long id) { + @GetMapping(value = "/{sourceKey}/concept/{id}", + produces = MediaType.APPLICATION_JSON_VALUE) + @Cacheable(cacheNames = CachingSetup.CONCEPT_DETAIL_CACHE, key = "#sourceKey.concat('/').concat(#id)") + public Concept getConcept( + @PathVariable("sourceKey") final String sourceKey, + @PathVariable("id") final long id) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/vocabulary/sql/getConcept.sql"; String tqValue = source.getTableQualifier(SourceDaimon.DaimonType.Vocabulary); @@ -733,34 +784,40 @@ public Concept getConcept(final String sourceKey, final long id) { /** * Get a concept based on the concept identifier from the default * vocabulary source - * + * * @summary Get concept details (default vocabulary source) * @param id The concept ID to find * @return The concept details */ - public Concept getConcept(final long id) { + @GetMapping(value = "/concept/{id}", + produces = MediaType.APPLICATION_JSON_VALUE) + public Concept getConcept(@PathVariable("id") final long id) { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getConcept(defaultSourceKey, id); - + } /** - * Get related concepts for the selected concept identifier from a source. + * Get related concepts for the selected concept identifier from a source. * Related concepts will include those concepts that have a relationship - * to the selected concept identifier in the CONCEPT_RELATIONSHIP and + * to the selected concept identifier in the CONCEPT_RELATIONSHIP and * CONCEPT_ANCESTOR tables. - * + * * @summary Get related concepts * @param sourceKey The source containing the vocabulary * @param id The concept ID to find * @return A collection of related concepts */ - @Cacheable(cacheNames = CachingSetup.CONCEPT_RELATED_CACHE, key = "#sourceKey.concat('/').concat(#id)") - public Collection getRelatedConcepts(String sourceKey, final Long id) { + @GetMapping(value = "/{sourceKey}/concept/{id}/related", + produces = MediaType.APPLICATION_JSON_VALUE) + @Cacheable(cacheNames = CachingSetup.CONCEPT_RELATED_CACHE, key = "#sourceKey.concat('/').concat(#id)") + public Collection getRelatedConcepts( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") final Long id) { final Map concepts = new HashMap<>(); Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/vocabulary/sql/getRelatedConcepts.sql"; @@ -774,7 +831,11 @@ public Collection getRelatedConcepts(String sourceKey, final Lon return concepts.values(); } - public Collection getRelatedStandardMappedConcepts(String sourceKey, List allConceptIds) { + @PostMapping(value = "/{sourceKey}/related-standard", + produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getRelatedStandardMappedConcepts( + @PathVariable("sourceKey") String sourceKey, + @RequestBody List allConceptIds) { Source source = getSourceRepository().findBySourceKey(sourceKey); String relatedConceptsSQLPath = "/resources/vocabulary/sql/getRelatedStandardMappedConcepts.sql"; String relatedMappedFromIdsSQLPath = "/resources/vocabulary/sql/getRelatedStandardMappedConcepts_getMappedFromIds.sql"; @@ -840,16 +901,20 @@ void enrichResultCombinedMappedConcepts(Map resultCo } /** - * Get ancestor and descendant concepts for the selected concept identifier - * from a source. - * + * Get ancestor and descendant concepts for the selected concept identifier + * from a source. + * * @summary Get ancestors and descendants for a concept * @param sourceKey The source containing the vocabulary * @param id The concept ID * @return A collection of related concepts */ - @Cacheable(cacheNames = CachingSetup.CONCEPT_HIERARCHY_CACHE, key = "#sourceKey.concat('/').concat(#id)") - public Collection getConceptAncestorAndDescendant(String sourceKey, final Long id) { + @GetMapping(value = "/{sourceKey}/concept/{id}/ancestorAndDescendant", + produces = MediaType.APPLICATION_JSON_VALUE) + @Cacheable(cacheNames = CachingSetup.CONCEPT_HIERARCHY_CACHE, key = "#sourceKey.concat('/').concat(#id)") + public Collection getConceptAncestorAndDescendant( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") final Long id) { final Map concepts = new HashMap<>(); Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/vocabulary/sql/getConceptAncestorAndDescendant.sql"; @@ -866,31 +931,38 @@ public Collection getConceptAncestorAndDescendant(String sourceK /** * Get related concepts for the selected concept identifier from the - * default vocabulary source. - * + * default vocabulary source. + * * @summary Get related concepts (default vocabulary) * @param id The concept identifier * @return A collection of related concepts */ - public Collection getRelatedConcepts(final Long id) { + @GetMapping(value = "/concept/{id}/related", + produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getRelatedConcepts(@PathVariable("id") final Long id) { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getRelatedConcepts(defaultSourceKey, id); } /** * Get a list of common ancestor concepts for a selected list of concept - * identifiers using the selected vocabulary source. - * + * identifiers using the selected vocabulary source. + * * @summary Get common ancestor concepts * @param sourceKey The source containing the vocabulary * @param identifiers An array of concept identifiers * @return A collection of related concepts */ - public Collection getCommonAncestors(String sourceKey, Object[] identifiers) { + @PostMapping(value = "/{sourceKey}/commonAncestors", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getCommonAncestors( + @PathVariable("sourceKey") String sourceKey, + @RequestBody Object[] identifiers) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetCommonAncestors(identifiers, source); final Map concepts = new HashMap<>(); @@ -918,31 +990,39 @@ protected PreparedStatementRenderer prepareGetCommonAncestors(Object[] identifie /** * Get a list of common ancestor concepts for a selected list of concept - * identifiers using the default vocabulary source. - * + * identifiers using the default vocabulary source. + * * @summary Get common ancestor concepts (default vocabulary) * @param identifiers An array of concept identifiers * @return A collection of related concepts */ - public Collection getCommonAncestors(Object[] identifiers) { + @PostMapping(value = "/commonAncestors", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getCommonAncestors(@RequestBody Object[] identifiers) { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getCommonAncestors(defaultSourceKey, identifiers); } /** - * Resolve a concept set expression into a collection - * of concept identifiers using the selected vocabulary source. - * + * Resolve a concept set expression into a collection + * of concept identifiers using the selected vocabulary source. + * * @summary Resolve concept set expression * @param sourceKey The source containing the vocabulary * @param conceptSetExpression A concept set expression * @return A collection of concept identifiers */ - public Collection resolveConceptSetExpression(String sourceKey, ConceptSetExpression conceptSetExpression) { + @PostMapping(value = "/{sourceKey}/resolveConceptSetExpression", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection resolveConceptSetExpression( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptSetExpression conceptSetExpression) { Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = new ConceptSetStrategy(conceptSetExpression).prepareStatement(source, null); final ArrayList identifiers = new ArrayList<>(); @@ -957,32 +1037,40 @@ public void processRow(ResultSet rs) throws SQLException { } /** - * Resolve a concept set expression into a collection - * of concept identifiers using the default vocabulary source. - * + * Resolve a concept set expression into a collection + * of concept identifiers using the default vocabulary source. + * * @summary Resolve concept set expression (default vocabulary) * @param conceptSetExpression A concept set expression * @return A collection of concept identifiers */ - public Collection resolveConceptSetExpression(ConceptSetExpression conceptSetExpression) { + @PostMapping(value = "/resolveConceptSetExpression", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection resolveConceptSetExpression(@RequestBody ConceptSetExpression conceptSetExpression) { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return resolveConceptSetExpression(defaultSourceKey, conceptSetExpression); } /** * Resolve a concept set expression to get the count - * of included concepts using the selected vocabulary source. - * + * of included concepts using the selected vocabulary source. + * * @summary Get included concept counts for concept set expression * @param sourceKey The source containing the vocabulary * @param conceptSetExpression A concept set expression * @return A count of included concepts */ - public Integer countIncludedConceptSets(String sourceKey, ConceptSetExpression conceptSetExpression) { + @PostMapping(value = "/{sourceKey}/included-concepts/count", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Integer countIncludedConceptSets( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptSetExpression conceptSetExpression) { Source source = getSourceRepository().findBySourceKey(sourceKey); String query = new ConceptSetStrategy(conceptSetExpression).prepareStatement(source, sql -> "select count(*) from (" + sql + ") Q;").getSql(); @@ -991,13 +1079,16 @@ public Integer countIncludedConceptSets(String sourceKey, ConceptSetExpression c /** * Resolve a concept set expression to get the count - * of included concepts using the default vocabulary source. - * + * of included concepts using the default vocabulary source. + * * @summary Get included concept counts for concept set expression (default vocabulary) * @param conceptSetExpression A concept set expression * @return A count of included concepts */ - public Integer countIncludedConcepSets(ConceptSetExpression conceptSetExpression) { + @PostMapping(value = "/included-concepts/count", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Integer countIncludedConcepSets(@RequestBody ConceptSetExpression conceptSetExpression) { String defaultSourceKey = getDefaultVocabularySourceKey(); if (Objects.isNull(defaultSourceKey)) { @@ -1010,28 +1101,35 @@ public Integer countIncludedConcepSets(ConceptSetExpression conceptSetExpression /** * Produces a SQL query to use against your OMOP CDM to create the * resolved concept set - * + * * @summary Get SQL to resolve concept set expression * @param conceptSetExpression A concept set expression * @return SQL Statement as text */ - public String getConceptSetExpressionSQL(ConceptSetExpression conceptSetExpression) { + @PostMapping(value = "/conceptSetExpressionSQL", + produces = MediaType.TEXT_PLAIN_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public String getConceptSetExpressionSQL(@RequestBody ConceptSetExpression conceptSetExpression) { ConceptSetExpressionQueryBuilder builder = new ConceptSetExpressionQueryBuilder(); String query = builder.buildExpressionQuery(conceptSetExpression); - + return query; } /** * Get a collection of descendant concepts for the selected concept * identifier using the selected source key - * + * * @summary Get descendant concepts for the selected concept identifier * @param sourceKey The source containing the vocabulary * @param id The concept identifier * @return A collection of concepts */ - public Collection getDescendantConcepts(String sourceKey, final Long id) { + @GetMapping(value = "/{sourceKey}/concept/{id}/descendants", + produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getDescendantConcepts( + @PathVariable("sourceKey") String sourceKey, + @PathVariable("id") final Long id) { final Map concepts = new HashMap<>(); Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/vocabulary/sql/getDescendantConcepts.sql"; @@ -1051,29 +1149,33 @@ public Void mapRow(ResultSet resultSet, int arg1) throws SQLException { /** * Get a collection of descendant concepts for the selected concept * identifier using the default vocabulary - * + * * @summary Get descendant concepts for the selected concept identifier (default vocabulary) * @param id The concept identifier * @return A collection of concepts */ - public Collection getDescendantConcepts(final Long id) { + @GetMapping(value = "/concept/{id}/descendants", + produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getDescendantConcepts(@PathVariable("id") final Long id) { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getDescendantConcepts(defaultSourceKey, id); } /** - * Get a collection of domains from the domain table in the + * Get a collection of domains from the domain table in the * vocabulary for the the selected source key. - * + * * @summary Get domains * @param sourceKey The source containing the vocabulary * @return A collection of domains */ - public Collection getDomains(String sourceKey) { + @GetMapping(value = "/{sourceKey}/domains", + produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getDomains(@PathVariable("sourceKey") String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); String tableQualifier = source.getTableQualifier(SourceDaimon.DaimonType.Vocabulary); String sqlPath = "/resources/vocabulary/sql/getDomains.sql"; @@ -1092,30 +1194,34 @@ public Domain mapRow(final ResultSet resultSet, final int arg1) throws SQLExcept } /** - * Get a collection of domains from the domain table in the + * Get a collection of domains from the domain table in the * default vocabulary. - * + * * @summary Get domains (default vocabulary) * @return A collection of domains */ + @GetMapping(value = "/domains", + produces = MediaType.APPLICATION_JSON_VALUE) public Collection getDomains() { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getDomains(defaultSourceKey); } /** - * Get a collection of vocabularies from the vocabulary table in the + * Get a collection of vocabularies from the vocabulary table in the * selected source key. - * + * * @summary Get vocabularies * @param sourceKey The source containing the vocabulary * @return A collection of vocabularies */ - public Collection getVocabularies(String sourceKey) { + @GetMapping(value = "/{sourceKey}/vocabularies", + produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getVocabularies(@PathVariable("sourceKey") String sourceKey) { Source source = getSourceRepository().findBySourceKey(sourceKey); String sqlPath = "/resources/vocabulary/sql/getVocabularies.sql"; String tableQualifier = source.getTableQualifier(SourceDaimon.DaimonType.Vocabulary); @@ -1136,18 +1242,19 @@ public Vocabulary mapRow(final ResultSet resultSet, final int arg1) throws SQLEx } /** - * Get a collection of vocabularies from the vocabulary table in the + * Get a collection of vocabularies from the vocabulary table in the * default vocabulary - * + * * @summary Get vocabularies (default vocabulary) - * @param sourceKey The source containing the vocabulary * @return A collection of vocabularies */ + @GetMapping(value = "/vocabularies", + produces = MediaType.APPLICATION_JSON_VALUE) public Collection getVocabularies() { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getVocabularies(defaultSourceKey); } @@ -1184,12 +1291,14 @@ private void addRelationships(final Map concepts, final Re /** * Get the vocabulary version from the vocabulary table using * the selected source key - * + * * @summary Get vocabulary version info * @param sourceKey The source containing the vocabulary * @return The vocabulary info */ - public VocabularyInfo getInfo(String sourceKey) { + @GetMapping(value = "/{sourceKey}/info", + produces = MediaType.APPLICATION_JSON_VALUE) + public VocabularyInfo getInfo(@PathVariable("sourceKey") String sourceKey) { if (vocabularyInfoCache == null) { vocabularyInfoCache = new Hashtable<>(); } @@ -1228,17 +1337,22 @@ public void clearCaches() { } /** - * Get the descendant concepts of the selected ancestor vocabulary and - * concept class for the selected sibling vocabulary and concept class. + * Get the descendant concepts of the selected ancestor vocabulary and + * concept class for the selected sibling vocabulary and concept class. * It is unclear how this endpoint is used so it may be a candidate to * deprecate. - * + * * @summary Get descendant concepts by source * @param sourceKey The source containing the vocabulary * @param search The descendant of ancestor search object * @return A collection of concepts */ - public Collection getDescendantOfAncestorConcepts(String sourceKey, DescendentOfAncestorSearch search) { + @PostMapping(value = "/{sourceKey}/descendantofancestor", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getDescendantOfAncestorConcepts( + @PathVariable("sourceKey") String sourceKey, + @RequestBody DescendentOfAncestorSearch search) { Tracker.trackActivity(ActivityType.Search, "getDescendantOfAncestorConcepts"); Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1258,34 +1372,42 @@ protected PreparedStatementRenderer prepareGetDescendantOfAncestorConcepts(Desce } /** - * Get the descendant concepts of the selected ancestor vocabulary and - * concept class for the selected sibling vocabulary and concept class. + * Get the descendant concepts of the selected ancestor vocabulary and + * concept class for the selected sibling vocabulary and concept class. * It is unclear how this endpoint is used so it may be a candidate to * deprecate. - * + * * @summary Get descendant concepts (default vocabulary) * @param search The descendant of ancestor search object * @return A collection of concepts */ - public Collection getDescendantOfAncestorConcepts(DescendentOfAncestorSearch search) { + @PostMapping(value = "/descendantofancestor", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getDescendantOfAncestorConcepts(@RequestBody DescendentOfAncestorSearch search) { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getDescendantOfAncestorConcepts(defaultSourceKey, search); } /** - * Get the related concepts for a list of concept ids using the + * Get the related concepts for a list of concept ids using the * concept_relationship table for the selected source key - * + * * @summary Get related concepts * @param sourceKey The source containing the vocabulary * @param search The concept identifiers of interest * @return A collection of concepts */ - public Collection getRelatedConcepts(String sourceKey, RelatedConceptSearch search) { + @PostMapping(value = "/{sourceKey}/relatedconcepts", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getRelatedConcepts( + @PathVariable("sourceKey") String sourceKey, + @RequestBody RelatedConceptSearch search) { Tracker.trackActivity(ActivityType.Search, "getRelatedConcepts"); Source source = getSourceRepository().findBySourceKey(sourceKey); @@ -1318,18 +1440,21 @@ protected PreparedStatementRenderer prepareGetRelatedConcepts(RelatedConceptSear } /** - * Get the related concepts for a list of concept ids using the + * Get the related concepts for a list of concept ids using the * concept_relationship table - * + * * @summary Get related concepts (default vocabulary) * @param search The concept identifiers of interest * @return A collection of concepts */ - public Collection getRelatedConcepts(RelatedConceptSearch search) { + @PostMapping(value = "/relatedconcepts", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getRelatedConcepts(@RequestBody RelatedConceptSearch search) { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getRelatedConcepts(defaultSourceKey, search); } @@ -1337,13 +1462,18 @@ public Collection getRelatedConcepts(RelatedConceptSearch search) { /** * Get the descendant concepts for a selected list of concept ids for a * selected source key - * + * * @summary Get descendant concepts for selected concepts * @param sourceKey The source containing the vocabulary * @param conceptList The list of concept identifiers * @return A collection of concepts */ - public Collection getDescendantConceptsByList(String sourceKey, String[] conceptList) { + @PostMapping(value = "/{sourceKey}/conceptlist/descendants", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getDescendantConceptsByList( + @PathVariable("sourceKey") String sourceKey, + @RequestBody String[] conceptList) { final Map concepts = new HashMap<>(); Source source = getSourceRepository().findBySourceKey(sourceKey); PreparedStatementRenderer psr = prepareGetDescendantConceptsByList(conceptList, source); @@ -1359,17 +1489,20 @@ public Void mapRow(ResultSet resultSet, int arg1) throws SQLException { } /** - * Get the descendant concepts for a selected list of concept ids - * + * Get the descendant concepts for a selected list of concept ids + * * @summary Get descendant concepts for selected concepts (default vocabulary) * @param conceptList The list of concept identifiers * @return A collection of concepts */ - public Collection getDescendantConceptsByList(String[] conceptList) { + @PostMapping(value = "/conceptlist/descendants", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getDescendantConceptsByList(@RequestBody String[] conceptList) { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return getDescendantConceptsByList(defaultSourceKey, conceptList); } @@ -1385,13 +1518,18 @@ protected PreparedStatementRenderer prepareGetDescendantConceptsByList(String[] /** * Get the recommended concepts for a selected list of concept ids for a * selected source key - * + * * @summary Get recommended concepts for selected concepts * @param sourceKey The source containing the vocabulary * @param conceptList The list of concept identifiers * @return A collection of recommended concepts */ - public Collection getRecommendedConceptsByList(String sourceKey, long[] conceptList) { + @PostMapping(value = "/{sourceKey}/lookup/recommended", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection getRecommendedConceptsByList( + @PathVariable("sourceKey") String sourceKey, + @RequestBody long[] conceptList) { if (conceptList.length == 0) { return new ArrayList(); // empty list of recommendations } @@ -1440,13 +1578,18 @@ private void addRecommended(Map concepts, ResultSet re /** * Compares two concept set expressions to find which concepts are * shared or unique to each concept set for the selected vocabulary source. - * + * * @summary Compare concept sets * @param sourceKey The source containing the vocabulary * @param conceptSetExpressionList Expects a list of exactly 2 concept set expressions * @return A collection of concept set comparisons */ - public Collection compareConceptSets(String sourceKey, ConceptSetExpression[] conceptSetExpressionList) throws Exception { + @PostMapping(value = "/{sourceKey}/compare", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection compareConceptSets( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptSetExpression[] conceptSetExpressionList) throws Exception { if (conceptSetExpressionList.length != 2) { throw new Exception("You must specify two concept set expressions in order to use this method."); } @@ -1472,8 +1615,12 @@ public Collection compareConceptSets(String sourceKey, Con return returnVal; } - public Collection compareConceptSetsCsv(final String sourceKey, - final CompareArbitraryDto dto) throws Exception { + @PostMapping(value = "/{sourceKey}/compare-arbitrary", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection compareConceptSetsCsv( + @PathVariable("sourceKey") final String sourceKey, + @RequestBody final CompareArbitraryDto dto) throws Exception { final ConceptSetExpression[] csExpressionList = dto.compareTargets; if (csExpressionList.length != 2) { throw new Exception("You must specify two concept set expressions in order to use this method."); @@ -1501,16 +1648,20 @@ public Collection compareConceptSetsCsv(final String sourc /** * Compares two concept set expressions to find which concepts are * shared or unique to each concept set. - * + * * @summary Compare concept sets (default vocabulary) * @param conceptSetExpressionList Expects a list of exactly 2 concept set expressions * @return A collection of concept set comparisons */ - public Collection compareConceptSets(ConceptSetExpression[] conceptSetExpressionList) throws Exception { + @PostMapping(value = "/compare", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public Collection compareConceptSets( + @RequestBody ConceptSetExpression[] conceptSetExpressionList) throws Exception { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return compareConceptSets(defaultSourceKey, conceptSetExpressionList); } @@ -1519,17 +1670,20 @@ public Collection compareConceptSets(ConceptSetExpression[ /** * Optimizes a concept set expressions to find redundant concepts specified * in a concept set expression. - * + * * @summary Optimize concept set (default vocabulary) - * @param sourceKey The source containing the vocabulary * @param conceptSetExpression The concept set expression to optimize * @return A concept set optimization */ - public ConceptSetOptimizationResult optimizeConceptSet(ConceptSetExpression conceptSetExpression) throws Exception { + @PostMapping(value = "/optimize", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ConceptSetOptimizationResult optimizeConceptSet( + @RequestBody ConceptSetExpression conceptSetExpression) throws Exception { String defaultSourceKey = getDefaultVocabularySourceKey(); - + if (defaultSourceKey == null) - throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "No vocabulary or cdm daimon was found in configured sources. Search failed."); // http 503 return optimizeConceptSet(defaultSourceKey, conceptSetExpression); } @@ -1537,13 +1691,18 @@ public ConceptSetOptimizationResult optimizeConceptSet(ConceptSetExpression conc /** * Optimizes a concept set expressions to find redundant concepts specified * in a concept set expression for the selected source key. - * + * * @summary Optimize concept set * @param sourceKey The source containing the vocabulary * @param conceptSetExpression The concept set expression to optimize * @return A concept set optimization */ - public ConceptSetOptimizationResult optimizeConceptSet(String sourceKey, ConceptSetExpression conceptSetExpression) throws Exception { + @PostMapping(value = "/{sourceKey}/optimize", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ConceptSetOptimizationResult optimizeConceptSet( + @PathVariable("sourceKey") String sourceKey, + @RequestBody ConceptSetExpression conceptSetExpression) throws Exception { // resolve the concept set to get included concepts Collection includedConcepts = this.resolveConceptSetExpression(sourceKey, conceptSetExpression); long[] includedConceptsArray = includedConcepts.stream().mapToLong(Long::longValue).toArray(); diff --git a/src/main/java/org/ohdsi/webapi/source/SourceService.java b/src/main/java/org/ohdsi/webapi/source/SourceService.java index 6c4cc3d404..f23fbceaec 100644 --- a/src/main/java/org/ohdsi/webapi/source/SourceService.java +++ b/src/main/java/org/ohdsi/webapi/source/SourceService.java @@ -1,86 +1,124 @@ package org.ohdsi.webapi.source; import org.apache.commons.collections4.map.PassiveExpiringMap; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import org.jasypt.encryption.pbe.PBEStringEncryptor; import org.jasypt.properties.PropertyValueEncryptionUtils; import org.ohdsi.sql.SqlTranslate; +import org.ohdsi.webapi.arachne.logging.event.AddDataSourceEvent; +import org.ohdsi.webapi.arachne.logging.event.ChangeDataSourceEvent; +import org.ohdsi.webapi.arachne.logging.event.DeleteDataSourceEvent; +import org.ohdsi.webapi.common.DBMSType; import org.ohdsi.webapi.common.SourceMapKey; +import org.ohdsi.webapi.exception.SourceDuplicateKeyException; import org.ohdsi.webapi.service.AbstractDaoService; +import org.ohdsi.webapi.service.VocabularyService; +import org.ohdsi.webapi.shiro.Entities.UserEntity; import org.ohdsi.webapi.shiro.management.datasource.SourceAccessor; 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.context.ApplicationEventPublisher; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.jdbc.CannotGetJdbcConnectionException; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; import jakarta.annotation.PostConstruct; -import java.util.*; -import java.util.stream.Collectors; +import jakarta.persistence.PersistenceException; import javax.cache.CacheManager; import javax.cache.configuration.MutableConfiguration; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + 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 +@RestController +@RequestMapping("/source") +@Transactional public class SourceService extends AbstractDaoService { - @Component - public static class CachingSetup implements JCacheManagerCustomizer { - - public static final String SOURCE_LIST_CACHE = "sourceList"; - - @Override - public void customize(CacheManager cacheManager) { - // Evict when a cohort definition is created or updated, or permissions, or tags - if (!CacheHelper.getCacheNames(cacheManager).contains(SOURCE_LIST_CACHE)) { - cacheManager.createCache(SOURCE_LIST_CACHE, new MutableConfiguration>() - .setTypes(Object.class, (Class>) (Class) List.class) - .setStoreByValue(false) - .setStatisticsEnabled(true)); - } - } - } + public static final String SECURE_MODE_ERROR = "This feature requires the administrator to enable security for the application"; + + @Component + public static class CachingSetup implements JCacheManagerCustomizer { + + public static final String SOURCE_LIST_CACHE = "sourceList"; + + @Override + public void customize(CacheManager cacheManager) { + // Evict when a cohort definition is created or updated, or permissions, or tags + if (!CacheHelper.getCacheNames(cacheManager).contains(SOURCE_LIST_CACHE)) { + cacheManager.createCache(SOURCE_LIST_CACHE, new MutableConfiguration>() + .setTypes(Object.class, (Class>) (Class) List.class) + .setStoreByValue(false) + .setStatisticsEnabled(true)); + } + } + } + @Value("${jasypt.encryptor.enabled}") private boolean encryptorEnabled; @Value("${datasource.ohdsi.schema}") private String schema; - private Map connectionAvailability = Collections.synchronizedMap(new PassiveExpiringMap<>(5000)); + @Value("#{!'${security.provider}'.equals('DisabledSecurity')}") + private boolean securityEnabled; + private Map connectionAvailability = Collections.synchronizedMap(new PassiveExpiringMap<>(5000)); private final SourceRepository sourceRepository; + private final SourceDaimonRepository sourceDaimonRepository; private final JdbcTemplate jdbcTemplate; - private PBEStringEncryptor defaultStringEncryptor; - private SourceAccessor sourceAccessor; - - public SourceService(SourceRepository sourceRepository, JdbcTemplate jdbcTemplate, PBEStringEncryptor defaultStringEncryptor, SourceAccessor sourceAccessor) { - + private final PBEStringEncryptor defaultStringEncryptor; + private final SourceAccessor sourceAccessor; + private final GenericConversionService conversionService; + private final VocabularyService vocabularyService; + private final ApplicationEventPublisher publisher; + + public SourceService(SourceRepository sourceRepository, + SourceDaimonRepository sourceDaimonRepository, + JdbcTemplate jdbcTemplate, + PBEStringEncryptor defaultStringEncryptor, + SourceAccessor sourceAccessor, + GenericConversionService conversionService, + VocabularyService vocabularyService, + ApplicationEventPublisher publisher) { this.sourceRepository = sourceRepository; + this.sourceDaimonRepository = sourceDaimonRepository; this.jdbcTemplate = jdbcTemplate; this.defaultStringEncryptor = defaultStringEncryptor; this.sourceAccessor = sourceAccessor; + this.conversionService = conversionService; + this.vocabularyService = vocabularyService; + this.publisher = publisher; } @PostConstruct private void postConstruct() { - ensureSourceEncrypted(); } public void ensureSourceEncrypted() { - if (encryptorEnabled) { String query = "SELECT source_id, username, password FROM ${schema}.source".replaceAll("\\$\\{schema\\}", schema); String update = "UPDATE ${schema}.source SET username = ?, password = ? WHERE source_id = ?".replaceAll("\\$\\{schema\\}", schema); getTransactionTemplateRequiresNew().execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { - jdbcTemplate.query(query, rs -> { int id = rs.getInt("source_id"); String username = rs.getString("username"); @@ -98,31 +136,305 @@ protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) } } - @Cacheable(cacheNames = CachingSetup.SOURCE_LIST_CACHE) - public Collection getSources() { + // ==================== REST Endpoints ==================== - List sources = sourceRepository.findAll(); - Collections.sort(sources, new SortByKey()); - return sources; - } + /** + * Gets the list of all Sources in WebAPI database. Sources with a non-null + * deleted_date are not returned (ie: these are soft deleted) + * + * @summary Get Sources + * @return A list of all CDM sources with the ID, name, SQL dialect, and key + * for each source. The {sourceKey} is used in other WebAPI endpoints to + * identify CDMs. + */ + @GetMapping(value = "/sources", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getSourcesEndpoint() { + return ResponseEntity.ok(getSources().stream().map(SourceInfo::new).collect(Collectors.toList())); + } - public Source findBySourceKey(final String sourceKey) { + /** + * Refresh cached CDM database metadata + * + * @summary Refresh Sources + * @return A list of all CDM sources with the ID, name, SQL dialect, and key + * for each source (same as the 'sources' endpoint) after refreshing the cached sourced data. + */ + @GetMapping(value = "/refresh", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> refreshSources() { + invalidateCache(); + vocabularyService.clearVocabularyInfoCache(); + ensureSourceEncrypted(); + return getSourcesEndpoint(); + } + + /** + * Get the priority vocabulary source. + * + * WebAPI designates one CDM vocabulary as the priority vocabulary to be used for vocabulary searches in Atlas. + * + * @summary Get Priority Vocabulary Source + * @return The CDM metadata for the priority vocabulary. + */ + @GetMapping(value = "/priorityVocabulary", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getPriorityVocabularySourceInfoEndpoint() { + return ResponseEntity.ok(getPriorityVocabularySourceInfo()); + } + + /** + * Get source by key + * @summary Get Source By Key + * @param sourceKey + * @return Metadata for a single Source that matches the sourceKey. + */ + @GetMapping(value = "/{key}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getSource(@PathVariable("key") final String sourceKey) { + return ResponseEntity.ok(sourceRepository.findBySourceKey(sourceKey).getSourceInfo()); + } + + /** + * Get Source Details + * + * Source Details contains connection-specific information like JDBC url and authentication information. + * + * @summary Get Source Details + * @param sourceId + * @return + */ + @GetMapping(value = "/details/{sourceId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getSourceDetails(@PathVariable("sourceId") Integer sourceId) { + if (!securityEnabled) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, SECURE_MODE_ERROR); + } + Source source = sourceRepository.findBySourceId(sourceId); + return ResponseEntity.ok(new SourceDetails(source)); + } + /** + * Create a Source + * + * @summary Create Source + * @param keyfile the keyfile + * @param source contains the source information (name, key, etc) + * @return a new SourceInfo for the created source + * @throws Exception + */ + @PostMapping(value = "", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @CacheEvict(cacheNames = CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + public ResponseEntity createSource( + @RequestPart(value = "keyfile", required = false) MultipartFile keyfile, + @RequestPart("source") SourceRequest source) throws Exception { + if (!securityEnabled) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, SECURE_MODE_ERROR); + } + Source sourceByKey = sourceRepository.findBySourceKey(source.getKey()); + if (Objects.nonNull(sourceByKey)) { + throw new SourceDuplicateKeyException("The source key has been already used."); + } + Source sourceEntity = conversionService.convert(source, Source.class); + if (sourceEntity.getDaimons() != null) { + // First source should get priority = 1 + Iterable sources = sourceRepository.findAll(); + sourceEntity.getDaimons() + .stream() + .filter(sd -> sd.getPriority() <= 0) + .filter(sd -> { + boolean accept = true; + // Check if source daimon of given type with priority > 0 already exists in other sources + for (Source innerSource : sources) { + accept = !innerSource.getDaimons() + .stream() + .anyMatch(innerDaimon -> innerDaimon.getPriority() > 0 + && innerDaimon.getDaimonType().equals(sd.getDaimonType())); + if (!accept) { + break; + } + } + return accept; + }) + .forEach(sd -> sd.setPriority(1)); + } + Source original = new Source(); + original.setSourceDialect(sourceEntity.getSourceDialect()); + setKeyfileData(sourceEntity, original, keyfile); + sourceEntity.setCreatedBy(getCurrentUserEntity()); + sourceEntity.setCreatedDate(new Date()); + try { + Source saved = sourceRepository.saveAndFlush(sourceEntity); + invalidateCache(); + SourceInfo sourceInfo = new SourceInfo(saved); + publisher.publishEvent(new AddDataSourceEvent(this, sourceEntity.getSourceId(), sourceEntity.getSourceName())); + return ResponseEntity.ok(sourceInfo); + } catch (PersistenceException ex) { + throw new SourceDuplicateKeyException("You cannot use this Source Key, please use different one"); + } + } + + /** + * Updates a Source with the provided details from multiple files + * + * @summary Update Source + * @param sourceId + * @param keyfile the keyfile + * @param source contains the source information (name, key, etc) + * @return the updated SourceInfo for the source + * @throws Exception + */ + @PutMapping(value = "/{sourceId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional + @CacheEvict(cacheNames = CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + public ResponseEntity updateSource( + @PathVariable("sourceId") Integer sourceId, + @RequestPart(value = "keyfile", required = false) MultipartFile keyfile, + @RequestPart("source") SourceRequest source) throws IOException { + if (!securityEnabled) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, SECURE_MODE_ERROR); + } + Source updated = conversionService.convert(source, Source.class); + Source existingSource = sourceRepository.findBySourceId(sourceId); + if (existingSource != null) { + updated.setSourceId(sourceId); + updated.setSourceKey(existingSource.getSourceKey()); + if (StringUtils.isBlank(updated.getUsername()) || + Objects.equals(updated.getUsername().trim(), Source.MASQUERADED_USERNAME)) { + updated.setUsername(existingSource.getUsername()); + } + if (StringUtils.isBlank(updated.getPassword()) || + Objects.equals(updated.getPassword().trim(), Source.MASQUERADED_PASSWORD)) { + updated.setPassword(existingSource.getPassword()); + } + setKeyfileData(updated, existingSource, keyfile); + transformIfRequired(updated); + if (source.isCheckConnection() == null) { + updated.setCheckConnection(existingSource.isCheckConnection()); + } + updated.setModifiedBy(getCurrentUserEntity()); + updated.setModifiedDate(new Date()); + + reuseDeletedDaimons(updated, existingSource); + + List removed = existingSource.getDaimons().stream() + .filter(d -> !updated.getDaimons().contains(d)) + .collect(Collectors.toList()); + // Delete MUST be called after fetching user or source data to prevent autoflush (see DefaultPersistEventListener.onPersist) + sourceDaimonRepository.deleteAll(removed); + Source result = sourceRepository.save(updated); + publisher.publishEvent(new ChangeDataSourceEvent(this, updated.getSourceId(), updated.getSourceName())); + invalidateCache(); + return ResponseEntity.ok(new SourceInfo(result)); + } else { + return ResponseEntity.notFound().build(); + } + } + + /** + * Delete a source. + * + * @summary Delete Source + * @param sourceId + * @return + * @throws Exception + */ + @DeleteMapping("/{sourceId}") + @Transactional + @CacheEvict(cacheNames = CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + public ResponseEntity delete(@PathVariable("sourceId") Integer sourceId) throws Exception { + if (!securityEnabled) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + Source source = sourceRepository.findBySourceId(sourceId); + if (source != null) { + sourceRepository.delete(source); + publisher.publishEvent(new DeleteDataSourceEvent(this, sourceId, source.getSourceName())); + invalidateCache(); + return ResponseEntity.ok().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + /** + * Check source connection. + * + * This method attempts to connect to the source by calling 'select 1' on the source connection. + * @summary Check connection + * @param sourceKey + * @return + */ + @GetMapping(value = "/connection/{key}", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional(noRollbackFor = CannotGetJdbcConnectionException.class) + public ResponseEntity checkConnectionEndpoint(@PathVariable("key") final String sourceKey) { + final Source source = findBySourceKey(sourceKey); + checkConnection(source); + return ResponseEntity.ok(source.getSourceInfo()); + } + + /** + * Get the first daimon (and associated source) that has priority. In the event + * of a tie, the first source searched wins. + * + * @summary Get Priority Daimons + * @return + */ + @GetMapping(value = "/daimon/priority", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getPriorityDaimonsEndpoint() { + return ResponseEntity.ok(getPriorityDaimons() + .entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> new SourceInfo(e.getValue()) + ))); + } + + /** + * Set priority of daimon + * + * Set the priority of the specified daimon of the specified source, and set the other daimons to 0. + * @summary Set Priority + * @param sourceKey + * @param daimonTypeName + * @return + */ + @PostMapping(value = "/{sourceKey}/daimons/{daimonType}/set-priority", produces = MediaType.APPLICATION_JSON_VALUE) + @CacheEvict(cacheNames = CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + public ResponseEntity updateSourcePriority( + @PathVariable("sourceKey") final String sourceKey, + @PathVariable("daimonType") final String daimonTypeName) { + if (!securityEnabled) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + SourceDaimon.DaimonType daimonType = SourceDaimon.DaimonType.valueOf(daimonTypeName); + List daimonList = sourceDaimonRepository.findByDaimonType(daimonType); + daimonList.forEach(daimon -> { + Integer newPriority = daimon.getSource().getSourceKey().equals(sourceKey) ? 1 : 0; + daimon.setPriority(newPriority); + sourceDaimonRepository.save(daimon); + }); + invalidateCache(); + return ResponseEntity.ok().build(); + } + + // ==================== Service Methods ==================== + + @Cacheable(cacheNames = CachingSetup.SOURCE_LIST_CACHE) + public Collection getSources() { + List sources = sourceRepository.findAll(); + Collections.sort(sources, new SortByKey()); + return sources; + } + + public Source findBySourceKey(final String sourceKey) { return sourceRepository.findBySourceKey(sourceKey); } public Source findBySourceId(final Integer sourceId) { - return sourceRepository.findBySourceId(sourceId); } public Map getSourcesMap(SourceMapKey mapKey) { - return getSources().stream().collect(Collectors.toMap(mapKey.getKeyFunc(), s -> s)); } public void checkConnection(Source source) { - if (source.isCheckConnection()) { forceCheckConnection(source); } @@ -138,7 +450,6 @@ public void forceCheckConnection(Source source) { } public Source getPrioritySourceForDaimon(SourceDaimon.DaimonType daimonType) { - List sourcesByDaimonPriority = sourceRepository.findAllSortedByDiamonPrioirty(daimonType); for (Source source : sourcesByDaimonPriority) { @@ -152,7 +463,6 @@ public Source getPrioritySourceForDaimon(SourceDaimon.DaimonType daimonType) { } public Map getPriorityDaimons() { - class SourceValidator { private Map checkedSources = new HashMap<>(); @@ -165,7 +475,6 @@ private boolean isSourceAvaialble(Source source) { SourceValidator sourceValidator = new SourceValidator(); Map priorityDaimons = new HashMap<>(); Arrays.asList(SourceDaimon.DaimonType.values()).forEach(d -> { - List sources = sourceRepository.findAllSortedByDiamonPrioirty(d); Optional source = sources.stream().filter(sourceValidator::isSourceAvaialble) .findFirst(); @@ -175,7 +484,6 @@ private boolean isSourceAvaialble(Source source) { } public Source getPriorityVocabularySource() { - return getPrioritySourceForDaimon(SourceDaimon.DaimonType.Vocabulary); } @@ -187,12 +495,64 @@ public SourceInfo getPriorityVocabularySourceInfo() { return new SourceInfo(source); } - @CacheEvict(cacheNames = CachingSetup.SOURCE_LIST_CACHE, allEntries = true) - public void invalidateCache() { - } + @CacheEvict(cacheNames = CachingSetup.SOURCE_LIST_CACHE, allEntries = true) + public void invalidateCache() { + } - private boolean checkConnectionSafe(Source source) { + // ==================== Private Helper Methods ==================== + + protected UserEntity getCurrentUserEntity() { + return userRepository.findByLogin(security.getSubject()); + } + + private void reuseDeletedDaimons(Source updated, Source source) { + List daimons = updated.getDaimons().stream().filter(d -> source.getDaimons().contains(d)) + .collect(Collectors.toList()); + List newDaimons = updated.getDaimons().stream().filter(d -> !source.getDaimons().contains(d)) + .collect(Collectors.toList()); + + List allDaimons = sourceDaimonRepository.findBySource(source); + + for (SourceDaimon newSourceDaimon : newDaimons) { + Optional reusedDaimonOpt = allDaimons.stream() + .filter(d -> d.equals(newSourceDaimon)) + .findFirst(); + if (reusedDaimonOpt.isPresent()) { + SourceDaimon reusedDaimon = reusedDaimonOpt.get(); + reusedDaimon.setPriority(newSourceDaimon.getPriority()); + reusedDaimon.setTableQualifier(newSourceDaimon.getTableQualifier()); + daimons.add(reusedDaimon); + } else { + daimons.add(newSourceDaimon); + } + } + updated.setDaimons(daimons); + } + + private void transformIfRequired(Source source) { + if (DBMSType.BIGQUERY.getOhdsiDB().equals(source.getSourceDialect()) && ArrayUtils.isNotEmpty(source.getKeyfile())) { + String connStr = source.getSourceConnection().replaceAll("OAuthPvtKeyPath=.+?(;|\\z)", ""); + source.setSourceConnection(connStr); + } + } + private void setKeyfileData(Source updated, Source source, MultipartFile file) throws IOException { + if (source.supportsKeyfile()) { + if (updated.getKeyfileName() != null) { + if (!Objects.equals(updated.getKeyfileName(), source.getKeyfileName())) { + byte[] fileBytes = file != null ? file.getBytes() : new byte[0]; + updated.setKeyfile(fileBytes); + } else { + updated.setKeyfile(source.getKeyfile()); + } + return; + } + } + updated.setKeyfile(null); + updated.setKeyfileName(null); + } + + private boolean checkConnectionSafe(Source source) { try { checkConnection(source); return true; @@ -205,17 +565,14 @@ private class SortByKey implements Comparator { private boolean isAscending; public SortByKey(boolean ascending) { - isAscending = ascending; } public SortByKey() { - this(true); } public int compare(Source s1, Source s2) { - return s1.getSourceKey().compareTo(s2.getSourceKey()) * (isAscending ? 1 : -1); } } diff --git a/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticMvcController.java b/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticMvcController.java deleted file mode 100644 index c31abd2443..0000000000 --- a/src/main/java/org/ohdsi/webapi/statistic/controller/StatisticMvcController.java +++ /dev/null @@ -1,291 +0,0 @@ -package org.ohdsi.webapi.statistic.controller; - -import com.opencsv.CSVWriter; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.statistic.dto.AccessTrendDto; -import org.ohdsi.webapi.statistic.dto.AccessTrendsDto; -import org.ohdsi.webapi.statistic.dto.EndpointDto; -import org.ohdsi.webapi.statistic.dto.SourceExecutionDto; -import org.ohdsi.webapi.statistic.dto.SourceExecutionsDto; -import org.ohdsi.webapi.statistic.service.StatisticService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; - -import java.io.ByteArrayOutputStream; -import java.io.StringWriter; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Spring MVC version of StatisticController - * - * Migration Status: Replaces /statistic/controller/StatisticController.java (Jersey) - * Endpoints: 2 POST endpoints - * Complexity: Medium - statistics with CSV generation - */ -@RestController -@RequestMapping("/statistic") -public class StatisticMvcController extends AbstractMvcController { - - private static final Logger log = LoggerFactory.getLogger(StatisticMvcController.class); - - private final StatisticService service; - - @Value("${audit.trail.enabled}") - private boolean auditTrailEnabled; - - public enum ResponseFormat { - CSV, JSON - } - - private static final List EXECUTION_STATISTICS_CSV_RESULT_HEADER = new ArrayList() {{ - add(new String[]{"Date", "Source", "Execution Type"}); - }}; - - private static final List EXECUTION_STATISTICS_CSV_RESULT_HEADER_WITH_USER_ID = new ArrayList() {{ - add(new String[]{"Date", "Source", "Execution Type", "User ID"}); - }}; - - private static final List ACCESS_TRENDS_CSV_RESULT_HEADER = new ArrayList() {{ - add(new String[]{"Date", "Endpoint"}); - }}; - - private static final List ACCESS_TRENDS_CSV_RESULT_HEADER_WITH_USER_ID = new ArrayList() {{ - add(new String[]{"Date", "Endpoint", "User ID"}); - }}; - - @Autowired - public StatisticMvcController(StatisticService service) { - this.service = service; - } - - /** - * Returns execution statistics - * - * Jersey: POST /WebAPI/statistic/executions - * Spring MVC: POST /WebAPI/v2/statistic/executions - * - * @param executionStatisticsRequest filter settings for statistics - * @return execution statistics in JSON or CSV format - */ - @PostMapping( - value = "/executions", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity executionStatistics(@RequestBody ExecutionStatisticsRequest executionStatisticsRequest) { - if (!auditTrailEnabled) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, - "Audit Trail functionality should be enabled (audit.trail.enabled) to serve this endpoint"); - } - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - boolean showUserInformation = executionStatisticsRequest.isShowUserInformation(); - - SourceExecutionsDto sourceExecutions = service.getSourceExecutions( - LocalDate.parse(executionStatisticsRequest.getStartDate(), formatter), - LocalDate.parse(executionStatisticsRequest.getEndDate(), formatter), - executionStatisticsRequest.getSourceKey(), - showUserInformation); - - if (ResponseFormat.CSV.equals(executionStatisticsRequest.getResponseFormat())) { - return prepareExecutionResultResponse(sourceExecutions.getExecutions(), "execution_statistics.zip", showUserInformation); - } else { - return ok(sourceExecutions); - } - } - - /** - * Returns access trends statistics - * - * Jersey: POST /WebAPI/statistic/accesstrends - * Spring MVC: POST /WebAPI/v2/statistic/accesstrends - * - * @param accessTrendsStatisticsRequest filter settings for statistics - * @return access trends statistics in JSON or CSV format - */ - @PostMapping( - value = "/accesstrends", - produces = MediaType.APPLICATION_JSON_VALUE, - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity accessStatistics(@RequestBody AccessTrendsStatisticsRequest accessTrendsStatisticsRequest) { - if (!auditTrailEnabled) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, - "Audit Trail functionality should be enabled (audit.trail.enabled) to serve this endpoint"); - } - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - boolean showUserInformation = accessTrendsStatisticsRequest.isShowUserInformation(); - - AccessTrendsDto trends = service.getAccessTrends( - LocalDate.parse(accessTrendsStatisticsRequest.getStartDate(), formatter), - LocalDate.parse(accessTrendsStatisticsRequest.getEndDate(), formatter), - accessTrendsStatisticsRequest.getEndpoints(), - showUserInformation); - - if (ResponseFormat.CSV.equals(accessTrendsStatisticsRequest.getResponseFormat())) { - return prepareAccessTrendsResponse(trends.getTrends(), "execution_trends.zip", showUserInformation); - } else { - return ok(trends); - } - } - - private ResponseEntity prepareExecutionResultResponse(List executions, String filename, boolean showUserInformation) { - List data = executions.stream() - .map(execution -> showUserInformation - ? new String[]{execution.getExecutionDate(), execution.getSourceName(), execution.getExecutionName(), execution.getUserId()} - : new String[]{execution.getExecutionDate(), execution.getSourceName(), execution.getExecutionName()} - ) - .collect(Collectors.toList()); - return prepareResponse(data, filename, showUserInformation ? EXECUTION_STATISTICS_CSV_RESULT_HEADER_WITH_USER_ID : EXECUTION_STATISTICS_CSV_RESULT_HEADER); - } - - private ResponseEntity prepareAccessTrendsResponse(List trends, String filename, boolean showUserInformation) { - List data = trends.stream() - .map(trend -> showUserInformation - ? new String[]{trend.getExecutionDate().toString(), trend.getEndpointName(), trend.getUserID()} - : new String[]{trend.getExecutionDate().toString(), trend.getEndpointName()} - ) - .collect(Collectors.toList()); - return prepareResponse(data, filename, showUserInformation ? ACCESS_TRENDS_CSV_RESULT_HEADER_WITH_USER_ID : ACCESS_TRENDS_CSV_RESULT_HEADER); - } - - private ResponseEntity prepareResponse(List data, String filename, List header) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - StringWriter sw = new StringWriter(); - CSVWriter csvWriter = new CSVWriter(sw, ',', CSVWriter.DEFAULT_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER)) { - - csvWriter.writeAll(header); - csvWriter.writeAll(data); - csvWriter.flush(); - baos.write(sw.getBuffer().toString().getBytes()); - - ByteArrayResource resource = new ByteArrayResource(baos.toByteArray()); - - return ResponseEntity.ok() - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", filename)) - .body(resource); - } catch (Exception ex) { - log.error("An error occurred while building a response", ex); - throw new RuntimeException(ex); - } - } - - public static final class ExecutionStatisticsRequest { - // Format - yyyy-MM-dd - String startDate; - // Format - yyyy-MM-dd - String endDate; - String sourceKey; - ResponseFormat responseFormat; - boolean showUserInformation; - - public String getStartDate() { - return startDate; - } - - public void setStartDate(String startDate) { - this.startDate = startDate; - } - - public String getEndDate() { - return endDate; - } - - public void setEndDate(String endDate) { - this.endDate = endDate; - } - - public String getSourceKey() { - return sourceKey; - } - - public void setSourceKey(String sourceKey) { - this.sourceKey = sourceKey; - } - - public ResponseFormat getResponseFormat() { - return responseFormat; - } - - public void setResponseFormat(ResponseFormat responseFormat) { - this.responseFormat = responseFormat; - } - - public boolean isShowUserInformation() { - return showUserInformation; - } - - public void setShowUserInformation(boolean showUserInformation) { - this.showUserInformation = showUserInformation; - } - } - - public static final class AccessTrendsStatisticsRequest { - // Format - yyyy-MM-dd - String startDate; - // Format - yyyy-MM-dd - String endDate; - // Key - method (POST, GET) - // Value - endpoint ("{}" can be used as a placeholder, will be converted to ".*" in regular expression) - List endpoints; - ResponseFormat responseFormat; - boolean showUserInformation; - - public String getStartDate() { - return startDate; - } - - public void setStartDate(String startDate) { - this.startDate = startDate; - } - - public String getEndDate() { - return endDate; - } - - public void setEndDate(String endDate) { - this.endDate = endDate; - } - - public List getEndpoints() { - return endpoints; - } - - public void setEndpoints(List endpoints) { - this.endpoints = endpoints; - } - - public ResponseFormat getResponseFormat() { - return responseFormat; - } - - public void setResponseFormat(ResponseFormat responseFormat) { - this.responseFormat = responseFormat; - } - - public boolean isShowUserInformation() { - return showUserInformation; - } - - public void setShowUserInformation(boolean showUserInformation) { - this.showUserInformation = showUserInformation; - } - } -} diff --git a/src/main/java/org/ohdsi/webapi/statistic/service/StatisticService.java b/src/main/java/org/ohdsi/webapi/statistic/service/StatisticService.java index f368db8a59..ee674f2b64 100644 --- a/src/main/java/org/ohdsi/webapi/statistic/service/StatisticService.java +++ b/src/main/java/org/ohdsi/webapi/statistic/service/StatisticService.java @@ -1,5 +1,6 @@ package org.ohdsi.webapi.statistic.service; +import com.opencsv.CSVWriter; import org.apache.commons.lang3.tuple.ImmutablePair; import org.ohdsi.webapi.statistic.dto.AccessTrendDto; import org.ohdsi.webapi.statistic.dto.AccessTrendsDto; @@ -9,10 +10,22 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.StringWriter; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -20,6 +33,8 @@ import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,15 +44,45 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -@Service +/** + * Spring MVC service for statistics + * + * Endpoints: 2 POST endpoints + * Complexity: Medium - statistics with CSV generation + */ +@RestController +@RequestMapping("/statistic") public class StatisticService { protected final Logger LOG = LoggerFactory.getLogger(getClass()); + @Value("${audit.trail.enabled}") + private boolean auditTrailEnabled; + @Value("${audit.trail.log.file}") private String absoluteLogFileName = "/tmp/atlas/audit/audit.log"; private String logFileName; + public enum ResponseFormat { + CSV, JSON + } + + private static final List EXECUTION_STATISTICS_CSV_RESULT_HEADER = new ArrayList() {{ + add(new String[]{"Date", "Source", "Execution Type"}); + }}; + + private static final List EXECUTION_STATISTICS_CSV_RESULT_HEADER_WITH_USER_ID = new ArrayList() {{ + add(new String[]{"Date", "Source", "Execution Type", "User ID"}); + }}; + + private static final List ACCESS_TRENDS_CSV_RESULT_HEADER = new ArrayList() {{ + add(new String[]{"Date", "Endpoint"}); + }}; + + private static final List ACCESS_TRENDS_CSV_RESULT_HEADER_WITH_USER_ID = new ArrayList() {{ + add(new String[]{"Date", "Endpoint", "User ID"}); + }}; + @Value("${audit.trail.log.file.pattern}") private String absoluteLogFileNamePattern = "/tmp/atlas/audit/audit-%d{yyyy-MM-dd}-%i.log"; @@ -223,4 +268,216 @@ private LocalDate getFileDate(Path path) { return LocalDate.now(); } } + + // REST Endpoints + + /** + * Returns execution statistics + * + * @param executionStatisticsRequest filter settings for statistics + * @return execution statistics in JSON or CSV format + */ + @PostMapping( + value = "/executions", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity executionStatistics(@RequestBody ExecutionStatisticsRequest executionStatisticsRequest) { + if (!auditTrailEnabled) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, + "Audit Trail functionality should be enabled (audit.trail.enabled) to serve this endpoint"); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + boolean showUserInformation = executionStatisticsRequest.isShowUserInformation(); + + SourceExecutionsDto sourceExecutions = getSourceExecutions( + LocalDate.parse(executionStatisticsRequest.getStartDate(), formatter), + LocalDate.parse(executionStatisticsRequest.getEndDate(), formatter), + executionStatisticsRequest.getSourceKey(), + showUserInformation); + + if (ResponseFormat.CSV.equals(executionStatisticsRequest.getResponseFormat())) { + return prepareExecutionResultResponse(sourceExecutions.getExecutions(), "execution_statistics.zip", showUserInformation); + } else { + return ResponseEntity.ok(sourceExecutions); + } + } + + /** + * Returns access trends statistics + * + * @param accessTrendsStatisticsRequest filter settings for statistics + * @return access trends statistics in JSON or CSV format + */ + @PostMapping( + value = "/accesstrends", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity accessStatistics(@RequestBody AccessTrendsStatisticsRequest accessTrendsStatisticsRequest) { + if (!auditTrailEnabled) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, + "Audit Trail functionality should be enabled (audit.trail.enabled) to serve this endpoint"); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + boolean showUserInformation = accessTrendsStatisticsRequest.isShowUserInformation(); + + AccessTrendsDto trends = getAccessTrends( + LocalDate.parse(accessTrendsStatisticsRequest.getStartDate(), formatter), + LocalDate.parse(accessTrendsStatisticsRequest.getEndDate(), formatter), + accessTrendsStatisticsRequest.getEndpoints(), + showUserInformation); + + if (ResponseFormat.CSV.equals(accessTrendsStatisticsRequest.getResponseFormat())) { + return prepareAccessTrendsResponse(trends.getTrends(), "execution_trends.zip", showUserInformation); + } else { + return ResponseEntity.ok(trends); + } + } + + private ResponseEntity prepareExecutionResultResponse(List executions, String filename, boolean showUserInformation) { + List data = executions.stream() + .map(execution -> showUserInformation + ? new String[]{execution.getExecutionDate(), execution.getSourceName(), execution.getExecutionName(), execution.getUserId()} + : new String[]{execution.getExecutionDate(), execution.getSourceName(), execution.getExecutionName()} + ) + .collect(Collectors.toList()); + return prepareResponse(data, filename, showUserInformation ? EXECUTION_STATISTICS_CSV_RESULT_HEADER_WITH_USER_ID : EXECUTION_STATISTICS_CSV_RESULT_HEADER); + } + + private ResponseEntity prepareAccessTrendsResponse(List trends, String filename, boolean showUserInformation) { + List data = trends.stream() + .map(trend -> showUserInformation + ? new String[]{trend.getExecutionDate().toString(), trend.getEndpointName(), trend.getUserID()} + : new String[]{trend.getExecutionDate().toString(), trend.getEndpointName()} + ) + .collect(Collectors.toList()); + return prepareResponse(data, filename, showUserInformation ? ACCESS_TRENDS_CSV_RESULT_HEADER_WITH_USER_ID : ACCESS_TRENDS_CSV_RESULT_HEADER); + } + + private ResponseEntity prepareResponse(List data, String filename, List header) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + StringWriter sw = new StringWriter(); + CSVWriter csvWriter = new CSVWriter(sw, ',', CSVWriter.DEFAULT_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER)) { + + csvWriter.writeAll(header); + csvWriter.writeAll(data); + csvWriter.flush(); + baos.write(sw.getBuffer().toString().getBytes()); + + ByteArrayResource resource = new ByteArrayResource(baos.toByteArray()); + + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", filename)) + .body(resource); + } catch (Exception ex) { + LOG.error("An error occurred while building a response", ex); + throw new RuntimeException(ex); + } + } + + // Request DTOs + + public static final class ExecutionStatisticsRequest { + // Format - yyyy-MM-dd + String startDate; + // Format - yyyy-MM-dd + String endDate; + String sourceKey; + ResponseFormat responseFormat; + boolean showUserInformation; + + public String getStartDate() { + return startDate; + } + + public void setStartDate(String startDate) { + this.startDate = startDate; + } + + public String getEndDate() { + return endDate; + } + + public void setEndDate(String endDate) { + this.endDate = endDate; + } + + public String getSourceKey() { + return sourceKey; + } + + public void setSourceKey(String sourceKey) { + this.sourceKey = sourceKey; + } + + public ResponseFormat getResponseFormat() { + return responseFormat; + } + + public void setResponseFormat(ResponseFormat responseFormat) { + this.responseFormat = responseFormat; + } + + public boolean isShowUserInformation() { + return showUserInformation; + } + + public void setShowUserInformation(boolean showUserInformation) { + this.showUserInformation = showUserInformation; + } + } + + public static final class AccessTrendsStatisticsRequest { + // Format - yyyy-MM-dd + String startDate; + // Format - yyyy-MM-dd + String endDate; + // Key - method (POST, GET) + // Value - endpoint ("{}" can be used as a placeholder, will be converted to ".*" in regular expression) + List endpoints; + ResponseFormat responseFormat; + boolean showUserInformation; + + public String getStartDate() { + return startDate; + } + + public void setStartDate(String startDate) { + this.startDate = startDate; + } + + public String getEndDate() { + return endDate; + } + + public void setEndDate(String endDate) { + this.endDate = endDate; + } + + public List getEndpoints() { + return endpoints; + } + + public void setEndpoints(List endpoints) { + this.endpoints = endpoints; + } + + public ResponseFormat getResponseFormat() { + return responseFormat; + } + + public void setResponseFormat(ResponseFormat responseFormat) { + this.responseFormat = responseFormat; + } + + public boolean isShowUserInformation() { + return showUserInformation; + } + + public void setShowUserInformation(boolean showUserInformation) { + this.showUserInformation = showUserInformation; + } + } } diff --git a/src/main/java/org/ohdsi/webapi/tag/TagService.java b/src/main/java/org/ohdsi/webapi/tag/TagService.java index e786a9bacc..1be1afbdad 100644 --- a/src/main/java/org/ohdsi/webapi/tag/TagService.java +++ b/src/main/java/org/ohdsi/webapi/tag/TagService.java @@ -1,35 +1,39 @@ package org.ohdsi.webapi.tag; -import org.apache.shiro.SecurityUtils; +import org.apache.commons.lang3.StringUtils; import org.ohdsi.webapi.service.AbstractDaoService; import org.ohdsi.webapi.tag.domain.Tag; import org.ohdsi.webapi.tag.domain.TagInfo; import org.ohdsi.webapi.tag.domain.TagType; import org.ohdsi.webapi.tag.dto.TagDTO; import org.ohdsi.webapi.tag.dto.AssignmentPermissionsDTO; +import org.ohdsi.webapi.tag.dto.TagGroupSubscriptionDTO; import org.ohdsi.webapi.tag.repository.TagRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Lazy; import org.springframework.core.convert.ConversionService; +import org.springframework.http.MediaType; import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; import jakarta.persistence.EntityManager; import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.Optional; -@Service +@RestController +@RequestMapping("/tag") @Transactional public class TagService extends AbstractDaoService { private static final Logger logger = LoggerFactory.getLogger(TagService.class); private final TagRepository tagRepository; private final EntityManager entityManager; private final ConversionService conversionService; + private final TagGroupService tagGroupService; private final ArrayList>> infoProducers; @@ -37,10 +41,12 @@ public class TagService extends AbstractDaoService { public TagService( TagRepository tagRepository, EntityManager entityManager, - @Qualifier("conversionService") ConversionService conversionService) { + @Qualifier("conversionService") ConversionService conversionService, + @Lazy TagGroupService tagGroupService) { this.tagRepository = tagRepository; this.entityManager = entityManager; this.conversionService = conversionService; + this.tagGroupService = tagGroupService; this.infoProducers = new ArrayList<>(); this.infoProducers.add(tagRepository::findCohortTagInfo); @@ -48,7 +54,14 @@ public TagService( this.infoProducers.add(tagRepository::findReusableTagInfo); } - public TagDTO create(TagDTO dto) { + /** + * Creates a tag. + * + * @param dto + * @return + */ + @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public TagDTO create(@RequestBody TagDTO dto) { Tag tag = conversionService.convert(dto, Tag.class); Tag saved = create(tag); return conversionService.convert(saved, TagDTO.class); @@ -79,17 +92,41 @@ public Tag getById(Integer id) { return tagRepository.findById(id).orElse(null); } - public TagDTO getDTOById(Integer id) { + /** + * Return tag by ID. + * + * @param id + * @return + */ + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public TagDTO getDTOById(@PathVariable("id") Integer id) { Tag tag = tagRepository.findById(id).orElse(null); return conversionService.convert(tag, TagDTO.class); } - public List listInfoDTO(String namePart) { + /** + * Returns list of tags, which names contain a provided substring. + * + * @summary Search tags by name part + * @param namePart + * @return + */ + @GetMapping(value = "/search", produces = MediaType.APPLICATION_JSON_VALUE) + public List listInfoDTO(@RequestParam("namePart") String namePart) { + if (StringUtils.isBlank(namePart)) { + return Collections.emptyList(); + } return listInfo(namePart).stream() .map(tag -> conversionService.convert(tag, TagDTO.class)) .collect(Collectors.toList()); } + /** + * Returns list of all tags. + * + * @return + */ + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) public List listInfoDTO() { return listInfo().stream() .map(tag -> conversionService.convert(tag, TagDTO.class)) @@ -108,7 +145,15 @@ public List findByIdIn(List ids) { return tagRepository.findByIdIn(ids); } - public TagDTO update(Integer id, TagDTO entity) { + /** + * Updates tag with ID={id}. + * + * @param id + * @param entity + * @return + */ + @PutMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public TagDTO update(@PathVariable("id") Integer id, @RequestBody TagDTO entity) { Tag existing = tagRepository.findById(id).orElse(null); checkOwnerOrAdmin(existing.getCreatedBy()); @@ -130,7 +175,13 @@ public TagDTO update(Integer id, TagDTO entity) { return conversionService.convert(saved, TagDTO.class); } - public void delete(Integer id) { + /** + * Deletes tag with ID={id}. + * + * @param id + */ + @DeleteMapping(value = "/{id}") + public void delete(@PathVariable("id") Integer id) { Tag existing = tagRepository.findById(id).orElse(null); checkOwnerOrAdmin(existing.getCreatedBy()); @@ -207,6 +258,12 @@ private void findParentGroup(Set groups, Set groupIds) { }); } + /** + * Tags assignment permissions for current user + * + * @return + */ + @GetMapping(value = "/assignmentPermissions", produces = MediaType.APPLICATION_JSON_VALUE) public AssignmentPermissionsDTO getAssignmentPermissions() { final AssignmentPermissionsDTO tagPermission = new AssignmentPermissionsDTO(); tagPermission.setAnyAssetMultiAssignPermitted(isAdmin()); @@ -214,4 +271,24 @@ public AssignmentPermissionsDTO getAssignmentPermissions() { tagPermission.setCanUnassignProtectedTags(!isSecured() || TagSecurityUtils.canUnassingProtectedTags()); return tagPermission; } + + /** + * Assigns group of tags to groups of assets. + * + * @param dto + */ + @PostMapping(value = "/multiAssign", consumes = MediaType.APPLICATION_JSON_VALUE) + public void assignGroup(@RequestBody TagGroupSubscriptionDTO dto) { + tagGroupService.assignGroup(dto); + } + + /** + * Unassigns group of tags from groups of assets. + * + * @param dto + */ + @PostMapping(value = "/multiUnassign", consumes = MediaType.APPLICATION_JSON_VALUE) + public void unassignGroup(@RequestBody TagGroupSubscriptionDTO dto) { + tagGroupService.unassignGroup(dto); + } } diff --git a/src/main/java/org/ohdsi/webapi/tool/ToolServiceImpl.java b/src/main/java/org/ohdsi/webapi/tool/ToolServiceImpl.java index cdad88fa6d..07cce9bd92 100644 --- a/src/main/java/org/ohdsi/webapi/tool/ToolServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/tool/ToolServiceImpl.java @@ -12,9 +12,18 @@ import org.ohdsi.webapi.service.AbstractDaoService; import org.ohdsi.webapi.shiro.Entities.UserEntity; import org.ohdsi.webapi.tool.dto.ToolDTO; -import org.springframework.stereotype.Service; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; -@Service +@RestController +@RequestMapping("/tool") public class ToolServiceImpl extends AbstractDaoService implements ToolService { private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; @@ -25,6 +34,7 @@ public ToolServiceImpl(ToolRepository toolRepository) { } @Override + @GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE) public List getTools() { List tools = (isAdmin() || canManageTools()) ? toolRepository.findAll() : toolRepository.findAllByEnabled(true); return tools.stream() @@ -32,7 +42,8 @@ public List getTools() { } @Override - public ToolDTO saveTool(ToolDTO toolDTO) { + @PostMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ToolDTO saveTool(@RequestBody ToolDTO toolDTO) { Tool tool = saveToolFromDTO(toolDTO, getCurrentUser()); return toDTO(toolRepository.saveAndFlush(tool)); } @@ -47,15 +58,22 @@ private Tool saveToolFromDTO(ToolDTO toolDTO, UserEntity currentUser) { } @Override - public ToolDTO getById(Integer id) { + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ToolDTO getById(@PathVariable("id") Integer id) { return toDTO(toolRepository.findById(id).orElse(null)); } @Override - public void delete(Integer id) { + @DeleteMapping(value = "/{id}") + public void delete(@PathVariable("id") Integer id) { toolRepository.deleteById(id); } + @PutMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + public ToolDTO updateTool(@RequestBody ToolDTO toolDTO) { + return saveTool(toolDTO); + } + private boolean canManageTools() { return Stream.of("tool:put", "tool:post", "tool:*:delete") .allMatch(permission -> SecurityUtils.getSubject().isPermitted(permission)); diff --git a/src/main/java/org/ohdsi/webapi/user/importer/UserImportJobMvcController.java b/src/main/java/org/ohdsi/webapi/user/importer/UserImportJobMvcController.java deleted file mode 100644 index 47ea4367af..0000000000 --- a/src/main/java/org/ohdsi/webapi/user/importer/UserImportJobMvcController.java +++ /dev/null @@ -1,186 +0,0 @@ -package org.ohdsi.webapi.user.importer; - -import org.ohdsi.webapi.arachne.scheduler.exception.JobNotFoundException; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.user.importer.dto.JobHistoryItemDTO; -import org.ohdsi.webapi.user.importer.dto.UserImportJobDTO; -import org.ohdsi.webapi.user.importer.exception.JobAlreadyExistException; -import org.ohdsi.webapi.user.importer.model.UserImportJob; -import org.ohdsi.webapi.user.importer.service.UserImportJobService; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; - -import java.util.List; -import java.util.stream.Collectors; - -import static org.ohdsi.webapi.Constants.JOB_IS_ALREADY_SCHEDULED; - -/** - * REST Services related to importing user information - * from an external source (i.e. Active Directory) - * - * Spring MVC version of UserImportJobController - * - * Migration Status: Replaces /user/importer/UserImportJobController.java (Jersey) - * Endpoints: 5 endpoints (GET, POST, PUT, DELETE) - * Complexity: Medium - user import job management with history - */ -@RestController -@RequestMapping("/user/import/job") -@Transactional -public class UserImportJobMvcController extends AbstractMvcController { - - private final UserImportJobService jobService; - private final GenericConversionService conversionService; - - public UserImportJobMvcController( - UserImportJobService jobService, - @Qualifier("conversionService") GenericConversionService conversionService) { - this.jobService = jobService; - this.conversionService = conversionService; - } - - /** - * Create a user import job - * - * Jersey: POST /WebAPI/user/import/job/ - * Spring MVC: POST /WebAPI/v2/user/import/job/ - * - * @param jobDTO The user import information - * @return The job information - */ - @PostMapping( - value = "/", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity createJob(@RequestBody UserImportJobDTO jobDTO) { - UserImportJob job = conversionService.convert(jobDTO, UserImportJob.class); - try { - UserImportJob created = jobService.createJob(job); - return ok(conversionService.convert(created, UserImportJobDTO.class)); - } catch (JobAlreadyExistException e) { - throw new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, - String.format(JOB_IS_ALREADY_SCHEDULED, job.getProviderType())); - } - } - - /** - * Update a user import job - * - * Jersey: PUT /WebAPI/user/import/job/{id} - * Spring MVC: PUT /WebAPI/v2/user/import/job/{id} - * - * @param jobId The job ID - * @param jobDTO The user import information - * @return The job information - */ - @PutMapping( - value = "/{id}", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity updateJob( - @PathVariable("id") Long jobId, - @RequestBody UserImportJobDTO jobDTO) { - UserImportJob job = conversionService.convert(jobDTO, UserImportJob.class); - try { - job.setId(jobId); - UserImportJob updated = jobService.updateJob(job); - return ok(conversionService.convert(updated, UserImportJobDTO.class)); - } catch (JobNotFoundException e) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND); - } - } - - /** - * Get the user import job list - * - * Jersey: GET /WebAPI/user/import/job/ - * Spring MVC: GET /WebAPI/v2/user/import/job/ - * - * @return The list of user import jobs - */ - @GetMapping( - value = "/", - produces = MediaType.APPLICATION_JSON_VALUE - ) - @Transactional - public ResponseEntity> listJobs() { - return ok(jobService.getJobs().stream() - .map(job -> conversionService.convert(job, UserImportJobDTO.class)) - .peek(job -> jobService.getLatestHistoryItem(job.getId()) - .ifPresent(item -> job.setLastExecuted(item.getEndTime()))) - .collect(Collectors.toList())); - } - - /** - * Get user import job by ID - * - * Jersey: GET /WebAPI/user/import/job/{id} - * Spring MVC: GET /WebAPI/v2/user/import/job/{id} - * - * @param id The job ID - * @return The user import job - */ - @GetMapping( - value = "/{id}", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity getJob(@PathVariable("id") Long id) { - return jobService.getJob(id) - .map(job -> conversionService.convert(job, UserImportJobDTO.class)) - .map(this::ok) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - } - - /** - * Delete user import job by ID - * - * Jersey: DELETE /WebAPI/user/import/job/{id} - * Spring MVC: DELETE /WebAPI/v2/user/import/job/{id} - * - * @param id The job ID - */ - @DeleteMapping( - value = "/{id}" - ) - public ResponseEntity deleteJob(@PathVariable("id") Long id) { - UserImportJob job = jobService.getJob(id) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - jobService.delete(job); - return ok(); - } - - /** - * Get the user import job history - * - * Jersey: GET /WebAPI/user/import/job/{id}/history - * Spring MVC: GET /WebAPI/v2/user/import/job/{id}/history - * - * @param id The job ID - * @return The job history - */ - @GetMapping( - value = "/{id}/history", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity> getImportHistory(@PathVariable("id") Long id) { - return ok(jobService.getJobHistoryItems(id) - .map(item -> conversionService.convert(item, JobHistoryItemDTO.class)) - .collect(Collectors.toList())); - } -} diff --git a/src/main/java/org/ohdsi/webapi/user/importer/UserImportMvcController.java b/src/main/java/org/ohdsi/webapi/user/importer/UserImportMvcController.java deleted file mode 100644 index dddaf6270a..0000000000 --- a/src/main/java/org/ohdsi/webapi/user/importer/UserImportMvcController.java +++ /dev/null @@ -1,249 +0,0 @@ -package org.ohdsi.webapi.user.importer; - -import org.ohdsi.analysis.Utils; -import org.ohdsi.webapi.arachne.scheduler.model.JobExecutingType; -import org.ohdsi.webapi.mvc.AbstractMvcController; -import org.ohdsi.webapi.user.importer.converter.RoleGroupMappingConverter; -import org.ohdsi.webapi.user.importer.dto.UserImportJobDTO; -import org.ohdsi.webapi.user.importer.exception.JobAlreadyExistException; -import org.ohdsi.webapi.user.importer.model.AtlasUserRoles; -import org.ohdsi.webapi.user.importer.model.AuthenticationProviders; -import org.ohdsi.webapi.user.importer.model.ConnectionInfo; -import org.ohdsi.webapi.user.importer.model.LdapGroup; -import org.ohdsi.webapi.user.importer.model.LdapProviderType; -import org.ohdsi.webapi.user.importer.model.RoleGroupEntity; -import org.ohdsi.webapi.user.importer.model.RoleGroupMapping; -import org.ohdsi.webapi.user.importer.model.UserImportJob; -import org.ohdsi.webapi.user.importer.service.UserImportJobService; -import org.ohdsi.webapi.user.importer.service.UserImportService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; - -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; - -import static org.ohdsi.webapi.Constants.JOB_IS_ALREADY_SCHEDULED; - -/** - * Spring MVC version of UserImportController - * - * Migration Status: Replaces /user/importer/UserImportController.java (Jersey) - * Endpoints: 6 endpoints (GET, POST) - * Complexity: Medium - user import from LDAP/AD with role mapping - */ -@RestController -@RequestMapping("/user") -public class UserImportMvcController extends AbstractMvcController { - - private static final Logger logger = LoggerFactory.getLogger(UserImportMvcController.class); - - private final UserImportService userImportService; - private final UserImportJobService userImportJobService; - private final GenericConversionService conversionService; - - @Value("${security.ad.url}") - private String adUrl; - - @Value("${security.ldap.url}") - private String ldapUrl; - - @Autowired - public UserImportMvcController(UserImportService userImportService, - UserImportJobService userImportJobService, - GenericConversionService conversionService) { - this.userImportService = userImportService; - this.userImportJobService = userImportJobService; - this.conversionService = conversionService; - } - - /** - * Get authentication providers - * - * Jersey: GET /WebAPI/user/providers - * Spring MVC: GET /WebAPI/v2/user/providers - * - * @return authentication providers configuration - */ - @GetMapping( - value = "/providers", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity getAuthenticationProviders() { - AuthenticationProviders providers = new AuthenticationProviders(); - providers.setAdUrl(adUrl); - providers.setLdapUrl(ldapUrl); - return ok(providers); - } - - /** - * Test connection to LDAP/AD provider - * - * Jersey: GET /WebAPI/user/import/{type}/test - * Spring MVC: GET /WebAPI/v2/user/import/{type}/test - * - * @param type provider type (ad or ldap) - * @return connection test result - */ - @GetMapping( - value = "/import/{type}/test", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity testConnection(@PathVariable("type") String type) { - LdapProviderType provider = LdapProviderType.fromValue(type); - ConnectionInfo result = new ConnectionInfo(); - userImportService.testConnection(provider); - result.setState(ConnectionInfo.ConnectionState.SUCCESS); - result.setMessage("Connection success"); - return ok(result); - } - - /** - * Find groups in LDAP/AD - * - * Jersey: GET /WebAPI/user/import/{type}/groups - * Spring MVC: GET /WebAPI/v2/user/import/{type}/groups - * - * @param type provider type (ad or ldap) - * @param searchStr search string - * @return list of LDAP groups - */ - @GetMapping( - value = "/import/{type}/groups", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity> findGroups( - @PathVariable("type") String type, - @RequestParam(value = "search", required = false) String searchStr) { - LdapProviderType provider = LdapProviderType.fromValue(type); - return ok(userImportService.findGroups(provider, searchStr)); - } - - /** - * Find users in directory - * - * Jersey: POST /WebAPI/user/import/{type} - * Spring MVC: POST /WebAPI/v2/user/import/{type} - * - * @param type provider type (ad or ldap) - * @param mapping role group mapping - * @return list of Atlas user roles - */ - @PostMapping( - value = "/import/{type}", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity> findDirectoryUsers( - @PathVariable("type") String type, - @RequestBody RoleGroupMapping mapping) { - LdapProviderType provider = LdapProviderType.fromValue(type); - return ok(userImportService.findUsers(provider, mapping)); - } - - /** - * Import users from directory - * - * Jersey: POST /WebAPI/user/import - * Spring MVC: POST /WebAPI/v2/user/import - * - * @param users list of Atlas user roles to import - * @param provider provider type - * @param preserveRoles whether to preserve existing roles - * @return created user import job - */ - @PostMapping( - value = "/import", - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity importUsers( - @RequestBody List users, - @RequestParam(value = "provider") String provider, - @RequestParam(value = "preserve", defaultValue = "TRUE") Boolean preserveRoles) { - LdapProviderType providerType = LdapProviderType.fromValue(provider); - - UserImportJobDTO jobDto = new UserImportJobDTO(); - jobDto.setProviderType(providerType); - jobDto.setPreserveRoles(preserveRoles); - jobDto.setEnabled(true); - jobDto.setStartDate(getJobStartDate()); - jobDto.setFrequency(JobExecutingType.ONCE); - jobDto.setRecurringTimes(0); - if (users != null) { - jobDto.setUserRoles(Utils.serialize(users)); - } - - try { - UserImportJob job = conversionService.convert(jobDto, UserImportJob.class); - UserImportJob created = userImportJobService.createJob(job); - return ok(conversionService.convert(created, UserImportJobDTO.class)); - } catch (JobAlreadyExistException e) { - throw new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, - String.format(JOB_IS_ALREADY_SCHEDULED, jobDto.getProviderType())); - } - } - - /** - * Save role group mapping - * - * Jersey: POST /WebAPI/user/import/{type}/mapping - * Spring MVC: POST /WebAPI/v2/user/import/{type}/mapping - * - * @param type provider type (ad or ldap) - * @param mapping role group mapping - */ - @PostMapping( - value = "/import/{type}/mapping", - consumes = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity saveMapping(@PathVariable("type") String type, @RequestBody RoleGroupMapping mapping) { - LdapProviderType providerType = LdapProviderType.fromValue(type); - List mappingEntities = RoleGroupMappingConverter.convertRoleGroupMapping(mapping); - userImportService.saveRoleGroupMapping(providerType, mappingEntities); - return ok(); - } - - /** - * Get role group mapping - * - * Jersey: GET /WebAPI/user/import/{type}/mapping - * Spring MVC: GET /WebAPI/v2/user/import/{type}/mapping - * - * @param type provider type (ad or ldap) - * @return role group mapping - */ - @GetMapping( - value = "/import/{type}/mapping", - produces = MediaType.APPLICATION_JSON_VALUE - ) - public ResponseEntity getMapping(@PathVariable("type") String type) { - LdapProviderType providerType = LdapProviderType.fromValue(type); - List mappingEntities = userImportService.getRoleGroupMapping(providerType); - return ok(RoleGroupMappingConverter.convertRoleGroupMapping(type, mappingEntities)); - } - - private Date getJobStartDate() { - Calendar calendar = GregorianCalendar.getInstance(); - // Job will be started in five seconds after now - calendar.add(Calendar.SECOND, 5); - calendar.set(Calendar.MILLISECOND, 0); - - return calendar.getTime(); - } -} diff --git a/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportJobServiceImpl.java b/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportJobServiceImpl.java index 9897a8b57f..cbcd7914d7 100644 --- a/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportJobServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportJobServiceImpl.java @@ -3,10 +3,14 @@ import com.cosium.spring.data.jpa.entity.graph.domain2.EntityGraph; import com.cosium.spring.data.jpa.entity.graph.domain2.NamedEntityGraph; import com.cronutils.model.definition.CronDefinition; +import org.ohdsi.webapi.arachne.scheduler.exception.JobNotFoundException; import org.ohdsi.webapi.arachne.scheduler.model.ScheduledTask; import org.ohdsi.webapi.arachne.scheduler.service.BaseJobServiceImpl; import org.ohdsi.webapi.Constants; import org.ohdsi.webapi.job.JobTemplate; +import org.ohdsi.webapi.user.importer.dto.JobHistoryItemDTO; +import org.ohdsi.webapi.user.importer.dto.UserImportJobDTO; +import org.ohdsi.webapi.user.importer.exception.JobAlreadyExistException; import org.ohdsi.webapi.user.importer.model.LdapProviderType; import org.ohdsi.webapi.user.importer.model.RoleGroupEntity; import org.ohdsi.webapi.user.importer.model.UserImportJob; @@ -23,11 +27,22 @@ import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.scheduling.TaskScheduler; -import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; import jakarta.annotation.PostConstruct; import java.util.List; @@ -35,9 +50,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.ohdsi.webapi.Constants.JOB_IS_ALREADY_SCHEDULED; import static org.ohdsi.webapi.Constants.SYSTEM_USER; -@Service +@RestController +@RequestMapping("/user/import/job") @Transactional public class UserImportJobServiceImpl extends BaseJobServiceImpl implements UserImportJobService { @@ -49,6 +66,7 @@ public class UserImportJobServiceImpl extends BaseJobServiceImpl private final JobRepository jobRepositoryBatch; private final PlatformTransactionManager transactionManager; private final JobTemplate jobTemplate; + private final GenericConversionService conversionService; private EntityGraph jobWithMappingEntityGraph = NamedEntityGraph.loading("jobWithMapping"); public UserImportJobServiceImpl(TaskScheduler taskScheduler, @@ -61,7 +79,8 @@ public UserImportJobServiceImpl(TaskScheduler taskScheduler, TransactionTemplate transactionTemplate, JobRepository jobRepositoryBatch, PlatformTransactionManager transactionManager, - JobTemplate jobTemplate) { + JobTemplate jobTemplate, + @Qualifier("conversionService") GenericConversionService conversionService) { super(taskScheduler, cronDefinition, jobRepository); this.userImportService = userImportService; @@ -72,6 +91,7 @@ public UserImportJobServiceImpl(TaskScheduler taskScheduler, this.jobRepositoryBatch = jobRepositoryBatch; this.transactionManager = transactionManager; this.jobTemplate = jobTemplate; + this.conversionService = conversionService; } @PostConstruct @@ -145,6 +165,104 @@ public Optional getLatestHistoryItem(Long id) { return jobHistoryItemRepository.findFirstByUserImportIdOrderByEndTimeDesc(id); } + // ==================== REST Endpoints ==================== + + /** + * Create a user import job + */ + @PostMapping( + value = "/", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public UserImportJobDTO createJobEndpoint(@RequestBody UserImportJobDTO jobDTO) { + UserImportJob job = conversionService.convert(jobDTO, UserImportJob.class); + try { + UserImportJob created = createJob(job); + return conversionService.convert(created, UserImportJobDTO.class); + } catch (JobAlreadyExistException e) { + throw new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, + String.format(JOB_IS_ALREADY_SCHEDULED, job.getProviderType())); + } + } + + /** + * Update a user import job + */ + @PutMapping( + value = "/{id}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public UserImportJobDTO updateJobEndpoint( + @PathVariable("id") Long jobId, + @RequestBody UserImportJobDTO jobDTO) { + UserImportJob job = conversionService.convert(jobDTO, UserImportJob.class); + try { + job.setId(jobId); + UserImportJob updated = updateJob(job); + return conversionService.convert(updated, UserImportJobDTO.class); + } catch (JobNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + } + + /** + * Get the user import job list + */ + @GetMapping( + value = "/", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @Transactional + public List listJobsEndpoint() { + return getJobs().stream() + .map(job -> conversionService.convert(job, UserImportJobDTO.class)) + .peek(job -> getLatestHistoryItem(job.getId()) + .ifPresent(item -> job.setLastExecuted(item.getEndTime()))) + .collect(Collectors.toList()); + } + + /** + * Get user import job by ID + */ + @GetMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public UserImportJobDTO getJobEndpoint(@PathVariable("id") Long id) { + return getJob(id) + .map(job -> conversionService.convert(job, UserImportJobDTO.class)) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + } + + /** + * Delete user import job by ID + */ + @DeleteMapping( + value = "/{id}" + ) + public void deleteJobEndpoint(@PathVariable("id") Long id) { + UserImportJob job = getJob(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + delete(job); + } + + /** + * Get the user import job history + */ + @GetMapping( + value = "/{id}/history", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public List getImportHistoryEndpoint(@PathVariable("id") Long id) { + return getJobHistoryItems(id) + .map(item -> conversionService.convert(item, JobHistoryItemDTO.class)) + .collect(Collectors.toList()); + } + + // ==================== Internal Methods ==================== + Step userImportStep() { UserImportTasklet userImportTasklet = new UserImportTasklet(transactionTemplate, userImportService); diff --git a/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportServiceImpl.java b/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportServiceImpl.java index bb4abdf61c..0ef046c962 100644 --- a/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportServiceImpl.java @@ -1,13 +1,20 @@ package org.ohdsi.webapi.user.importer.service; import org.apache.commons.collections.CollectionUtils; +import org.ohdsi.analysis.Utils; +import org.ohdsi.webapi.arachne.scheduler.model.JobExecutingType; import org.ohdsi.webapi.shiro.Entities.RoleEntity; import org.ohdsi.webapi.shiro.Entities.UserEntity; import org.ohdsi.webapi.shiro.Entities.UserOrigin; import org.ohdsi.webapi.shiro.Entities.UserRepository; import org.ohdsi.webapi.shiro.PermissionManager; import org.ohdsi.webapi.user.Role; +import org.ohdsi.webapi.user.importer.converter.RoleGroupMappingConverter; +import org.ohdsi.webapi.user.importer.dto.UserImportJobDTO; +import org.ohdsi.webapi.user.importer.exception.JobAlreadyExistException; import org.ohdsi.webapi.user.importer.model.AtlasUserRoles; +import org.ohdsi.webapi.user.importer.model.AuthenticationProviders; +import org.ohdsi.webapi.user.importer.model.ConnectionInfo; import org.ohdsi.webapi.user.importer.model.LdapGroup; import org.ohdsi.webapi.user.importer.model.LdapProviderType; import org.ohdsi.webapi.user.importer.model.LdapUserImportStatus; @@ -27,13 +34,26 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.filter.AndFilter; import org.springframework.ldap.filter.EqualsFilter; import org.springframework.ldap.support.LdapUtils; -import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; import org.springframework.transaction.annotation.Transactional; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -42,13 +62,17 @@ import java.util.Set; import java.util.stream.Collectors; +import static org.ohdsi.webapi.Constants.JOB_IS_ALREADY_SCHEDULED; import static org.ohdsi.webapi.user.importer.providers.AbstractLdapProvider.OBJECTCLASS_ATTR; import static org.ohdsi.webapi.user.importer.providers.OhdsiLdapUtils.getCriteria; -@Service +@RestController +@RequestMapping("/user") @Transactional(readOnly = true) public class UserImportServiceImpl implements UserImportService { + // Note: @RestController already includes @Component, so @Service is not needed + private static final Logger logger = LoggerFactory.getLogger(UserImportService.class); private final Map providersMap = new HashMap<>(); @@ -61,20 +85,34 @@ public class UserImportServiceImpl implements UserImportService { private final RoleGroupRepository roleGroupMappingRepository; + private final UserImportJobService userImportJobService; + + private final GenericConversionService conversionService; + @Value("${security.ad.default.import.group}#{T(java.util.Collections).emptyList()}") private List defaultRoles; + @Value("${security.ad.url}") + private String adUrl; + + @Value("${security.ldap.url}") + private String ldapUrl; + public UserImportServiceImpl(@Autowired(required = false) ActiveDirectoryProvider activeDirectoryProvider, @Autowired(required = false) DefaultLdapProvider ldapProvider, UserRepository userRepository, UserImportJobRepository userImportJobRepository, PermissionManager userManager, - RoleGroupRepository roleGroupMappingRepository) { + RoleGroupRepository roleGroupMappingRepository, + @Autowired(required = false) UserImportJobService userImportJobService, + GenericConversionService conversionService) { this.userRepository = userRepository; this.userImportJobRepository = userImportJobRepository; this.userManager = userManager; this.roleGroupMappingRepository = roleGroupMappingRepository; + this.userImportJobService = userImportJobService; + this.conversionService = conversionService; Optional.ofNullable(activeDirectoryProvider).ifPresent(provider -> providersMap.put(LdapProviderType.ACTIVE_DIRECTORY, provider)); Optional.ofNullable(ldapProvider).ifPresent(provider -> providersMap.put(LdapProviderType.LDAP, provider)); } @@ -84,6 +122,139 @@ protected Optional getProvider(LdapProviderType type) { return Optional.ofNullable(providersMap.get(type)); } + // ==================== REST Endpoints ==================== + + /** + * Get authentication providers + */ + @GetMapping( + value = "/providers", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public AuthenticationProviders getAuthenticationProviders() { + AuthenticationProviders providers = new AuthenticationProviders(); + providers.setAdUrl(adUrl); + providers.setLdapUrl(ldapUrl); + return providers; + } + + /** + * Test connection to LDAP/AD provider + */ + @GetMapping( + value = "/import/{type}/test", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ConnectionInfo testConnectionEndpoint(@PathVariable("type") String type) { + LdapProviderType provider = LdapProviderType.fromValue(type); + ConnectionInfo result = new ConnectionInfo(); + testConnection(provider); + result.setState(ConnectionInfo.ConnectionState.SUCCESS); + result.setMessage("Connection success"); + return result; + } + + /** + * Find groups in LDAP/AD + */ + @GetMapping( + value = "/import/{type}/groups", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public List findGroupsEndpoint( + @PathVariable("type") String type, + @RequestParam(value = "search", required = false) String searchStr) { + LdapProviderType provider = LdapProviderType.fromValue(type); + return findGroups(provider, searchStr); + } + + /** + * Find users in directory + */ + @PostMapping( + value = "/import/{type}", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public List findDirectoryUsers( + @PathVariable("type") String type, + @RequestBody RoleGroupMapping mapping) { + LdapProviderType provider = LdapProviderType.fromValue(type); + return findUsers(provider, mapping); + } + + /** + * Import users from directory + */ + @PostMapping( + value = "/import", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public UserImportJobDTO importUsersEndpoint( + @RequestBody List users, + @RequestParam(value = "provider") String provider, + @RequestParam(value = "preserve", defaultValue = "TRUE") Boolean preserveRoles) { + LdapProviderType providerType = LdapProviderType.fromValue(provider); + + UserImportJobDTO jobDto = new UserImportJobDTO(); + jobDto.setProviderType(providerType); + jobDto.setPreserveRoles(preserveRoles); + jobDto.setEnabled(true); + jobDto.setStartDate(getJobStartDate()); + jobDto.setFrequency(JobExecutingType.ONCE); + jobDto.setRecurringTimes(0); + if (users != null) { + jobDto.setUserRoles(Utils.serialize(users)); + } + + try { + UserImportJob job = conversionService.convert(jobDto, UserImportJob.class); + UserImportJob created = userImportJobService.createJob(job); + return conversionService.convert(created, UserImportJobDTO.class); + } catch (JobAlreadyExistException e) { + throw new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, + String.format(JOB_IS_ALREADY_SCHEDULED, jobDto.getProviderType())); + } + } + + /** + * Save role group mapping + */ + @PostMapping( + value = "/import/{type}/mapping", + consumes = MediaType.APPLICATION_JSON_VALUE + ) + public void saveMappingEndpoint(@PathVariable("type") String type, @RequestBody RoleGroupMapping mapping) { + LdapProviderType providerType = LdapProviderType.fromValue(type); + List mappingEntities = RoleGroupMappingConverter.convertRoleGroupMapping(mapping); + saveRoleGroupMapping(providerType, mappingEntities); + } + + /** + * Get role group mapping + */ + @GetMapping( + value = "/import/{type}/mapping", + produces = MediaType.APPLICATION_JSON_VALUE + ) + public RoleGroupMapping getMappingEndpoint(@PathVariable("type") String type) { + LdapProviderType providerType = LdapProviderType.fromValue(type); + List mappingEntities = getRoleGroupMapping(providerType); + return RoleGroupMappingConverter.convertRoleGroupMapping(type, mappingEntities); + } + + private Date getJobStartDate() { + Calendar calendar = GregorianCalendar.getInstance(); + // Job will be started in five seconds after now + calendar.add(Calendar.SECOND, 5); + calendar.set(Calendar.MILLISECOND, 0); + + return calendar.getTime(); + } + + // ==================== Service Methods ==================== + @Override public List findGroups(LdapProviderType type, String searchStr) { diff --git a/src/test/java/org/ohdsi/webapi/test/migration/DualRuntimeTestSupport.java b/src/test/java/org/ohdsi/webapi/test/migration/DualRuntimeTestSupport.java deleted file mode 100644 index 7c03f409b1..0000000000 --- a/src/test/java/org/ohdsi/webapi/test/migration/DualRuntimeTestSupport.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.ohdsi.webapi.test.migration; - -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; - -import static org.junit.Assert.*; - -/** - * Test support utilities for Spring MVC endpoints. - * Jersey has been removed - Spring MVC now serves all /WebAPI/* endpoints. - */ -public class DualRuntimeTestSupport { - - private final TestRestTemplate restTemplate; - private final String baseUri; - - public DualRuntimeTestSupport(TestRestTemplate restTemplate, String baseUri) { - this.restTemplate = restTemplate; - this.baseUri = baseUri; - } - - /** - * Test endpoint (deprecated - kept for backward compatibility) - * @deprecated Use verifyEndpoint instead - */ - @Deprecated - public void assertEndpointParity(String endpoint, Class responseType) { - verifyEndpoint(endpoint, 200, responseType); - } - - /** - * Test endpoint with request body (deprecated - kept for backward compatibility) - * @deprecated Use verifyEndpoint with custom assertions instead - */ - @Deprecated - public void assertEndpointParity(String endpoint, R requestBody, HttpMethod method, Class responseType) { - String url = baseUri + endpoint; - HttpEntity request = requestBody != null ? new HttpEntity<>(requestBody) : null; - ResponseEntity response = restTemplate.exchange(url, method, request, responseType); - assertNotNull("Response should not be null for endpoint: " + endpoint, response); - } - - /** - * Verify an endpoint exists and returns expected status - */ - public ResponseEntity verifyEndpoint(String endpoint, int expectedStatus, Class responseType) { - String url = baseUri + endpoint; - ResponseEntity response = restTemplate.getForEntity(url, responseType); - - assertEquals("Expected status code " + expectedStatus + " for endpoint: " + endpoint, - expectedStatus, response.getStatusCodeValue()); - - return response; - } - - /** - * Check if endpoint is available (returns non-404) - */ - public boolean isMvcEndpointAvailable(String endpoint) { - String url = baseUri + endpoint; - try { - ResponseEntity response = restTemplate.getForEntity(url, String.class); - return response.getStatusCodeValue() != 404; - } catch (Exception e) { - return false; - } - } - - /** - * Get the base URI for constructing test URLs - */ - public String getBaseUri() { - return baseUri; - } - - /** - * Get Jersey endpoint URL (deprecated - Jersey removed) - * @deprecated Use getMvcUrl instead - */ - @Deprecated - public String getJerseyUrl(String endpoint) { - return baseUri + endpoint; - } - - /** - * Get Spring MVC endpoint URL - * baseUri already includes context path (/WebAPI), so result is /WebAPI/endpoint - */ - public String getMvcUrl(String endpoint) { - return baseUri + endpoint; - } -} diff --git a/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase1IT.java b/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase1IT.java deleted file mode 100644 index 00eac4c100..0000000000 --- a/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase1IT.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.ohdsi.webapi.test.migration; - -import org.junit.Test; -import org.ohdsi.webapi.test.WebApiIT; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.web.client.TestRestTemplate; - -/** - * Integration tests for Phase 1: Foundation & Parallel Runtime - * - * Verifies that: - * 1. Spring MVC is configured and running alongside Jersey - * 2. Both frameworks can handle requests independently - * 3. Configuration is correct for dual-runtime operation - */ -public class MigrationPhase1IT extends WebApiIT { - - @Value("${baseUri}") - private String baseUri; - - private final TestRestTemplate restTemplate = new TestRestTemplate(); - - @Test - public void testSpringMvcIsConfigured() { - // This test verifies that Spring MVC dispatcher servlet is active - // During migration, we use /v2/ prefix to avoid conflicts with Jersey - // A 404 for /WebAPI/v2/test is expected (no controllers yet), - // but it should be handled by Spring MVC, not Jersey - - log.info("Testing Spring MVC configuration..."); - log.info("Base URI: {}", baseUri); - - // This validates that Spring MVC is intercepting requests to /v2/ paths - // We're not testing a specific endpoint, just that the framework is active - } - - @Test - public void testJerseyStillWorks() { - // Verify that existing Jersey endpoints still function - String url = baseUri + "/WebAPI/info"; - - try { - var response = restTemplate.getForEntity(url, String.class); - log.info("Jersey /info endpoint returned status: {}", response.getStatusCode()); - // Should return 200 or appropriate status, not 404 - } catch (Exception e) { - log.info("Jersey endpoint test: {}", e.getMessage()); - } - } -} diff --git a/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase2IT.java b/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase2IT.java deleted file mode 100644 index 9d439b877a..0000000000 --- a/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase2IT.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.ohdsi.webapi.test.migration; - -import org.junit.Test; -import org.ohdsi.webapi.test.WebApiIT; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import static org.junit.Assert.*; - -/** - * Integration tests for Phase 2: Provider Migration - * - * Verifies that: - * 1. GlobalExceptionHandler handles exceptions correctly (replaces GenericExceptionMapper) - * 2. JDBC connection exceptions are handled (replaces JdbcExceptionMapper) - * 3. LocaleInterceptor resolves locale correctly (replaces LocaleFilter) - * 4. OutputStreamMessageConverter works (replaces OutputStreamWriter) - */ -public class MigrationPhase2IT extends WebApiIT { - - @Value("${baseUri}") - private String baseUri; - - private final TestRestTemplate restTemplate = new TestRestTemplate(); - - @Test - public void testGlobalExceptionHandlerIsConfigured() { - log.info("Testing GlobalExceptionHandler configuration..."); - - // The @RestControllerAdvice annotation should be picked up by Spring - // This test verifies that Spring MVC exception handling is active - - // When we migrate a controller and it throws an exception, - // GlobalExceptionHandler should catch it and return proper error response - - // For now, just verify the handler class exists and is properly annotated - try { - Class.forName("org.ohdsi.webapi.mvc.GlobalExceptionHandler"); - log.info("GlobalExceptionHandler class found"); - } catch (ClassNotFoundException e) { - fail("GlobalExceptionHandler class not found"); - } - } - - @Test - public void testLocaleInterceptorIsConfigured() { - log.info("Testing LocaleInterceptor configuration..."); - - // The LocaleInterceptor should be registered in WebMvcConfig - // It should intercept requests and set locale based on headers/params - - // This will be fully testable once we have a migrated controller - // that uses locale-specific responses - - try { - Class.forName("org.ohdsi.webapi.i18n.mvc.LocaleInterceptor"); - log.info("LocaleInterceptor class found"); - } catch (ClassNotFoundException e) { - fail("LocaleInterceptor class not found"); - } - } - - @Test - public void testOutputStreamMessageConverterIsConfigured() { - log.info("Testing OutputStreamMessageConverter configuration..."); - - // The OutputStreamMessageConverter should be registered in WebMvcConfig - // It allows controllers to return ByteArrayOutputStream for downloads - - try { - Class.forName("org.ohdsi.webapi.mvc.OutputStreamMessageConverter"); - log.info("OutputStreamMessageConverter class found"); - } catch (ClassNotFoundException e) { - fail("OutputStreamMessageConverter class not found"); - } - } - - @Test - public void testExceptionHandlingWhenControllerNotFound() { - // Test that 404 errors are handled correctly by Spring MVC - // (Jersey has been removed) - - String url = baseUri + "/WebAPI/nonexistent-endpoint"; - ResponseEntity response = restTemplate.getForEntity(url, String.class); - - log.info("Response status for non-existent endpoint: {}", response.getStatusCode()); - - // Should get 404, not 500 - assertTrue("Should return 404 or similar client error", - response.getStatusCode().is4xxClientError()); - } -} diff --git a/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase3IT.java b/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase3IT.java deleted file mode 100644 index b2881074d6..0000000000 --- a/src/test/java/org/ohdsi/webapi/test/migration/MigrationPhase3IT.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.ohdsi.webapi.test.migration; - -import org.junit.Test; -import org.ohdsi.webapi.info.Info; -import org.ohdsi.webapi.test.WebApiIT; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import static org.junit.Assert.*; - -/** - * Integration tests for Phase 3: Simple Controllers Migration - * - * Verifies that all 6 migrated simple controllers work correctly: - * 1. InfoMvcController - * 2. CacheMvcController - * 3. ActivityMvcController - * 4. SqlRenderMvcController - * 5. DDLMvcController - * 6. I18nMvcController - * - * Each test verifies: - * - Controller is accessible at /WebAPI/v2/* URL - * - Returns expected HTTP status - * - Response body is valid (where applicable) - */ -public class MigrationPhase3IT extends WebApiIT { - - private final TestRestTemplate restTemplate = new TestRestTemplate(); - private DualRuntimeTestSupport support; - - @Test - public void testInfoMvcController() { - log.info("Testing InfoMvcController..."); - - // Initialize support with baseUri from parent class - support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); - - String mvcUrl = support.getMvcUrl("/info/"); - ResponseEntity response = restTemplate.getForEntity(mvcUrl, Info.class); - - log.info("InfoMvcController status: {}", response.getStatusCode()); - assertEquals("Should return 200 OK", HttpStatus.OK, response.getStatusCode()); - - Info info = response.getBody(); - assertNotNull("Info should not be null", info); - assertNotNull("Version should not be null", info.getVersion()); - log.info("WebAPI version: {}", info.getVersion()); - } - - @Test - public void testCacheMvcController() { - log.info("Testing CacheMvcController..."); - - support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); - - // Test GET /cache/ - String mvcUrl = support.getMvcUrl("/cache/"); - ResponseEntity response = restTemplate.getForEntity(mvcUrl, String.class); - - log.info("CacheMvcController status: {}", response.getStatusCode()); - assertEquals("Should return 200 OK", HttpStatus.OK, response.getStatusCode()); - assertNotNull("Response body should not be null", response.getBody()); - } - - @Test - public void testActivityMvcController() { - log.info("Testing ActivityMvcController..."); - - support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); - - String mvcUrl = support.getMvcUrl("/activity/latest"); - ResponseEntity response = restTemplate.getForEntity(mvcUrl, Object[].class); - - log.info("ActivityMvcController status: {}", response.getStatusCode()); - assertEquals("Should return 200 OK", HttpStatus.OK, response.getStatusCode()); - assertNotNull("Activity array should not be null", response.getBody()); - } - - @Test - public void testSqlRenderMvcController() { - log.info("Testing SqlRenderMvcController..."); - - support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); - - // This requires a POST request with JSON body - // For now, just verify the endpoint exists - String mvcUrl = support.getMvcUrl("/sqlrender/translate"); - - // POST without body should still return a response (not 404) - ResponseEntity response = restTemplate.postForEntity(mvcUrl, null, String.class); - - log.info("SqlRenderMvcController status: {}", response.getStatusCode()); - // Should get 200 or 400, not 404 - assertTrue("Should not return 404", - response.getStatusCode() != HttpStatus.NOT_FOUND); - } - - @Test - public void testDDLMvcController() { - log.info("Testing DDLMvcController..."); - - support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); - - // Test GET /ddl/results - String mvcUrl = support.getMvcUrl("/ddl/results?dialect=postgresql&schema=results"); - ResponseEntity response = restTemplate.getForEntity(mvcUrl, String.class); - - log.info("DDLMvcController status: {}", response.getStatusCode()); - assertEquals("Should return 200 OK", HttpStatus.OK, response.getStatusCode()); - assertNotNull("DDL SQL should not be null", response.getBody()); - assertTrue("DDL should contain SQL", response.getBody().length() > 0); - } - - @Test - public void testI18nMvcController() { - log.info("Testing I18nMvcController..."); - - support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); - - // Test GET /i18n/ - String mvcUrl = support.getMvcUrl("/i18n/"); - ResponseEntity response = restTemplate.getForEntity(mvcUrl, String.class); - - log.info("I18nMvcController status: {}", response.getStatusCode()); - assertEquals("Should return 200 OK", HttpStatus.OK, response.getStatusCode()); - assertNotNull("i18n resources should not be null", response.getBody()); - - // Test GET /i18n/locales - mvcUrl = support.getMvcUrl("/i18n/locales"); - response = restTemplate.getForEntity(mvcUrl, String.class); - - assertEquals("Should return 200 OK for locales", HttpStatus.OK, response.getStatusCode()); - assertNotNull("Locales should not be null", response.getBody()); - } - - @Test - public void testAllMigratedControllersAccessible() { - log.info("Testing all Phase 3 controllers are accessible..."); - - support = new DualRuntimeTestSupport(restTemplate, getBaseUri()); - - String[] endpoints = { - "/info/", - "/cache/", - "/activity/latest", - "/ddl/results", - "/i18n/", - "/i18n/locales" - }; - - int successCount = 0; - for (String endpoint : endpoints) { - if (support.isMvcEndpointAvailable(endpoint)) { - successCount++; - log.info("✓ {} is available", endpoint); - } else { - log.warn("✗ {} is NOT available", endpoint); - } - } - - assertEquals("All 6 endpoints should be accessible", endpoints.length, successCount); - } -} From 1b984a4fb148917ead98b2839d19047669901db5 Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:32:14 +0800 Subject: [PATCH 06/18] fix build issues --- .../java/org/ohdsi/webapi/WebMvcConfig.java | 23 ++-------- .../ohdsi/webapi/i18n/I18nServiceImpl.java | 7 +--- .../webapi/i18n/mvc/LocaleInterceptor.java | 4 +- .../webapi/mvc/AbstractMvcController.java | 5 +-- .../webapi/mvc/GlobalExceptionHandler.java | 14 ++----- .../mvc/OutputStreamMessageConverter.java | 12 ++---- .../ohdsi/webapi/mvc/ResponseConverters.java | 42 ------------------- .../mvc/controller/CacheMvcController.java | 17 ++------ .../shiro/management/FilterChainBuilder.java | 2 +- .../ohdsi/webapi/source/SourceService.java | 3 +- .../service/UserImportServiceImpl.java | 3 +- .../webapi/tagging/ReusableTaggingTest.java | 22 +++++----- 12 files changed, 35 insertions(+), 119 deletions(-) delete mode 100644 src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java diff --git a/src/main/java/org/ohdsi/webapi/WebMvcConfig.java b/src/main/java/org/ohdsi/webapi/WebMvcConfig.java index 427c57adc8..4c456fa857 100644 --- a/src/main/java/org/ohdsi/webapi/WebMvcConfig.java +++ b/src/main/java/org/ohdsi/webapi/WebMvcConfig.java @@ -11,23 +11,7 @@ /** * Spring MVC Configuration. - * Jersey has been removed - Spring MVC now serves all endpoints. - * - * Spring MVC endpoints are served at: /WebAPI/* (via server.context-path=/WebAPI) - * - * NOTE: We don't use @EnableWebMvc because: - * - Spring Boot auto-configures Spring MVC by default - * - @EnableWebMvc would disable Spring Boot's auto-configuration - * - This would conflict with existing I18nConfig (duplicate localeResolver bean) - * - We only need to customize specific aspects, not override everything - * - * NOTE: We don't need a custom ServletRegistrationBean because: - * - Spring Boot's default DispatcherServlet already serves at context-path + /* - * - With server.context-path=/WebAPI, it automatically serves /WebAPI/* - * - @ComponentScan in WebApi.java finds controllers in org.ohdsi.webapi.mvc.controller - * - * @see org.ohdsi.webapi.I18nConfig - * @see org.ohdsi.webapi.WebApi + * Configures interceptors, message converters, and other MVC components. */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { @@ -37,7 +21,7 @@ public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - // Add locale interceptor if available (replaces Jersey LocaleFilter) + // Add locale interceptor if available if (localeInterceptor != null) { registry.addInterceptor(localeInterceptor) .addPathPatterns("/**"); @@ -46,8 +30,7 @@ public void addInterceptors(InterceptorRegistry registry) { @Override public void extendMessageConverters(List> converters) { - // Add custom OutputStreamMessageConverter (replaces Jersey's OutputStreamWriter) - // Spring Boot already configures Jackson converter, so we just extend the list + // Add custom OutputStreamMessageConverter for ByteArrayOutputStream responses converters.add(new org.ohdsi.webapi.mvc.OutputStreamMessageConverter()); } } diff --git a/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java b/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java index 75201fed57..dedfb0f07e 100644 --- a/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java @@ -23,11 +23,8 @@ import java.util.Objects; /** - * Spring MVC version of I18nController - * - * Migration Status: Replaces /i18n/I18nController.java (Jersey) - * Endpoints: 2 GET endpoints - * Complexity: Simple - i18n resource handling + * Internationalization service. + * Provides localized message resources and available locales. */ @RestController @RequestMapping("/i18n") diff --git a/src/main/java/org/ohdsi/webapi/i18n/mvc/LocaleInterceptor.java b/src/main/java/org/ohdsi/webapi/i18n/mvc/LocaleInterceptor.java index 9b121c0f3a..c6b8a8452e 100644 --- a/src/main/java/org/ohdsi/webapi/i18n/mvc/LocaleInterceptor.java +++ b/src/main/java/org/ohdsi/webapi/i18n/mvc/LocaleInterceptor.java @@ -11,10 +11,8 @@ import java.util.Locale; /** - * Spring MVC HandlerInterceptor replacement for Jersey's LocaleFilter. + * Locale interceptor. * Extracts locale from request headers and sets it in LocaleContextHolder. - * - * Migration Status: Replaces /i18n/LocaleFilter.java (JAX-RS ContainerRequestFilter) */ @Component public class LocaleInterceptor implements HandlerInterceptor { diff --git a/src/main/java/org/ohdsi/webapi/mvc/AbstractMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/AbstractMvcController.java index 714c4fedab..50ca744158 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/AbstractMvcController.java +++ b/src/main/java/org/ohdsi/webapi/mvc/AbstractMvcController.java @@ -6,9 +6,8 @@ import org.springframework.http.ResponseEntity; /** - * Base class for Spring MVC controllers during Jersey migration. - * Provides common functionality and utilities for migrated controllers. - * Extends AbstractAdminService to inherit security helper methods (isSecured, isAdmin, isModerator). + * Base class for Spring MVC controllers. + * Provides common response helper methods and security utilities. */ public abstract class AbstractMvcController extends AbstractAdminService { diff --git a/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java b/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java index 29cb48ddf8..ca088de967 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java +++ b/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java @@ -35,13 +35,8 @@ import java.util.Objects; /** - * Spring MVC Global Exception Handler - * - * Replaces Jersey JAX-RS exception mappers: - * - GenericExceptionMapper.java (handles all throwables) - * - JdbcExceptionMapper.java (handles database connection failures) - * - * Migration Status: Replaces both JAX-RS @Provider exception mappers + * Global exception handler for REST controllers. + * Handles all exceptions and returns appropriate HTTP responses. */ @RestControllerAdvice public class GlobalExceptionHandler { @@ -53,8 +48,7 @@ public class GlobalExceptionHandler { private ApplicationEventPublisher eventPublisher; /** - * Handle database connection failures - * Replaces: JdbcExceptionMapper + * Handle database connection failures. */ @ExceptionHandler(CannotGetJdbcConnectionException.class) public ResponseEntity handleDatabaseConnectionException(CannotGetJdbcConnectionException exception) { @@ -90,7 +84,7 @@ public ResponseEntity handleAuthorizationException(Exception ex) { } /** - * Handle Spring ResponseStatusException (replaces JAX-RS NotFoundException, ForbiddenException, etc.) + * Handle ResponseStatusException. */ @ExceptionHandler(ResponseStatusException.class) public ResponseEntity handleResponseStatusException(ResponseStatusException ex) { diff --git a/src/main/java/org/ohdsi/webapi/mvc/OutputStreamMessageConverter.java b/src/main/java/org/ohdsi/webapi/mvc/OutputStreamMessageConverter.java index eed1d22100..e054f6895d 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/OutputStreamMessageConverter.java +++ b/src/main/java/org/ohdsi/webapi/mvc/OutputStreamMessageConverter.java @@ -14,15 +14,9 @@ import java.util.List; /** - * Spring MVC HttpMessageConverter for ByteArrayOutputStream - * - * Replaces Jersey JAX-RS MessageBodyWriter: - * - OutputStreamWriter.java - * - * This converter allows controllers to return ByteArrayOutputStream directly, + * HttpMessageConverter for ByteArrayOutputStream. + * Allows controllers to return ByteArrayOutputStream directly, * which is useful for streaming/downloading generated content. - * - * Migration Status: Replaces JAX-RS @Provider MessageBodyWriter */ @Component public class OutputStreamMessageConverter implements HttpMessageConverter { @@ -40,7 +34,7 @@ public boolean canWrite(Class clazz, MediaType mediaType) { @Override public List getSupportedMediaTypes() { - // Support all media types (like Jersey's implementation) + // Support all media types return Collections.singletonList(MediaType.ALL); } diff --git a/src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java b/src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java deleted file mode 100644 index 4eff55db7d..0000000000 --- a/src/main/java/org/ohdsi/webapi/mvc/ResponseConverters.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.ohdsi.webapi.mvc; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import org.springframework.http.ResponseEntity; - -/** - * Utility class to convert JAX-RS ResponseEntity objects to Spring ResponseEntity. - * Used during migration to facilitate gradual conversion of endpoints. - */ -public class ResponseConverters { - - /** - * Convert JAX-RS ResponseEntity to Spring ResponseEntity - */ - public static ResponseEntity toResponseEntity(ResponseEntity response) { - if (response == null) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - - HttpStatus status = HttpStatus.valueOf(response.getStatusCode().value()); - - @SuppressWarnings("unchecked") - T body = (T) response.getBody(); - - if (body == null) { - return ResponseEntity.status(status).build(); - } - - return ResponseEntity.status(status).body(body); - } - - // JAX-RS conversion methods removed - no longer needed after migration to Spring MVC - - /** - * Create ResponseEntity from status code and entity - */ - public static ResponseEntity fromStatusCode(int statusCode, T entity) { - return ResponseEntity.status(statusCode).body(entity); - } -} diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java index f885bcb719..4c3ff11dc4 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java @@ -17,11 +17,8 @@ import java.util.stream.StreamSupport; /** - * Spring MVC version of CacheService - * - * Migration Status: Replaces /cache/CacheService.java (Jersey) - * Endpoints: 2 GET endpoints - * Complexity: Simple - basic cache operations + * Cache management controller. + * Provides endpoints for viewing and clearing application caches. */ @RestController @RequestMapping("/cache") @@ -43,10 +40,7 @@ public CacheMvcController(CacheManager cacheManager) { } /** - * Get list of all caches with statistics - * - * Jersey: GET /WebAPI/cache/ - * Spring MVC: GET /WebAPI/v2/cache/ + * Get list of all caches with statistics. */ @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> getCacheInfoList() { @@ -68,10 +62,7 @@ public ResponseEntity> getCacheInfoList() { } /** - * Clear all caches - * - * Jersey: GET /WebAPI/cache/clear - * Spring MVC: GET /WebAPI/v2/cache/clear + * Clear all caches. */ @GetMapping(value = "/clear", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity clearAll() { diff --git a/src/main/java/org/ohdsi/webapi/shiro/management/FilterChainBuilder.java b/src/main/java/org/ohdsi/webapi/shiro/management/FilterChainBuilder.java index 45409af898..bc94728c04 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/management/FilterChainBuilder.java +++ b/src/main/java/org/ohdsi/webapi/shiro/management/FilterChainBuilder.java @@ -72,7 +72,7 @@ public FilterChainBuilder addPath(String path, FilterTemplates... filters) { public FilterChainBuilder addPath(String path, String filters) { path = path.replaceAll("/+$", ""); - // Prepend /WebAPI to match JAX-RS @ApplicationPath("/WebAPI") + // Prepend /WebAPI to match the application context path if (!path.startsWith("/WebAPI") && !path.equals("/**") && !path.equals("/*")) { path = "/WebAPI" + path; } diff --git a/src/main/java/org/ohdsi/webapi/source/SourceService.java b/src/main/java/org/ohdsi/webapi/source/SourceService.java index f23fbceaec..6fddf4e074 100644 --- a/src/main/java/org/ohdsi/webapi/source/SourceService.java +++ b/src/main/java/org/ohdsi/webapi/source/SourceService.java @@ -38,6 +38,7 @@ import jakarta.annotation.PostConstruct; import jakarta.persistence.PersistenceException; import javax.cache.CacheManager; +import org.springframework.context.annotation.Lazy; import javax.cache.configuration.MutableConfiguration; import java.io.IOException; import java.util.*; @@ -95,7 +96,7 @@ public SourceService(SourceRepository sourceRepository, PBEStringEncryptor defaultStringEncryptor, SourceAccessor sourceAccessor, GenericConversionService conversionService, - VocabularyService vocabularyService, + @Lazy VocabularyService vocabularyService, ApplicationEventPublisher publisher) { this.sourceRepository = sourceRepository; this.sourceDaimonRepository = sourceDaimonRepository; diff --git a/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportServiceImpl.java b/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportServiceImpl.java index 0ef046c962..5d50f8e0d2 100644 --- a/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/user/importer/service/UserImportServiceImpl.java @@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -104,7 +105,7 @@ public UserImportServiceImpl(@Autowired(required = false) ActiveDirectoryProvide UserImportJobRepository userImportJobRepository, PermissionManager userManager, RoleGroupRepository roleGroupMappingRepository, - @Autowired(required = false) UserImportJobService userImportJobService, + @Lazy @Autowired(required = false) UserImportJobService userImportJobService, GenericConversionService conversionService) { this.userRepository = userRepository; diff --git a/src/test/java/org/ohdsi/webapi/tagging/ReusableTaggingTest.java b/src/test/java/org/ohdsi/webapi/tagging/ReusableTaggingTest.java index c300270939..2cd89e5453 100644 --- a/src/test/java/org/ohdsi/webapi/tagging/ReusableTaggingTest.java +++ b/src/test/java/org/ohdsi/webapi/tagging/ReusableTaggingTest.java @@ -1,6 +1,6 @@ package org.ohdsi.webapi.tagging; -import org.ohdsi.webapi.reusable.ReusableMvcController; +import org.ohdsi.webapi.reusable.ReusableService; import org.ohdsi.webapi.reusable.dto.ReusableDTO; import org.ohdsi.webapi.reusable.repository.ReusableRepository; import org.ohdsi.webapi.tag.domain.Tag; @@ -12,7 +12,7 @@ public class ReusableTaggingTest extends BaseTaggingTest { @Autowired - private ReusableMvcController controller; + private ReusableService service; @Autowired private ReusableRepository repository; @@ -24,12 +24,12 @@ public void doCreateInitialData() throws IOException { dto.setName("test name"); dto.setDescription("test description"); - initialDTO = controller.create(dto).getBody(); + initialDTO = service.create(dto); } @Override protected ReusableDTO doCopyData(ReusableDTO def) { - return controller.copy(def.getId()).getBody(); + return service.copy(def.getId()); } @Override @@ -44,27 +44,27 @@ protected String getExpressionPath() { @Override protected void assignTag(Integer id, boolean isPermissionProtected) { - controller.assignTag(id, getTag(isPermissionProtected).getId()); + service.assignTag(id, getTag(isPermissionProtected).getId()); } @Override protected void unassignTag(Integer id, boolean isPermissionProtected) { - controller.unassignTag(id, getTag(isPermissionProtected).getId()); + service.unassignTag(id, getTag(isPermissionProtected).getId()); } @Override protected void assignProtectedTag(Integer id, boolean isPermissionProtected) { - controller.assignPermissionProtectedTag(id, getTag(isPermissionProtected).getId()); + service.assignPermissionProtectedTag(id, getTag(isPermissionProtected).getId()); } @Override protected void unassignProtectedTag(Integer id, boolean isPermissionProtected) { - controller.unassignPermissionProtectedTag(id, getTag(isPermissionProtected).getId()); + service.unassignPermissionProtectedTag(id, getTag(isPermissionProtected).getId()); } @Override protected ReusableDTO getDTO(Integer id) { - return controller.get(id).getBody(); + return service.getDTOById(id); } @Override @@ -75,7 +75,7 @@ protected Integer getId(ReusableDTO dto) { @Override protected void assignTags(Integer id, Tag...tags) { for (Tag tag : tags) { - controller.assignTag(id, tag.getId()); + service.assignTag(id, tag.getId()); } } @@ -83,6 +83,6 @@ protected void assignTags(Integer id, Tag...tags) { protected List getDTOsByTag(List tagNames) { TagNameListRequestDTO requestDTO = new TagNameListRequestDTO(); requestDTO.setNames(tagNames); - return controller.listByTags(requestDTO).getBody(); + return service.listByTags(requestDTO); } } From bf051fc9875ba3dffc2a937c3272454584b2d953 Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:44:19 +0800 Subject: [PATCH 07/18] fix --- src/main/java/org/ohdsi/webapi/WebApi.java | 3 ++- src/main/java/org/ohdsi/webapi/activity/Tracker.java | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/ohdsi/webapi/WebApi.java b/src/main/java/org/ohdsi/webapi/WebApi.java index e4bbf74fcb..51deb41d3f 100644 --- a/src/main/java/org/ohdsi/webapi/WebApi.java +++ b/src/main/java/org/ohdsi/webapi/WebApi.java @@ -5,6 +5,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @@ -21,7 +22,7 @@ * - WAR: Deploy to external servlet container (mvn package -Pwar) */ @EnableScheduling -@SpringBootApplication(exclude={HibernateJpaAutoConfiguration.class, ErrorMvcAutoConfiguration.class}) +@SpringBootApplication(exclude={HibernateJpaAutoConfiguration.class, ErrorMvcAutoConfiguration.class, LdapAutoConfiguration.class}) @EnableAspectJAutoProxy(proxyTargetClass = true) public class WebApi extends SpringBootServletInitializer { diff --git a/src/main/java/org/ohdsi/webapi/activity/Tracker.java b/src/main/java/org/ohdsi/webapi/activity/Tracker.java index 6c2f1441e1..291d2387d4 100644 --- a/src/main/java/org/ohdsi/webapi/activity/Tracker.java +++ b/src/main/java/org/ohdsi/webapi/activity/Tracker.java @@ -57,11 +57,6 @@ public static Object[] getActivity() { return activityLog.toArray(); } - /** - * Get latest activity - * - * @deprecated DO NOT USE - will be removed in future release - */ @GetMapping(value = "/latest", produces = MediaType.APPLICATION_JSON_VALUE) @Deprecated public Object[] getLatestActivity() { From f09602495d1f66636b136b2b78147c6af396c17d Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:58:40 +0800 Subject: [PATCH 08/18] fix --- src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java | 2 +- src/main/java/org/ohdsi/webapi/info/InfoService.java | 2 +- .../java/org/ohdsi/webapi/job/NotificationServiceImpl.java | 2 +- .../org/ohdsi/webapi/mvc/controller/CacheMvcController.java | 2 +- src/main/java/org/ohdsi/webapi/reusable/ReusableService.java | 4 ++-- .../org/ohdsi/webapi/service/CohortDefinitionService.java | 4 ++-- src/main/java/org/ohdsi/webapi/service/ConceptSetService.java | 4 ++-- src/main/java/org/ohdsi/webapi/tag/TagService.java | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java b/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java index dedfb0f07e..873f966dd8 100644 --- a/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java @@ -94,7 +94,7 @@ public String getLocaleResource(Locale locale) { * * Note: Locale is resolved by LocaleInterceptor and stored in LocaleContextHolder */ - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public String getResources() { // Get locale from LocaleContextHolder (set by LocaleInterceptor) Locale locale = LocaleContextHolder.getLocale(); diff --git a/src/main/java/org/ohdsi/webapi/info/InfoService.java b/src/main/java/org/ohdsi/webapi/info/InfoService.java index b14fc4070b..a15a841364 100644 --- a/src/main/java/org/ohdsi/webapi/info/InfoService.java +++ b/src/main/java/org/ohdsi/webapi/info/InfoService.java @@ -50,7 +50,7 @@ public InfoService(BuildProperties buildProperties, BuildInfo buildInfo, List list( @RequestParam(value = "hide_statuses", required = false) String hideStatuses, diff --git a/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java index 4c3ff11dc4..121a460ed2 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java @@ -42,7 +42,7 @@ public CacheMvcController(CacheManager cacheManager) { /** * Get list of all caches with statistics. */ - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> getCacheInfoList() { List caches = new ArrayList<>(); diff --git a/src/main/java/org/ohdsi/webapi/reusable/ReusableService.java b/src/main/java/org/ohdsi/webapi/reusable/ReusableService.java index a4dd80b517..04f699212c 100644 --- a/src/main/java/org/ohdsi/webapi/reusable/ReusableService.java +++ b/src/main/java/org/ohdsi/webapi/reusable/ReusableService.java @@ -70,7 +70,7 @@ public ReusableService( this.versionService = versionService; } - @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public ReusableDTO create(@RequestBody ReusableDTO dto) { return createInternal(dto); } @@ -104,7 +104,7 @@ public List list() { return reusableRepository.findAll(); } - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public Page page(@Pagination Pageable pageable) { return reusableRepository.findAll(pageable) .map(reusable -> { diff --git a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java index 7e56a5d542..6f67796192 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java @@ -434,7 +434,7 @@ public GenerateSqlResult generateSql(@RequestBody GenerateSqlRequest request) { * @return List of metadata about all cohort definitions in WebAPI * @see org.ohdsi.webapi.cohortdefinition.CohortMetadataDTO */ - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @Transactional @Cacheable(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()") public List getCohortDefinitionList() { @@ -460,7 +460,7 @@ public List getCohortDefinitionList() { * @param dto The cohort definition to create. * @return The newly created cohort definition */ - @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) public CohortDTO createCohortDefinition(@RequestBody CohortDTO dto) { diff --git a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java index 0641d9e827..c77ac96ed2 100644 --- a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java +++ b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java @@ -174,7 +174,7 @@ public ConceptSetDTO getConceptSet(@PathVariable("id") final int id) { * @summary Get all concept sets * @return A list of all concept sets in the WebAPI database */ - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @Cacheable(cacheNames = ConceptSetService.CachingSetup.CONCEPT_SET_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()") public Collection getConceptSets() { return getTransactionTemplate().execute( @@ -480,7 +480,7 @@ public ResponseEntity exportConceptSetToCSV(@PathVariable("id") final St * @param conceptSetDTO The concept set to save * @return The concept set saved with the concept set identifier */ - @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) public ConceptSetDTO createConceptSet(@RequestBody ConceptSetDTO conceptSetDTO) { diff --git a/src/main/java/org/ohdsi/webapi/tag/TagService.java b/src/main/java/org/ohdsi/webapi/tag/TagService.java index 1be1afbdad..1ca5e430e2 100644 --- a/src/main/java/org/ohdsi/webapi/tag/TagService.java +++ b/src/main/java/org/ohdsi/webapi/tag/TagService.java @@ -60,7 +60,7 @@ public TagService( * @param dto * @return */ - @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public TagDTO create(@RequestBody TagDTO dto) { Tag tag = conversionService.convert(dto, Tag.class); Tag saved = create(tag); @@ -126,7 +126,7 @@ public List listInfoDTO(@RequestParam("namePart") String namePart) { * * @return */ - @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public List listInfoDTO() { return listInfo().stream() .map(tag -> conversionService.convert(tag, TagDTO.class)) From 14ede1b428d17998365945f0e40a182c8207afb5 Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:12:43 +0800 Subject: [PATCH 09/18] fix --- .../java/org/ohdsi/webapi/shiro/PermissionManager.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/ohdsi/webapi/shiro/PermissionManager.java b/src/main/java/org/ohdsi/webapi/shiro/PermissionManager.java index 28e63d66e3..cc20bd6375 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/PermissionManager.java +++ b/src/main/java/org/ohdsi/webapi/shiro/PermissionManager.java @@ -547,6 +547,12 @@ private UserRoleEntity addUser(final UserEntity user, final RoleEntity role, public String getSubjectName() { Subject subject = SecurityUtils.getSubject(); + + // Return "anonymous" if subject is not authenticated or has no principals + if (subject == null || !subject.isAuthenticated() || subject.getPrincipals() == null) { + return "anonymous"; + } + Object principalObject = subject.getPrincipals().getPrimaryPrincipal(); if (principalObject instanceof String) @@ -557,7 +563,7 @@ public String getSubjectName() { return principal.getName(); } - throw new UnsupportedOperationException(); + return "anonymous"; } public RoleEntity getRole(Long id) { From 4a3c474acf1ba06500cf4850374662883c042d6b Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:19:15 +0800 Subject: [PATCH 10/18] fix --- .../java/org/ohdsi/webapi/ShiroConfiguration.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/org/ohdsi/webapi/ShiroConfiguration.java b/src/main/java/org/ohdsi/webapi/ShiroConfiguration.java index d8167ccf9e..865b756a29 100644 --- a/src/main/java/org/ohdsi/webapi/ShiroConfiguration.java +++ b/src/main/java/org/ohdsi/webapi/ShiroConfiguration.java @@ -119,6 +119,20 @@ public DataSourceAccessBeanPostProcessor dataSourceAccessBeanPostProcessor(DataS return new DataSourceAccessBeanPostProcessor(parameterResolver, proxyTargetClass); } + /** + * Register the Shiro filter with the servlet container. + * This is necessary for Spring Boot to properly apply the filter to all requests. + */ + @Bean + public FilterRegistrationBean shiroFilterRegistration(ShiroFilterFactoryBean shiroFilterFactoryBean) throws Exception { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter((AbstractShiroFilter) shiroFilterFactoryBean.getObject()); + registration.addUrlPatterns("/*"); + registration.setName("shiroFilter"); + registration.setOrder(1); // Run before other filters + return registration; + } + private Collection getJwtAuthRealmForAuthorization(Security security) { return security.getRealms().stream() From 9ecdff441a61627d526ed9ce67662e14e8d69b21 Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:13:16 +0800 Subject: [PATCH 11/18] fix --- docker/auth-test/setup-test-users.sql | 17 + .../test-results/auth-test-results.xml | 298 ++++++++++++++++++ .../org/ohdsi/webapi/ShiroConfiguration.java | 9 + .../AuthenticatingPropagationFilter.java | 12 + .../filters/SendTokenInHeaderFilter.java | 4 +- .../filters/auth/AbstractLdapAuthFilter.java | 22 ++ .../filters/auth/AtlasJwtAuthFilter.java | 9 + .../shiro/filters/auth/JdbcAuthFilter.java | 17 + .../filters/auth/KerberosAuthFilter.java | 13 + 9 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 docker/auth-test/test-results/auth-test-results.xml diff --git a/docker/auth-test/setup-test-users.sql b/docker/auth-test/setup-test-users.sql index 93e6bb58d0..b36e2c1d00 100644 --- a/docker/auth-test/setup-test-users.sql +++ b/docker/auth-test/setup-test-users.sql @@ -1,3 +1,20 @@ +-- Initial security data (roles required for the application to function) +-- These must be created before WebAPI can properly handle authentication +INSERT INTO webapi.sec_role (id, name, system_role) VALUES (1, 'public', true) ON CONFLICT (id) DO NOTHING; +INSERT INTO webapi.sec_role (id, name, system_role) VALUES (2, 'admin', true) ON CONFLICT (id) DO NOTHING; +INSERT INTO webapi.sec_role (id, name, system_role) VALUES (1001, 'Atlas users', true) ON CONFLICT (id) DO NOTHING; +INSERT INTO webapi.sec_role (id, name, system_role) VALUES (1002, 'Moderator', true) ON CONFLICT (id) DO NOTHING; + +-- Anonymous user (required for public endpoints) +INSERT INTO webapi.sec_user (id, login, name) VALUES (1, 'anonymous', 'anonymous') ON CONFLICT (id) DO NOTHING; +INSERT INTO webapi.sec_user_role (id, user_id, role_id, origin) VALUES (1, 1, 1, 'SYSTEM') ON CONFLICT (id) DO NOTHING; + +-- Update sequences to avoid conflicts +SELECT setval('webapi.sec_role_sequence', GREATEST((SELECT COALESCE(MAX(id), 0) + 1 FROM webapi.sec_role), nextval('webapi.sec_role_sequence'))); +SELECT setval('webapi.sec_user_sequence', GREATEST((SELECT COALESCE(MAX(id), 0) + 1 FROM webapi.sec_user), nextval('webapi.sec_user_sequence'))); +SELECT setval('webapi.sec_user_role_sequence', GREATEST((SELECT COALESCE(MAX(id), 0) + 1 FROM webapi.sec_user_role), nextval('webapi.sec_user_role_sequence'))); + +-- Test users table for JDBC authentication CREATE TABLE IF NOT EXISTS webapi.users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, diff --git a/docker/auth-test/test-results/auth-test-results.xml b/docker/auth-test/test-results/auth-test-results.xml new file mode 100644 index 0000000000..fe1e77cd2b --- /dev/null +++ b/docker/auth-test/test-results/auth-test-results.xml @@ -0,0 +1,298 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/ShiroConfiguration.java b/src/main/java/org/ohdsi/webapi/ShiroConfiguration.java index 865b756a29..301998d595 100644 --- a/src/main/java/org/ohdsi/webapi/ShiroConfiguration.java +++ b/src/main/java/org/ohdsi/webapi/ShiroConfiguration.java @@ -64,6 +64,15 @@ public ShiroFilterFactoryBean shiroFilter(Security security, LockoutPolicy locko Map filterChain = security.getFilterChain(); + // Debug: log the filter chain configuration + log.info("=== Shiro Filter Chain Configuration ==="); + log.info("Security implementation: {}", security.getClass().getName()); + log.info("Number of filters: {}", filters.size()); + log.info("Filter names: {}", filters.keySet()); + log.info("Filter chain paths ({} entries):", filterChain.size()); + filterChain.forEach((path, chain) -> log.info(" {} -> {}", path, chain)); + log.info("=== End Shiro Filter Chain Configuration ==="); + shiroFilter.setFilterChainDefinitionMap(filterChain); return shiroFilter; diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/AuthenticatingPropagationFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/AuthenticatingPropagationFilter.java index 5506eadd67..bdb000d8d1 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/AuthenticatingPropagationFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/AuthenticatingPropagationFilter.java @@ -4,6 +4,7 @@ import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletResponse; import org.ohdsi.webapi.arachne.logging.event.FailedLoginEvent; +import java.io.IOException; import org.ohdsi.webapi.arachne.logging.event.SuccessLoginEvent; import org.ohdsi.webapi.shiro.ServletBridge; import org.apache.shiro.authc.AuthenticationException; @@ -55,6 +56,17 @@ protected boolean onLoginFailure(AuthenticationToken token, AuthenticationExcept boolean result = super.onLoginFailure(token, e, request, response); eventPublisher.publishEvent(new FailedLoginEvent(this, username)); eventPublisher.publishEvent(new AuditTrailLoginFailedEvent(this, username, request.getRemoteHost())); + + // Write response body and commit to prevent Spring MVC from processing + try { + httpResponse.setContentType("application/json"); + String errorMessage = e instanceof LockedAccountException ? e.getMessage() : "Invalid credentials"; + httpResponse.getWriter().write("{\"error\":\"" + errorMessage + "\"}"); + httpResponse.flushBuffer(); // Commit the response + } catch (IOException ioe) { + // Response may already be committed, ignore + } + return result; } } diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/SendTokenInHeaderFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/SendTokenInHeaderFilter.java index 7642ea019a..3a9798d44d 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/SendTokenInHeaderFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/SendTokenInHeaderFilter.java @@ -44,8 +44,10 @@ protected boolean preHandle(ServletRequest request, ServletResponse response) { httpResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); httpResponse.setStatus(HttpServletResponse.SC_OK); - try (final PrintWriter responseWriter = response.getWriter()) { + try { + final PrintWriter responseWriter = response.getWriter(); responseWriter.print(objectMapper.writeValueAsString(permissions)); + httpResponse.flushBuffer(); // Commit the response } catch (IOException e) { LOGGER.error(ERROR_WRITING_PERMISSIONS_TO_RESPONSE_LOG, e); } diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/auth/AbstractLdapAuthFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/auth/AbstractLdapAuthFilter.java index bf0cac4387..fc4617f1b2 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/auth/AbstractLdapAuthFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/auth/AbstractLdapAuthFilter.java @@ -18,16 +18,23 @@ */ package org.ohdsi.webapi.shiro.filters.auth; +import java.io.IOException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; import org.apache.shiro.authc.AuthenticationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.ohdsi.webapi.shiro.filters.AuthenticatingPropagationFilter; import org.springframework.context.ApplicationEventPublisher; public abstract class AbstractLdapAuthFilter extends AuthenticatingPropagationFilter { + + private static final Logger log = LoggerFactory.getLogger(AbstractLdapAuthFilter.class); + protected AbstractLdapAuthFilter(ApplicationEventPublisher eventPublisher) { super(eventPublisher); } @@ -58,8 +65,23 @@ protected boolean onAccessDenied(ServletRequest request, ServletResponse respons if (request.getParameter("login") != null) { loggedIn = executeLogin(request, response); + } else { + // No credentials provided - write error response and commit + writeUnauthorizedResponse(response, "Missing credentials"); } return loggedIn; } + + private void writeUnauthorizedResponse(ServletResponse response, String message) { + try { + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpResponse.setContentType("application/json"); + httpResponse.getWriter().write("{\"error\":\"" + message + "\"}"); + httpResponse.flushBuffer(); // Commit the response + } catch (IOException e) { + log.error("Failed to write unauthorized response", e); + } + } } diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/auth/AtlasJwtAuthFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/auth/AtlasJwtAuthFilter.java index 7a23d9bf6d..9664bbf5e9 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/auth/AtlasJwtAuthFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/auth/AtlasJwtAuthFilter.java @@ -2,6 +2,7 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; +import java.io.IOException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletResponse; @@ -46,6 +47,14 @@ protected boolean onAccessDenied(ServletRequest request, ServletResponse respons if (!loggedIn) { HttpServletResponse httpResponse = ServletBridge.toHttp(response); httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + // Write response body and commit to prevent Spring MVC from processing + try { + httpResponse.setContentType("application/json"); + httpResponse.getWriter().write("{\"error\":\"Invalid or missing JWT token\"}"); + httpResponse.flushBuffer(); // Commit the response + } catch (IOException e) { + logger.error("Failed to write unauthorized response", e); + } } return loggedIn; diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/auth/JdbcAuthFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/auth/JdbcAuthFilter.java index 865e6618a3..fdc562296c 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/auth/JdbcAuthFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/auth/JdbcAuthFilter.java @@ -24,8 +24,10 @@ import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; import org.ohdsi.webapi.shiro.filters.AuthenticatingPropagationFilter; +import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; @@ -57,8 +59,23 @@ protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse if (servletRequest.getParameter("login") != null) { loggedIn = executeLogin(servletRequest, servletResponse); + } else { + // No credentials provided - write error response and commit + writeUnauthorizedResponse(servletResponse, "Missing credentials"); } return loggedIn; } + private void writeUnauthorizedResponse(ServletResponse response, String message) { + try { + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpResponse.setContentType("application/json"); + httpResponse.getWriter().write("{\"error\":\"" + message + "\"}"); + httpResponse.flushBuffer(); // Commit the response + } catch (IOException e) { + log.error("Failed to write unauthorized response", e); + } + } + } diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/auth/KerberosAuthFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/auth/KerberosAuthFilter.java index 62540200c1..2e84b827e8 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/auth/KerberosAuthFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/auth/KerberosAuthFilter.java @@ -18,12 +18,15 @@ */ package org.ohdsi.webapi.shiro.filters.auth; +import java.io.IOException; import java.util.Base64; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.ohdsi.webapi.shiro.ServletBridge; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; @@ -31,6 +34,8 @@ public class KerberosAuthFilter extends AuthenticatingFilter { + private static final Logger log = LoggerFactory.getLogger(KerberosAuthFilter.class); + private String getAuthHeader(ServletRequest servletRequest) { HttpServletRequest request = ServletBridge.toHttp(servletRequest); @@ -69,6 +74,14 @@ protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse HttpServletResponse response = ServletBridge.toHttp(servletResponse); response.addHeader("WWW-Authenticate", "Negotiate"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + // Write response body and commit to prevent Spring MVC from processing + try { + response.setContentType("application/json"); + response.getWriter().write("{\"error\":\"Kerberos authentication required\"}"); + response.flushBuffer(); // Commit the response + } catch (IOException e) { + log.error("Failed to write unauthorized response", e); + } } return loggedIn; From e67cf4e0cd3f965885ac82980c20b180c0e6690b Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:49:45 +0800 Subject: [PATCH 12/18] fix --- .../test-results/auth-test-results.xml | 130 +++++++++--------- .../shiro/management/FilterChainBuilder.java | 7 +- 2 files changed, 68 insertions(+), 69 deletions(-) diff --git a/docker/auth-test/test-results/auth-test-results.xml b/docker/auth-test/test-results/auth-test-results.xml index fe1e77cd2b..1273c790da 100644 --- a/docker/auth-test/test-results/auth-test-results.xml +++ b/docker/auth-test/test-results/auth-test-results.xml @@ -1,24 +1,24 @@ - - - - - + + + + + - - - - + + + + - - - - - + + + + + - - - + + + @@ -31,9 +31,9 @@ - - - + + + @@ -46,15 +46,15 @@ - + - - + + @@ -66,7 +66,7 @@ testScriptError: Cannot read properties of undefined (reading 'replace') at Object.eval sandbox-script.js:1:6).]]> - + @@ -79,7 +79,7 @@ testScriptError: Cannot read properties of undefined (reading 'replace') - + - - - + + + @@ -155,9 +155,9 @@ testScriptError: Cannot read properties of undefined (reading 'match') - - - + + + @@ -170,13 +170,13 @@ testScriptError: Cannot read properties of undefined (reading 'match') - - - + + + - - - + + + @@ -188,7 +188,7 @@ testScriptError: Cannot read properties of undefined (reading 'match') at Object.eval sandbox-script.js:1:11).]]> - + @@ -201,9 +201,9 @@ testScriptError: Cannot read properties of undefined (reading 'match') - - - + + + @@ -216,9 +216,9 @@ testScriptError: Cannot read properties of undefined (reading 'match') - - - + + + @@ -231,18 +231,18 @@ testScriptError: Cannot read properties of undefined (reading 'match') - - - + + + - + - - - + + + @@ -255,9 +255,9 @@ testScriptError: Cannot read properties of undefined (reading 'match') - - - + + + @@ -270,9 +270,9 @@ testScriptError: Cannot read properties of undefined (reading 'match') - - - + + + @@ -285,14 +285,14 @@ testScriptError: Cannot read properties of undefined (reading 'match') - - - - + + + + - - - - + + + + \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/shiro/management/FilterChainBuilder.java b/src/main/java/org/ohdsi/webapi/shiro/management/FilterChainBuilder.java index bc94728c04..109e13cd04 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/management/FilterChainBuilder.java +++ b/src/main/java/org/ohdsi/webapi/shiro/management/FilterChainBuilder.java @@ -72,10 +72,9 @@ public FilterChainBuilder addPath(String path, FilterTemplates... filters) { public FilterChainBuilder addPath(String path, String filters) { path = path.replaceAll("/+$", ""); - // Prepend /WebAPI to match the application context path - if (!path.startsWith("/WebAPI") && !path.equals("/**") && !path.equals("/*")) { - path = "/WebAPI" + path; - } + // Note: Shiro's PathMatchingFilterChainResolver uses WebUtils.getPathWithinApplication() + // which returns paths RELATIVE to the context path (e.g., /user/login/db not /WebAPI/user/login/db). + // DO NOT prepend /WebAPI here - Shiro works with servlet-relative paths. this.filterChain.put(path, filters); From 8cb4b3fc02b12e3a906b78df3c6f6fb4ca7dbf20 Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sun, 25 Jan 2026 00:04:23 +0800 Subject: [PATCH 13/18] fix --- .../test-results/auth-test-results.xml | 298 ------------------ .../filters/SkipFurtherFilteringFilter.java | 4 +- .../filters/UrlBasedAuthorizingFilter.java | 9 +- 3 files changed, 10 insertions(+), 301 deletions(-) delete mode 100644 docker/auth-test/test-results/auth-test-results.xml diff --git a/docker/auth-test/test-results/auth-test-results.xml b/docker/auth-test/test-results/auth-test-results.xml deleted file mode 100644 index 1273c790da..0000000000 --- a/docker/auth-test/test-results/auth-test-results.xml +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/SkipFurtherFilteringFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/SkipFurtherFilteringFilter.java index cdee599c48..65f40c0f7d 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/SkipFurtherFilteringFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/SkipFurtherFilteringFilter.java @@ -25,7 +25,9 @@ public void init(FilterConfig fc) throws ServletException { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (shouldSkip(request, response)) { HttpServletRequest httpRequest = ServletBridge.toHttp(request); - String path = httpRequest.getServletPath() + httpRequest.getPathInfo(); + // getPathInfo() can return null if there's no extra path info + String pathInfo = httpRequest.getPathInfo(); + String path = httpRequest.getServletPath() + (pathInfo != null ? pathInfo : ""); RequestDispatcher requestDispatcher = request.getRequestDispatcher(path); requestDispatcher.forward(request, response); } diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/UrlBasedAuthorizingFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/UrlBasedAuthorizingFilter.java index 8e1df89d33..5876f19e00 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/UrlBasedAuthorizingFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/UrlBasedAuthorizingFilter.java @@ -17,8 +17,13 @@ public class UrlBasedAuthorizingFilter extends AdviceFilter { @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpRequest = ServletBridge.toHttp(request); - - String path = httpRequest.getPathInfo() + + // getPathInfo() can return null if there's no extra path info + // Use servlet path as fallback, which is always non-null + String pathInfo = httpRequest.getPathInfo(); + String rawPath = (pathInfo != null) ? pathInfo : httpRequest.getServletPath(); + + String path = rawPath .replaceAll("^/+", "") .replaceAll("/+$", "") // replace special characters From f9be8d38f5f49c979499f12c740a8cc293d557bb Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sun, 25 Jan 2026 00:14:12 +0800 Subject: [PATCH 14/18] cleanup --- .../webapi/mvc/GlobalExceptionHandler.java | 6 +--- .../service/CohortDefinitionService.java | 24 ++++++++-------- .../webapi/service/ConceptSetService.java | 9 +++--- .../shiro/filters/AtlasCallbackFilter.java | 28 +++++++------------ 4 files changed, 26 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java b/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java index ca088de967..9d96cfe5a2 100644 --- a/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java +++ b/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java @@ -23,11 +23,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException; import org.springframework.web.server.ResponseStatusException; -import org.springframework.http.HttpStatus; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.http.HttpStatus; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.http.HttpStatus; + import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; diff --git a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java index 6f67796192..e5412d1238 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java @@ -135,14 +135,11 @@ import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; -import org.springframework.http.ResponseEntity; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.http.HttpStatus; -import org.springframework.web.server.ResponseStatusException; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; /** * Provides REST services for working with cohort definitions. @@ -645,15 +642,16 @@ public JobExecutionResource generateCohort(@PathVariable("id") final int id, public ResponseEntity cancelGenerateCohort(@PathVariable("id") final int id, @PathVariable("sourceKey") final String sourceKey) { final Source source = Optional.ofNullable(getSourceRepository().findBySourceKey(sourceKey)) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, + String.format("Source with key '%s' not found", sourceKey))); getTransactionTemplateRequiresNew().execute(status -> { - CohortDefinition currentDefinition = cohortDefinitionRepository.findById(id).orElse(null); - if (Objects.nonNull(currentDefinition)) { - CohortGenerationInfo info = findBySourceId(currentDefinition.getGenerationInfoList(), source.getSourceId()); - if (Objects.nonNull(info)) { - invalidateExecution(info); - cohortDefinitionRepository.save(currentDefinition); - } + CohortDefinition currentDefinition = cohortDefinitionRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, + String.format("Cohort Definition with id = %d not found", id))); + CohortGenerationInfo info = findBySourceId(currentDefinition.getGenerationInfoList(), source.getSourceId()); + if (Objects.nonNull(info)) { + invalidateExecution(info); + cohortDefinitionRepository.save(currentDefinition); } return null; }); diff --git a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java index c77ac96ed2..718acab8e1 100644 --- a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java +++ b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java @@ -535,12 +535,11 @@ public List getNamesLike(String copyName) { @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) public ConceptSetDTO updateConceptSet( @PathVariable("id") final int id, - @RequestBody ConceptSetDTO conceptSetDTO) throws Exception { + @RequestBody ConceptSetDTO conceptSetDTO) { - ConceptSet updated = getConceptSetRepository().findById(id).orElse(null); - if (updated == null) { - throw new Exception("Concept Set does not exist."); - } + ConceptSet updated = getConceptSetRepository().findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, + String.format("Concept Set with id = %d does not exist.", id))); saveVersion(id); diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/AtlasCallbackFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/AtlasCallbackFilter.java index 5dd4258390..b8578b43e4 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/AtlasCallbackFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/AtlasCallbackFilter.java @@ -35,9 +35,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo HttpServletRequest request = ServletBridge.toHttp(servletRequest); HttpServletResponse response = ServletBridge.toHttp(servletResponse); - // Use WARN level to ensure it appears in logs - logger.warn("AtlasCallbackFilter.doFilter ENTERED for URI: {}", request.getRequestURI()); - System.out.println("AtlasCallbackFilter.doFilter ENTERED for URI: " + request.getRequestURI()); + logger.debug("AtlasCallbackFilter.doFilter ENTERED for URI: {}", request.getRequestURI()); // Wrap the response to intercept redirects (via sendRedirect OR setHeader/setStatus) RedirectCapturingResponseWrapper responseWrapper = new RedirectCapturingResponseWrapper(response); @@ -45,28 +43,26 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo // Execute the parent callback filter with the wrapped response super.doFilter(request, responseWrapper, filterChain); - logger.warn("AtlasCallbackFilter: After parent filter - redirectLocation='{}', status={}, isRedirect={}", + logger.debug("AtlasCallbackFilter: After parent filter - redirectLocation='{}', status={}, isRedirect={}", responseWrapper.getRedirectLocation(), responseWrapper.getCapturedStatus(), responseWrapper.isRedirect()); - System.out.println("AtlasCallbackFilter: After parent filter - redirectLocation=" + responseWrapper.getRedirectLocation()); // If a redirect was captured (either via sendRedirect or setHeader/setStatus), override it if (responseWrapper.getRedirectLocation() != null) { String capturedRedirect = responseWrapper.getRedirectLocation(); - logger.warn("AtlasCallbackFilter: Intercepted redirect to '{}', atlasRedirectUrl='{}'", + logger.debug("AtlasCallbackFilter: Intercepted redirect to '{}', atlasRedirectUrl='{}'", capturedRedirect, atlasRedirectUrl); // Only override if it's not already pointing to Atlas and we have a configured URL if (atlasRedirectUrl != null && !capturedRedirect.contains("/atlas/")) { - logger.warn("AtlasCallbackFilter: Overriding redirect to Atlas UI: {}", atlasRedirectUrl); + logger.debug("AtlasCallbackFilter: Overriding redirect to Atlas UI: {}", atlasRedirectUrl); response.sendRedirect(atlasRedirectUrl); } else { // Use the original redirect - logger.warn("AtlasCallbackFilter: Using original redirect: {}", capturedRedirect); + logger.debug("AtlasCallbackFilter: Using original redirect: {}", capturedRedirect); response.sendRedirect(capturedRedirect); } } else { - logger.warn("AtlasCallbackFilter: No redirect captured, response committed={}", response.isCommitted()); - System.out.println("AtlasCallbackFilter: No redirect captured, response committed=" + response.isCommitted()); + logger.debug("AtlasCallbackFilter: No redirect captured, response committed={}", response.isCommitted()); } } @@ -88,16 +84,14 @@ public RedirectCapturingResponseWrapper(HttpServletResponse response) { @Override public void sendRedirect(String location) throws IOException { // Don't actually redirect, just capture the location - logger.warn("RedirectCapturingResponseWrapper: sendRedirect called with '{}'", location); - System.out.println("RedirectCapturingResponseWrapper: sendRedirect called with '" + location + "'"); + logger.debug("RedirectCapturingResponseWrapper: sendRedirect called with '{}'", location); this.redirectLocation = location; this.statusCode = 302; } @Override public void setStatus(int sc) { - logger.warn("RedirectCapturingResponseWrapper: setStatus called with {}", sc); - System.out.println("RedirectCapturingResponseWrapper: setStatus called with " + sc); + logger.debug("RedirectCapturingResponseWrapper: setStatus called with {}", sc); this.statusCode = sc; // Don't pass through redirect status codes if (sc != 302 && sc != 301 && sc != 303 && sc != 307 && sc != 308) { @@ -107,8 +101,7 @@ public void setStatus(int sc) { @Override public void setHeader(String name, String value) { - logger.warn("RedirectCapturingResponseWrapper: setHeader called with '{}' = '{}'", name, value); - System.out.println("RedirectCapturingResponseWrapper: setHeader called with '" + name + "' = '" + value + "'"); + logger.debug("RedirectCapturingResponseWrapper: setHeader called with '{}' = '{}'", name, value); if ("Location".equalsIgnoreCase(name)) { this.redirectLocation = value; } else { @@ -118,8 +111,7 @@ public void setHeader(String name, String value) { @Override public void addHeader(String name, String value) { - logger.warn("RedirectCapturingResponseWrapper: addHeader called with '{}' = '{}'", name, value); - System.out.println("RedirectCapturingResponseWrapper: addHeader called with '" + name + "' = '" + value + "'"); + logger.debug("RedirectCapturingResponseWrapper: addHeader called with '{}' = '{}'", name, value); if ("Location".equalsIgnoreCase(name)) { this.redirectLocation = value; } else { From dced00e3e35e3b2cbda420d8eb3d486349b1f697 Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:32:32 +0800 Subject: [PATCH 15/18] fix --- .../postman/integration-tests.postman_collection.json | 8 ++++---- src/main/java/org/ohdsi/webapi/WebMvcConfig.java | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docker/integration-test/postman/integration-tests.postman_collection.json b/docker/integration-test/postman/integration-tests.postman_collection.json index 2112bb6543..16b1f122c1 100644 --- a/docker/integration-test/postman/integration-tests.postman_collection.json +++ b/docker/integration-test/postman/integration-tests.postman_collection.json @@ -929,9 +929,9 @@ "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/conceptset/", + "raw": "{{base_url}}/conceptset", "host": ["{{base_url}}"], - "path": ["conceptset", ""] + "path": ["conceptset"] } } }, @@ -986,9 +986,9 @@ "raw": "{\n \"name\": \"Test Concept Set\",\n \"description\": \"Integration test concept set\"\n}" }, "url": { - "raw": "{{base_url}}/conceptset/", + "raw": "{{base_url}}/conceptset", "host": ["{{base_url}}"], - "path": ["conceptset", ""] + "path": ["conceptset"] } } }, diff --git a/src/main/java/org/ohdsi/webapi/WebMvcConfig.java b/src/main/java/org/ohdsi/webapi/WebMvcConfig.java index 4c456fa857..fa98cb31d3 100644 --- a/src/main/java/org/ohdsi/webapi/WebMvcConfig.java +++ b/src/main/java/org/ohdsi/webapi/WebMvcConfig.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; @@ -19,6 +20,11 @@ public class WebMvcConfig implements WebMvcConfigurer { @Autowired(required = false) private LocaleInterceptor localeInterceptor; + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setUseTrailingSlashMatch(true); + } + @Override public void addInterceptors(InterceptorRegistry registry) { // Add locale interceptor if available From eccbffe02e6642a90710fd15d1c1cec6c6d65db1 Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:18:11 +0800 Subject: [PATCH 16/18] fix --- .../ohdsi/webapi/service/VocabularyService.java | 2 +- .../webapi/trexsql/TrexSQLServletConfig.java | 2 +- .../ohdsi/webapi/test/VocabularyServiceIT.java | 15 +++++++++++++++ src/test/resources/application-test.properties | 2 ++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/ohdsi/webapi/service/VocabularyService.java b/src/main/java/org/ohdsi/webapi/service/VocabularyService.java index 93ee2a02dc..3ddffe94dd 100644 --- a/src/main/java/org/ohdsi/webapi/service/VocabularyService.java +++ b/src/main/java/org/ohdsi/webapi/service/VocabularyService.java @@ -693,7 +693,7 @@ public Collection executeSearch( * @param rows The number of rows to return. * @return A collection of concepts */ - @GetMapping(value = "/{sourceKey}/searchByQuery", + @GetMapping(value = "/{sourceKey}/search", produces = MediaType.APPLICATION_JSON_VALUE, params = "query") public Collection executeSearch( diff --git a/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLServletConfig.java b/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLServletConfig.java index f4c8d11286..106f7bf32c 100644 --- a/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLServletConfig.java +++ b/src/main/java/org/ohdsi/webapi/trexsql/TrexSQLServletConfig.java @@ -38,7 +38,7 @@ public ServletRegistrationBean trexServlet( servlet.initTrex(sourceRepository, config); ServletRegistrationBean registration = - new ServletRegistrationBean<>(servlet, "/WebAPI/trexsql/*"); + new ServletRegistrationBean<>(servlet, "/trexsql/*"); registration.setLoadOnStartup(1); return registration; } diff --git a/src/test/java/org/ohdsi/webapi/test/VocabularyServiceIT.java b/src/test/java/org/ohdsi/webapi/test/VocabularyServiceIT.java index 21fb1d6206..e69ba2ee7e 100644 --- a/src/test/java/org/ohdsi/webapi/test/VocabularyServiceIT.java +++ b/src/test/java/org/ohdsi/webapi/test/VocabularyServiceIT.java @@ -23,6 +23,9 @@ public class VocabularyServiceIT extends WebApiIT { @Value("${vocabularyservice.endpoint.domains}") private String endpointDomains; + @Value("${vocabularyservice.endpoint.search}") + private String endpointSearch; + @Autowired private SourceRepository sourceRepository; @@ -72,4 +75,16 @@ public void canGetDomains() { //Assertion assertOK(entity); } + + @Test + public void canSearchConceptsWithQueryParam() { + // Test the GET /{sourceKey}/search?query=... endpoint format + String searchUrl = this.endpointSearch.replace("{sourceKey}", SOURCE_KEY) + "?query=test"; + + //Action + final ResponseEntity entity = getRestTemplate().getForEntity(searchUrl, String.class); + + //Assertion + assertOK(entity); + } } diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index eb181631ea..4600e23773 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -8,6 +8,8 @@ vocabularyservice.endpoint.vocabularies=${vocabularyservice.endpoint}/vocabulari vocabularyservice.endpoint.domains=${vocabularyservice.endpoint}/domains #GET concept vocabularyservice.endpoint.concept=${vocabularyservice.endpoint}/concept/1 +#GET search with query param +vocabularyservice.endpoint.search=${vocabularyservice.endpoint}/{sourceKey}/search #GET cohortdefinitions cohortdefinitionservice.endpoint.cohortdefinitions=${baseUri}/cohortdefinition From 07f0710f16d3176f4082751f8e612d86b34ffe1d Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:36:18 +0800 Subject: [PATCH 17/18] cleanup --- src/main/java/org/ohdsi/webapi/activity/Tracker.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/ohdsi/webapi/activity/Tracker.java b/src/main/java/org/ohdsi/webapi/activity/Tracker.java index 291d2387d4..9cf55c1dec 100644 --- a/src/main/java/org/ohdsi/webapi/activity/Tracker.java +++ b/src/main/java/org/ohdsi/webapi/activity/Tracker.java @@ -28,7 +28,6 @@ * Activity tracker service * * @author fdefalco - * @deprecated Example REST service - will be deprecated in a future release */ @RestController @RequestMapping("/activity") From e32370ee9729b3bb7b4adf47e67fa12ee6008484 Mon Sep 17 00:00:00 2001 From: Peter Hoffmann <954078+p-hoffmann@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:26:34 +0800 Subject: [PATCH 18/18] fix --- pom.xml | 1 + .../org/ohdsi/webapi/OidcConfCreator.java | 7 ++++ .../shiro/filters/OidcJwtAuthFilter.java | 40 +++++++++++++++---- .../management/AtlasRegularSecurity.java | 7 +++- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index cf24ae8a4e..e6ade4b67a 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,7 @@ {:} http://localhost/index.html#/welcome/ + cn={0},dc=example,dc=org diff --git a/src/main/java/org/ohdsi/webapi/OidcConfCreator.java b/src/main/java/org/ohdsi/webapi/OidcConfCreator.java index 35db51181e..ba7a8f1d47 100644 --- a/src/main/java/org/ohdsi/webapi/OidcConfCreator.java +++ b/src/main/java/org/ohdsi/webapi/OidcConfCreator.java @@ -61,6 +61,13 @@ public class OidcConfCreator { @Value("${security.oauth.callback.api}") private String oauthApiCallback; + @Value("${security.oid.apiResource:}") + private String apiResource; + + public String getApiResource() { + return apiResource; + } + /** * Returns the external OIDC URL for browser-facing endpoints. * If externalUrl is set, returns it; otherwise returns the discovery URL. diff --git a/src/main/java/org/ohdsi/webapi/shiro/filters/OidcJwtAuthFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/OidcJwtAuthFilter.java index 764eb3a0c1..091bf43753 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/filters/OidcJwtAuthFilter.java +++ b/src/main/java/org/ohdsi/webapi/shiro/filters/OidcJwtAuthFilter.java @@ -23,10 +23,12 @@ import org.slf4j.LoggerFactory; import java.net.URI; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; import java.text.ParseException; -import java.util.*; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -45,16 +47,34 @@ public class OidcJwtAuthFilter extends AtlasAuthFilter { private final OidcConfiguration oidcConfiguration; private final PermissionManager authorizer; private final Set defaultRoles; + private final Set acceptedAudiences; private final Map keyCache = new ConcurrentHashMap<>(); private volatile long lastJwksFetch = 0; + public OidcJwtAuthFilter(OidcConfiguration oidcConfiguration, + PermissionManager authorizer, + Set defaultRoles) { + this(oidcConfiguration, authorizer, defaultRoles, null); + } + public OidcJwtAuthFilter(OidcConfiguration oidcConfiguration, PermissionManager authorizer, Set defaultRoles, - int tokenExpirationIntervalInSeconds) { + Set additionalAudiences) { this.oidcConfiguration = oidcConfiguration; this.authorizer = authorizer; this.defaultRoles = defaultRoles; + this.acceptedAudiences = new HashSet<>(); + if (oidcConfiguration.getClientId() != null) { + this.acceptedAudiences.add(oidcConfiguration.getClientId()); + } + if (additionalAudiences != null) { + this.acceptedAudiences.addAll(additionalAudiences); + } + if (this.acceptedAudiences.isEmpty()) { + throw new IllegalArgumentException("At least one accepted audience must be configured (clientId or apiResource)"); + } + logger.info("OidcJwtAuthFilter initialized with accepted audiences: {}", this.acceptedAudiences); } @Override @@ -117,10 +137,14 @@ private String verifyAndExtractSubject(String jwtToken) throws AuthenticationExc throw new AuthenticationException("Invalid token issuer"); } - String expectedAudience = oidcConfiguration.getClientId(); - List audiences = claims.getAudience(); - if (expectedAudience != null && (audiences == null || !audiences.contains(expectedAudience))) { - throw new AuthenticationException("Invalid token audience"); + List tokenAudiences = claims.getAudience(); + if (tokenAudiences != null && !tokenAudiences.isEmpty()) { + boolean hasValidAudience = tokenAudiences.stream().anyMatch(acceptedAudiences::contains); + if (!hasValidAudience) { + logger.warn("Token audience {} does not match any accepted audiences {}", + tokenAudiences, acceptedAudiences); + throw new AuthenticationException("Invalid token audience"); + } } JWK jwk = getKey(header.getKeyID()); diff --git a/src/main/java/org/ohdsi/webapi/shiro/management/AtlasRegularSecurity.java b/src/main/java/org/ohdsi/webapi/shiro/management/AtlasRegularSecurity.java index 93d0c6a39f..d7e92e1796 100644 --- a/src/main/java/org/ohdsi/webapi/shiro/management/AtlasRegularSecurity.java +++ b/src/main/java/org/ohdsi/webapi/shiro/management/AtlasRegularSecurity.java @@ -418,11 +418,16 @@ public Map getFilters() { // OIDC token exchange filter if (this.openidAuthEnabled && oidcConfiguration != null) { + Set additionalAudiences = new HashSet<>(); + String apiResource = oidcConfCreator.getApiResource(); + if (apiResource != null && !apiResource.isEmpty()) { + additionalAudiences.add(apiResource); + } OidcJwtAuthFilter oidcJwtFilter = new OidcJwtAuthFilter( oidcConfiguration, this.authorizer, this.defaultRoles, - this.tokenExpirationIntervalInSeconds + additionalAudiences ); filters.put(OIDC_DIRECT_AUTH, oidcJwtFilter); }