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/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/pom.xml b/pom.xml index 33d89ebd96..e6ade4b67a 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 @@ -96,6 +93,7 @@ {:} http://localhost/index.html#/welcome/ + cn={0},dc=example,dc=org @@ -198,7 +196,6 @@ /WebAPI 1.17.4 - 3.1.9 600000 12 10000 @@ -716,28 +713,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 @@ -791,17 +766,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 @@ -895,13 +860,7 @@ 9.1.6 - - - commons-fileupload - commons-fileupload - ${commons-fileupload.version} - - + org.postgresql postgresql @@ -913,11 +872,7 @@ mssql-jdbc 12.8.1.jre11 - - com.microsoft.azure - msal4j - 1.9.0 - + com.opencsv opencsv @@ -935,11 +890,7 @@ commons-csv 1.8 - - org.dom4j - dom4j - 2.1.3 - + org.apache.shiro shiro-core @@ -1113,10 +1064,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 @@ -1198,7 +1151,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 @@ -1817,32 +1783,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/JerseyConfig.java b/src/main/java/org/ohdsi/webapi/JerseyConfig.java deleted file mode 100644 index 9f9089f636..0000000000 --- a/src/main/java/org/ohdsi/webapi/JerseyConfig.java +++ /dev/null @@ -1,83 +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.auth.AuthProviderService; -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(AuthProviderService.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/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/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/ShiroConfiguration.java b/src/main/java/org/ohdsi/webapi/ShiroConfiguration.java index d8167ccf9e..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; @@ -119,6 +128,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() 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/WebMvcConfig.java b/src/main/java/org/ohdsi/webapi/WebMvcConfig.java new file mode 100644 index 0000000000..fa98cb31d3 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/WebMvcConfig.java @@ -0,0 +1,42 @@ +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.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +/** + * Spring MVC Configuration. + * Configures interceptors, message converters, and other MVC components. + */ +@Configuration +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 + if (localeInterceptor != null) { + registry.addInterceptor(localeInterceptor) + .addPathPatterns("/**"); + } + } + + @Override + public void extendMessageConverters(List> converters) { + // Add custom OutputStreamMessageConverter for ByteArrayOutputStream responses + converters.add(new org.ohdsi.webapi.mvc.OutputStreamMessageConverter()); + } +} diff --git a/src/main/java/org/ohdsi/webapi/activity/Tracker.java b/src/main/java/org/ohdsi/webapi/activity/Tracker.java index 01fce0ae68..9cf55c1dec 100644 --- a/src/main/java/org/ohdsi/webapi/activity/Tracker.java +++ b/src/main/java/org/ohdsi/webapi/activity/Tracker.java @@ -17,33 +17,48 @@ 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 */ +@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(); } + + @GetMapping(value = "/latest", produces = MediaType.APPLICATION_JSON_VALUE) + @Deprecated + public Object[] getLatestActivity() { + return getActivity(); + } } 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/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<>(); 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 0c9e27cccd..0000000000 --- a/src/main/java/org/ohdsi/webapi/cache/CacheService.java +++ /dev/null @@ -1,93 +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 - @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 de95fbdf2a..0000000000 --- a/src/main/java/org/ohdsi/webapi/i18n/I18nController.java +++ /dev/null @@ -1,60 +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 - @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..873f966dd8 100644 --- a/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/i18n/I18nServiceImpl.java @@ -3,21 +3,39 @@ 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 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; import java.util.List; import java.util.Locale; +import java.util.Objects; -@Component +/** + * Internationalization service. + * Provides localized message resources and available locales. + */ +@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 @@ -53,7 +71,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); } } @@ -68,4 +86,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(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/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/i18n/mvc/LocaleInterceptor.java b/src/main/java/org/ohdsi/webapi/i18n/mvc/LocaleInterceptor.java new file mode 100644 index 0000000000..c6b8a8452e --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/i18n/mvc/LocaleInterceptor.java @@ -0,0 +1,62 @@ +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; + +/** + * Locale interceptor. + * Extracts locale from request headers and sets it in LocaleContextHolder. + */ +@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/info/InfoService.java b/src/main/java/org/ohdsi/webapi/info/InfoService.java index ca00fa67a6..a15a841364 100644 --- a/src/main/java/org/ohdsi/webapi/info/InfoService.java +++ b/src/main/java/org/ohdsi/webapi/info/InfoService.java @@ -18,21 +18,20 @@ 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; 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; -@Path("/info") -@Controller +@RestController +@RequestMapping("/info") public class InfoService { private final Info info; @@ -51,8 +50,7 @@ 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/job/NotificationServiceImpl.java b/src/main/java/org/ohdsi/webapi/job/NotificationServiceImpl.java index c914210543..a6ab3f0bfd 100644 --- a/src/main/java/org/ohdsi/webapi/job/NotificationServiceImpl.java +++ b/src/main/java/org/ohdsi/webapi/job/NotificationServiceImpl.java @@ -1,14 +1,26 @@ package org.ohdsi.webapi.job; +import org.apache.commons.lang3.StringUtils; import org.ohdsi.webapi.Constants; import org.ohdsi.webapi.shiro.Entities.UserEntity; import org.ohdsi.webapi.shiro.Entities.UserRepository; import org.ohdsi.webapi.shiro.PermissionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.batch.admin.service.SearchableJobExecutionDao; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.JobExecution; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +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 java.util.ArrayList; import java.util.Collections; @@ -19,11 +31,20 @@ import java.util.Objects; import java.util.Optional; import java.util.function.BiFunction; +import java.util.stream.Collectors; import static org.ohdsi.webapi.Constants.Params.SOURCE_KEY; -@Service +/** + * REST Services related to working with the system notifications + * + * @summary Notifications + */ +@RestController +@RequestMapping("/notifications") +@Transactional public class NotificationServiceImpl implements NotificationService { + private static final Logger log = LoggerFactory.getLogger(NotificationServiceImpl.class); private static final int MAX_SIZE = 10; private static final int PAGE_SIZE = MAX_SIZE * 10; private static final 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/AbstractMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/AbstractMvcController.java new file mode 100644 index 0000000000..50ca744158 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/AbstractMvcController.java @@ -0,0 +1,72 @@ +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. + * Provides common response helper methods and security utilities. + */ +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/GlobalExceptionHandler.java b/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java new file mode 100644 index 0000000000..9d96cfe5a2 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/GlobalExceptionHandler.java @@ -0,0 +1,211 @@ +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 org.springframework.web.server.ResponseStatusException; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Objects; + +/** + * Global exception handler for REST controllers. + * Handles all exceptions and returns appropriate HTTP responses. + */ +@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. + */ + @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) + 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 ResponseStatusException. + */ + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity handleResponseStatusException(ResponseStatusException ex) { + logException(ex); + 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); + } + + /** + * 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(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) { + 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/OutputStreamMessageConverter.java b/src/main/java/org/ohdsi/webapi/mvc/OutputStreamMessageConverter.java new file mode 100644 index 0000000000..e054f6895d --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/OutputStreamMessageConverter.java @@ -0,0 +1,54 @@ +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; + +/** + * HttpMessageConverter for ByteArrayOutputStream. + * Allows controllers to return ByteArrayOutputStream directly, + * which is useful for streaming/downloading generated content. + */ +@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 + 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/controller/CacheMvcController.java b/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java new file mode 100644 index 0000000000..121a460ed2 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/mvc/controller/CacheMvcController.java @@ -0,0 +1,85 @@ +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; + +/** + * Cache management controller. + * Provides endpoints for viewing and clearing application caches. + */ +@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. + */ + @GetMapping(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. + */ + @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/reusable/ReusableController.java b/src/main/java/org/ohdsi/webapi/reusable/ReusableController.java deleted file mode 100644 index 80d1bcf76c..0000000000 --- a/src/main/java/org/ohdsi/webapi/reusable/ReusableController.java +++ /dev/null @@ -1,229 +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 - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public ReusableDTO create(final ReusableDTO dto) { - return reusableService.create(dto); - } - - @GET - @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/reusable/ReusableService.java b/src/main/java/org/ohdsi/webapi/reusable/ReusableService.java index 846a005703..04f699212c 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(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(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/PermissionController.java b/src/main/java/org/ohdsi/webapi/security/PermissionController.java deleted file mode 100644 index 1af1059d0f..0000000000 --- a/src/main/java/org/ohdsi/webapi/security/PermissionController.java +++ /dev/null @@ -1,195 +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 - @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/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/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..825c7be5e4 100644 --- a/src/main/java/org/ohdsi/webapi/service/CDMResultsService.java +++ b/src/main/java/org/ohdsi/webapi/service/CDMResultsService.java @@ -40,20 +40,20 @@ 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 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; @@ -73,10 +73,12 @@ import static org.ohdsi.webapi.cdmresults.AchillesCacheTasklet.TREEMAP; /** + * CDM Results Service - provides REST endpoints for CDM results and Achilles reports. + * * @author fdefalco */ -@Path("/cdmresults") -@Component +@RestController +@RequestMapping("/cdmresults") @DependsOn({"jobInvalidator", "flyway"}) public class CDMResultsService extends AbstractDaoService implements InitializingBean { private final Logger logger = LoggerFactory.getLogger(CDMResultsService.class); @@ -154,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. @@ -176,11 +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. */ - @Path("{sourceKey}/conceptRecordCount") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public List>> getConceptRecordCount(@PathParam("sourceKey") 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); @@ -207,14 +211,12 @@ private List>> convertToResponse(Collection + * @param sourceKey The source key + * @param domain The domain + * @return ArrayNode */ - @GET - @Path("{sourceKey}/{domain}/") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(value = "/{sourceKey}/{domain}/", produces = MediaType.APPLICATION_JSON_VALUE) @AchillesCache(TREEMAP) public ArrayNode getTreemap( - @PathParam("domain") - final String domain, - @PathParam("sourceKey") - final String sourceKey) { + @PathVariable("sourceKey") final String sourceKey, + @PathVariable("domain") final String domain) { return getRawTreeMap(domain, sourceKey); } @@ -401,22 +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 */ - @GET - @Path("{sourceKey}/{domain}/{conceptId}") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(value = "/{sourceKey}/{domain}/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) @AchillesCache(DRILLDOWN) - public JsonNode getDrilldown(@PathParam("domain") - final String domain, - @PathParam("conceptId") - final int conceptId, - @PathParam("sourceKey") - 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 f86236c4f6..57e9fd7aef 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; @@ -34,23 +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) */ -@Path("/cohortanalysis/") -@Component +@RestController +@RequestMapping("/cohortanalysis") public class CohortAnalysisService extends AbstractDaoService implements GeneratesNotification { public static final String NAME = "cohortAnalysisJob"; @@ -120,12 +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 */ - @GET - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public List getCohortAnalyses() { String sqlPath = "/resources/cohortanalysis/sql/getCohortAnalyses.sql"; String search = "ohdsi_database_schema"; @@ -134,19 +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 - */ - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - public List getCohortAnalysesForCohortDefinition(@PathParam("id") 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(); @@ -154,19 +150,17 @@ public List getCohortAnalysesForCohortDefinition(@PathParam("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 - */ - @GET - @Path("/{id}/summary") - @Produces(MediaType.APPLICATION_JSON) - public CohortSummary getCohortSummary(@PathParam("id") 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 { @@ -179,21 +173,20 @@ public CohortSummary getCohortSummary(@PathParam("id") 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 */ - @POST - @Path("/preview") - @Produces(MediaType.TEXT_PLAIN) - @Consumes(MediaType.APPLICATION_JSON) - 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); } @@ -227,15 +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 */ - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 f8823b0a37..e5412d1238 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,11 @@ import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +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; /** * Provides REST services for working with cohort definitions. @@ -158,8 +147,8 @@ * @summary Provides REST services for working with cohort definitions. * @author cknoll1 */ -@Path("/cohortdefinition") -@Component +@RestController +@RequestMapping("/cohortdefinition") public class CohortDefinitionService extends AbstractDaoService implements HasTags { //create cache @@ -413,21 +402,17 @@ public GenerateSqlRequest() { public CohortExpressionQueryBuilder.BuildExpressionQueryOptions options; } - @Context 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 */ - @Path("sql") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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) { @@ -446,8 +431,7 @@ public GenerateSqlResult generateSql(GenerateSqlRequest request) { * @return List of metadata about all cohort definitions in WebAPI * @see org.ohdsi.webapi.cohortdefinition.CohortMetadataDTO */ - @GET - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @Transactional @Cacheable(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()") public List getCohortDefinitionList() { @@ -465,7 +449,7 @@ public List getCohortDefinitionList() { /** * Creates a cohort definition in the WebAPI database. - * + * * The values for createdBy and createdDate are automatically populated and * the function ignores parameter values for these fields. * @@ -473,12 +457,10 @@ public List getCohortDefinitionList() { * @param dto The cohort definition to create. * @return The newly created cohort definition */ - @POST + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) @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(); @@ -517,10 +499,8 @@ 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) { + @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)); @@ -557,10 +537,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 */ - @GET - @Path("/{id}/exists") - @Produces(MediaType.APPLICATION_JSON) - public int getCountCDefWithSameName(@PathParam("id") @DefaultValue("0") final int id, @QueryParam("name") 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); } @@ -575,13 +553,10 @@ 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) + @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(@PathParam("id") final int id, CohortDTO def) { + public CohortDTO saveCohortDefinition(@PathVariable("id") final int id, @RequestBody CohortDTO def) { Date currentTime = Calendar.getInstance().getTime(); saveVersion(id); @@ -609,12 +584,10 @@ 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) { + @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); @@ -665,21 +638,20 @@ 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) { + @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(NotFoundException::new); + .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; }); @@ -691,7 +663,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(); } /** @@ -707,11 +679,9 @@ 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") + @GetMapping(value = "/{id}/info", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public List getInfo(@PathParam("id") 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)); @@ -737,12 +707,10 @@ 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") + @GetMapping(value = "/{id}/copy", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) - public CohortDTO copy(@PathParam("id") final int id) { + public CohortDTO copy(@PathVariable("id") final int id) { CohortDTO sourceDef = getCohortDefinition(id); sourceDef.setId(null); // clear the ID sourceDef.setTags(null); @@ -766,11 +734,9 @@ public List getNamesLike(String copyName) { * @summary Delete Cohort Definition * @param id - the Cohort Definition ID to delete */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}") + @DeleteMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) - public void delete(@PathParam("id") 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 @@ -850,19 +816,16 @@ 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) { + @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)) { - 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)); @@ -885,14 +848,13 @@ 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) + @GetMapping(value = "/{id}/report/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional public InclusionRuleReport getInclusionRuleReport( - @PathParam("id") final int id, - @PathParam("sourceKey") final String sourceKey, - @DefaultValue("0") @QueryParam("mode") int modeId, @QueryParam("ccGenerateId") 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); @@ -908,44 +870,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 - */ - @POST - @Path("/check") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) + /** + * 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 */ - @POST - @Path("/checkV2") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) + @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() @@ -971,10 +927,8 @@ 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) { + @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); } @@ -993,10 +947,8 @@ 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) { + @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); } @@ -1012,18 +964,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(); } /** @@ -1033,12 +982,10 @@ 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/") + @PostMapping(value = "/{id}/tag", produces = MediaType.APPLICATION_JSON_VALUE) @CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true) @Transactional - public void assignTag(@PathParam("id") 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); } @@ -1050,12 +997,10 @@ 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}") + @DeleteMapping(value = "/{id}/tag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) @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(@PathVariable("id") final Integer id, @PathVariable("tagId") final int tagId) { CohortDefinition entity = cohortDefinitionRepository.findById(id).orElse(null); unassignTag(entity, tagId); } @@ -1069,11 +1014,9 @@ public void unassignTag(@PathParam("id") final Integer id, @PathParam("tagId") f * @param id * @param tagId */ - @POST - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/protectedtag/") + @PostMapping(value = "/{id}/protectedtag", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public void assignPermissionProtectedTag(@PathParam("id") final int id, final int tagId) { + public void assignPermissionProtectedTag(@PathVariable("id") final int id, @RequestBody final int tagId) { assignTag(id, tagId); } @@ -1084,11 +1027,9 @@ public void assignPermissionProtectedTag(@PathParam("id") final int id, final in * @param id * @param tagId */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/protectedtag/{tagId}") + @DeleteMapping(value = "/{id}/protectedtag/{tagId}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public void unassignPermissionProtectedTag(@PathParam("id") final int id, @PathParam("tagId") final int tagId) { + public void unassignPermissionProtectedTag(@PathVariable("id") final int id, @PathVariable("tagId") final int tagId) { unassignTag(id, tagId); } @@ -1100,11 +1041,9 @@ 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/") + @GetMapping(value = "/{id}/version", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public List getVersions(@PathParam("id") 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)) @@ -1119,11 +1058,9 @@ public List getVersions(@PathParam("id") final long id) { * @param version The version to fetch * @return */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") + @GetMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public CohortVersionFullDTO getVersion(@PathParam("id") final int id, @PathParam("version") 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); @@ -1142,12 +1079,10 @@ 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}") + @PutMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public VersionDTO updateVersion(@PathParam("id") final int id, @PathParam("version") 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); @@ -1163,11 +1098,9 @@ 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}") + @DeleteMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public void deleteVersion(@PathParam("id") final int id, @PathParam("version") 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); } @@ -1184,12 +1117,10 @@ 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") + @PutMapping(value = "/{id}/version/{version}/createAsset", produces = MediaType.APPLICATION_JSON_VALUE) @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(@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); @@ -1204,19 +1135,16 @@ public CohortDTO copyAssetFromVersion(@PathParam("id") final int id, @PathParam( /** * 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. */ - @POST - @Path("/byTags") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) + @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 89294c9de8..461a8169af 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; @@ -28,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; @@ -38,23 +39,26 @@ 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; /** * 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) */ -@Path("/cohortresults") -@Component +@RestController +@RequestMapping("/cohortresults") public class CohortResultsService extends AbstractDaoService { public static final String MIN_COVARIATE_PERSON_COUNT = "10"; @@ -93,14 +97,14 @@ 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) { + @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"; @@ -139,16 +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 */ - @GET - @Path("{sourceKey}/{id}/export.zip") - @Produces(MediaType.APPLICATION_OCTET_STREAM) - public Response exportCohortResults(@PathParam("id") int id, @PathParam("sourceKey") 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); @@ -228,46 +232,45 @@ public Void mapRow(ResultSet rs, int arg1) throws SQLException { throw new RuntimeException(ex); } - Response response = Response - .ok(baos) - .type(MediaType.APPLICATION_OCTET_STREAM) - .build(); + 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 */ - @POST - @Path("/warmup") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 */ - @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) { + @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<>(); @@ -282,16 +285,16 @@ public Collection getCompletedVisualiztion(@PathParam("id") 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 */ - @GET - @Path("{sourceKey}/{id}/tornado") - @Produces(MediaType.APPLICATION_JSON) - public TornadoReport getTornadoReport(@PathParam("sourceKey") final String sourceKey, @PathParam("id") 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); @@ -309,15 +312,14 @@ 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) { + @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); @@ -344,7 +346,7 @@ public CohortDashboard getDashboard(@PathParam("id") 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 @@ -353,13 +355,13 @@ 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) { + @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; @@ -382,19 +384,18 @@ public List getConditionTreemap(@PathParam("sourceKey /** * 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 */ - @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) { + @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() { @@ -421,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 @@ -430,15 +432,14 @@ 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) { + @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); @@ -461,7 +462,8 @@ public CohortConditionDrilldown getConditionResults(@PathParam("sourceKey") Stri /** * 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 @@ -469,14 +471,13 @@ 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) { + @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; @@ -500,16 +501,16 @@ public List getConditionEraTreemap(@PathParam("source /** * 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 */ - @GET - @Path("{sourceKey}/{id}/analyses") - @Produces(MediaType.APPLICATION_JSON) - public List getCompletedAnalyses(@PathParam("sourceKey") String sourceKey, @PathParam("id") 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(); @@ -566,16 +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 */ - @GET - @Path("{sourceKey}/{id}/info") - @Produces(MediaType.APPLICATION_JSON) - public GenerationInfoDTO getAnalysisProgress(@PathParam("sourceKey") String sourceKey, @PathParam("id") 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); @@ -583,7 +584,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)); }); } @@ -600,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 @@ -610,15 +611,14 @@ 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) { + @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; @@ -643,7 +643,7 @@ public CohortConditionEraDrilldown getConditionEraDrilldown(@PathParam("id") fin /** * 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 @@ -652,14 +652,13 @@ 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) { + @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; @@ -680,16 +679,10 @@ public List getDrugTreemap(@PathParam("id") final int 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 @@ -697,16 +690,16 @@ public List getDrugTreemap(@PathParam("id") final int * @param minIntervalPersonCountParam The minimum interval person count * @param sourceKey The source key * @param refresh Boolean - refresh visualization data - * @return + * @return CohortDrugDrilldown */ - @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) { + @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); @@ -729,7 +722,7 @@ public CohortDrugDrilldown getDrugResults(@PathParam("id") final int id, @PathPa /** * 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 @@ -738,14 +731,13 @@ 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) { + @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; @@ -766,16 +758,10 @@ public List getDrugEraTreemap(@PathParam("id") final 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 @@ -785,14 +771,14 @@ 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) { + @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; @@ -815,23 +801,22 @@ public CohortDrugEraDrilldown getDrugEraResults(@PathParam("id") final int id, @ /** * 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 */ - @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) { + @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); @@ -853,7 +838,7 @@ public CohortPersonSummary getPersonResults(@PathParam("id") 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 @@ -862,14 +847,13 @@ 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) { + @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; @@ -891,7 +875,7 @@ public CohortSpecificSummary getCohortSpecificResults(@PathParam("id") final int /** * 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 @@ -900,14 +884,13 @@ 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) { + @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; @@ -930,7 +913,7 @@ public CohortSpecificTreemap getCohortSpecificTreemapResults(@PathParam("id") fi /** * 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 @@ -940,15 +923,14 @@ 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) { + @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); @@ -970,16 +952,10 @@ public List getCohortProcedureDrilldown(@PathParam("id") fina 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 @@ -989,15 +965,14 @@ 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) { + @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); @@ -1020,7 +995,8 @@ public List getCohortDrugDrilldown(@PathParam("id") final int /** * 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 @@ -1029,15 +1005,14 @@ 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) { + @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; @@ -1062,7 +1037,7 @@ public List getCohortConditionDrilldown(@PathParam("id") fina /** * 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 @@ -1071,14 +1046,13 @@ 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) { + @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); @@ -1102,7 +1076,7 @@ public List getCohortObservationResults(@PathParam("i /** * 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 @@ -1112,15 +1086,14 @@ 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) { + @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); @@ -1142,7 +1115,7 @@ public CohortObservationDrilldown getCohortObservationResultsDrilldown(@PathPara /** * 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 @@ -1151,14 +1124,13 @@ 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) { + @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; @@ -1178,16 +1150,10 @@ public List getCohortMeasurementResults(@PathParam("i 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 @@ -1197,14 +1163,14 @@ 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) { + @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; @@ -1226,7 +1192,7 @@ public CohortMeasurementDrilldown getCohortMeasurementResultsDrilldown(@PathPara /** * 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 @@ -1235,14 +1201,13 @@ 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) { + @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; @@ -1263,7 +1228,7 @@ public CohortObservationPeriod getCohortObservationPeriod(@PathParam("id") final /** * 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 @@ -1272,14 +1237,13 @@ 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) { + @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); @@ -1302,7 +1266,7 @@ public CohortDataDensity getCohortDataDensity(@PathParam("id") 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 @@ -1311,14 +1275,13 @@ 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) { + @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); @@ -1342,7 +1305,7 @@ public List getProcedureTreemap(@PathParam("id") fina /** * 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 @@ -1352,15 +1315,14 @@ 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) { + @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; @@ -1382,7 +1344,7 @@ public CohortProceduresDrillDown getCohortProceduresDrilldown(@PathParam("id") f /** * 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 @@ -1391,14 +1353,13 @@ 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) { + @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); @@ -1422,7 +1383,7 @@ public List getVisitTreemap(@PathParam("id") final in /** * 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 @@ -1430,17 +1391,16 @@ public List getVisitTreemap(@PathParam("id") final in * @param minIntervalPersonCountParam The minimum interval person count * @param sourceKey The source key * @param refresh Boolean - refresh visualization data - * @return + * @return CohortVisitsDrilldown */ - @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) { + @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; @@ -1460,17 +1420,16 @@ public CohortVisitsDrilldown getCohortVisitsDrilldown(@PathParam("id") final int /** * Returns the summary for the cohort - * + * * @summary Get cohort summary * @param id The cohort ID * @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) { + @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(); @@ -1500,7 +1459,7 @@ public CohortSummary getCohortSummaryData(@PathParam("id") 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 @@ -1509,14 +1468,13 @@ 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) { + @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; @@ -1537,15 +1495,16 @@ public CohortDeathData getCohortDeathData(@PathParam("id") 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 */ - @GET - @Path("{sourceKey}/{id}/summaryanalyses") - @Produces(MediaType.APPLICATION_JSON) - public CohortSummary getCohortSummaryAnalyses(@PathParam("id") final int id, @PathParam("sourceKey") 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 { @@ -1560,16 +1519,16 @@ public CohortSummary getCohortSummaryAnalyses(@PathParam("id") final int id, @Pa /** * 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 */ - @GET - @Path("{sourceKey}/{id}/breakdown") - @Produces(MediaType.APPLICATION_JSON) - public Collection getCohortBreakdown(@PathParam("id") final int id, @PathParam("sourceKey") 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"; @@ -1586,16 +1545,16 @@ public Collection getCohortBreakdown(@PathParam("id") final int /** * 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 */ - @GET - @Path("{sourceKey}/{id}/members/count") - @Produces(MediaType.APPLICATION_JSON) - public Long getCohortMemberCount(@PathParam("id") final int id, @PathParam("sourceKey") 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"; @@ -1607,7 +1566,7 @@ public Long getCohortMemberCount(@PathParam("id") final int id, @PathParam("sour /** * 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 @@ -1615,12 +1574,11 @@ 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) { + @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; @@ -1638,21 +1596,21 @@ public List getCohortAnalysesForCohortDefinition(@PathParam("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 */ - @POST - @Path("{sourceKey}/exposurecohortrates") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public List getExposureOutcomeCohortRates(@PathParam("sourceKey") 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); @@ -1686,18 +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 */ - @POST - @Path("{sourceKey}/timetoevent") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public List getTimeToEventDrilldown(@PathParam("sourceKey") 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); @@ -1729,18 +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 */ - @POST - @Path("{sourceKey}/predictors") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public List getExposureOutcomeCohortPredictors(@PathParam("sourceKey") 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); @@ -1761,27 +1719,21 @@ public List getExposureOutcomeCohortPredictors(@PathParam("sour }); } - /** - * - * @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 */ - @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) { + @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; @@ -1814,17 +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 */ - @GET - @Path("{sourceKey}/{id}/datacompleteness") - @Produces(MediaType.APPLICATION_JSON) - public List getDataCompleteness(@PathParam("id") final int id, - @PathParam("sourceKey") 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<>(); @@ -1906,16 +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 */ - @GET - @Path("{sourceKey}/{id}/entropy") - @Produces(MediaType.APPLICATION_JSON) - public List getEntropy(@PathParam("id") final int id, @PathParam("sourceKey") 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<>(); @@ -1932,16 +1883,16 @@ public List getEntropy(@PathParam("id") final int id, @PathParam("s /** * 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 */ - @GET - @Path("{sourceKey}/{id}/allentropy") - @Produces(MediaType.APPLICATION_JSON) - public List getAllEntropy(@PathParam("id") final int id, @PathParam("sourceKey") 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(); @@ -1971,7 +1922,7 @@ public List getAllEntropy(@PathParam("id") final int id, @PathParam /** * 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 @@ -1979,12 +1930,12 @@ 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) { + @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; @@ -1992,20 +1943,18 @@ public HealthcareExposureReport getHealthcareUtilizationExposureReport(@PathPara /** * 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 */ - @GET - @Path("{sourceKey}/{id}/healthcareutilization/periods/{window}") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(value = "/{sourceKey}/{id}/healthcareutilization/periods/{window}", produces = MediaType.APPLICATION_JSON_VALUE) public List getHealthcareUtilizationPeriods( - @PathParam("id") final int id - , @PathParam("sourceKey") final String sourceKey - , @PathParam("window") 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; @@ -2014,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 @@ -2026,26 +1975,25 @@ 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) { + @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 @@ -2054,25 +2002,22 @@ 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 - - ) { + @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 @@ -2083,17 +2028,15 @@ 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 - ) { + @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; @@ -2101,20 +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 */ - @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) - { + @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 4da9844a36..7189c08aee 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortSampleService.java @@ -12,27 +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 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 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.*; -import java.util.Optional; -@Path("/cohortsample") -@Component -@Produces(MediaType.APPLICATION_JSON) +@RestController +@RequestMapping("/cohortsample") public class CohortSampleService { private final CohortDefinitionRepository cohortDefinitionRepository; private final CohortGenerationInfoRepository generationInfoRepository; @@ -59,11 +48,10 @@ public CohortSampleService( * @param sourceKey * @return JSON containing information about cohort samples */ - @Path("/{cohortDefinitionId}/{sourceKey}") - @GET + @GetMapping(value = "/{cohortDefinitionId}/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE) public CohortSampleListDTO listCohortSamples( - @PathParam("cohortDefinitionId") int cohortDefinitionId, - @PathParam("sourceKey") String sourceKey + @PathVariable("cohortDefinitionId") int cohortDefinitionId, + @PathVariable("sourceKey") String sourceKey ) { Source source = getSource(sourceKey); CohortSampleListDTO result = new CohortSampleListDTO(); @@ -89,13 +77,12 @@ public CohortSampleListDTO listCohortSamples( * @param fields * @return personId, gender, age of each person in the cohort sample */ - @Path("/{cohortDefinitionId}/{sourceKey}/{sampleId}") - @GET + @GetMapping(value = "/{cohortDefinitionId}/{sourceKey}/{sampleId}", produces = MediaType.APPLICATION_JSON_VALUE) public CohortSampleDTO getCohortSample( - @PathParam("cohortDefinitionId") int cohortDefinitionId, - @PathParam("sourceKey") String sourceKey, - @PathParam("sampleId") Integer sampleId, - @DefaultValue("") @QueryParam("fields") 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"); @@ -111,13 +98,12 @@ public CohortSampleDTO getCohortSample( * @param fields * @return A sample of persons from a cohort */ - @Path("/{cohortDefinitionId}/{sourceKey}/{sampleId}/refresh") - @POST + @PostMapping(value = "/{cohortDefinitionId}/{sourceKey}/{sampleId}/refresh", produces = MediaType.APPLICATION_JSON_VALUE) public CohortSampleDTO refreshCohortSample( - @PathParam("cohortDefinitionId") int cohortDefinitionId, - @PathParam("sourceKey") String sourceKey, - @PathParam("sampleId") Integer sampleId, - @DefaultValue("") @QueryParam("fields") 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"); @@ -130,10 +116,9 @@ public CohortSampleDTO refreshCohortSample( * @param cohortDefinitionId * @return true or false */ - @Path("/has-samples/{cohortDefinitionId}") - @GET + @GetMapping(value = "/has-samples/{cohortDefinitionId}", produces = MediaType.APPLICATION_JSON_VALUE) public Map hasSamples( - @PathParam("cohortDefinitionId") int cohortDefinitionId + @PathVariable("cohortDefinitionId") int cohortDefinitionId ) { int nSamples = this.samplingService.countSamples(cohortDefinitionId); return Collections.singletonMap("hasSamples", nSamples > 0); @@ -145,11 +130,10 @@ 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 + @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()); @@ -163,23 +147,21 @@ public Map hasSamples( * @param sampleParameters * @return */ - @Path("/{cohortDefinitionId}/{sourceKey}") - @POST - @Consumes(MediaType.APPLICATION_JSON) + @PostMapping(value = "/{cohortDefinitionId}/{sourceKey}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public CohortSampleDTO createCohortSample( - @PathParam("sourceKey") String sourceKey, - @PathParam("cohortDefinitionId") int cohortDefinitionId, - SampleParametersDTO sampleParameters + @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 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 +173,18 @@ 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 + @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 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.noContent().build(); } /** @@ -212,24 +193,23 @@ public Response deleteCohortSample( * @param cohortDefinitionId * @return */ - @Path("/{cohortDefinitionId}/{sourceKey}") - @DELETE - public Response deleteCohortSamples( - @PathParam("sourceKey") String sourceKey, - @PathParam("cohortDefinitionId") 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) { - 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..bd766e0d00 100644 --- a/src/main/java/org/ohdsi/webapi/service/CohortService.java +++ b/src/main/java/org/ohdsi/webapi/service/CohortService.java @@ -3,27 +3,26 @@ 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; 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 */ -@Path("/cohort/") -@Component +@RestController +@RequestMapping("/cohort") public class CohortService { @Autowired @@ -36,16 +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 */ - @GET - @Path("{id}") - @Produces(MediaType.APPLICATION_JSON) - public List getCohortListById(@PathParam("id") 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; @@ -53,15 +50,12 @@ public List getCohortListById(@PathParam("id") final long id) { /** * Imports a List of CohortEntity into the COHORT table - * + * * @param cohort List of CohortEntity * @return status */ - @POST - @Path("import") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.TEXT_PLAIN) - 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 91d60408af..718acab8e1 100644 --- a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java +++ b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java @@ -20,10 +20,7 @@ import java.util.stream.Collectors; 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 org.springframework.transaction.annotation.Transactional; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -77,7 +74,13 @@ 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.stereotype.Component; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; /** * Provides REST services for working with @@ -85,9 +88,9 @@ * * @summary Concept Set */ -@Component +@RestController +@RequestMapping("/conceptset") @Transactional -@Path("/conceptset/") public class ConceptSetService extends AbstractDaoService implements HasTags { //create cache @Component @@ -158,10 +161,8 @@ 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) { + @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); @@ -173,10 +174,9 @@ 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 - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(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) @@ -197,10 +197,8 @@ 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) { + @GetMapping(value = "/{id}/items", produces = MediaType.APPLICATION_JSON_VALUE) + public Iterable getConceptSetItems(@PathVariable("id") final int id) { return getConceptSetItemRepository().findAllByConceptSetId(id); } @@ -212,16 +210,15 @@ 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) { + @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); } /** @@ -236,17 +233,16 @@ 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) { + @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); } /** @@ -256,15 +252,13 @@ 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) { + @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); } /** @@ -275,17 +269,16 @@ 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) { - + @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 @@ -352,17 +345,19 @@ 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 - @GET - @Path("{id}/{name}/exists") - @Produces(MediaType.APPLICATION_JSON) - public Response getConceptSetExistsDeprecated(@PathParam("id") final int id, @PathParam("name") 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 Response.ok(cs).header("Warning: 299", warningMessage).build(); + return ResponseEntity.ok() + .header("Warning", "299 - " + warningMessage) + .body(cs); } /** @@ -376,10 +371,10 @@ 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) { + @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); } @@ -396,11 +391,11 @@ 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) + @PutMapping(value = "/{id}/items", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public boolean saveConceptSetItems(@PathParam("id") final int id, ConceptSetItem[] items) { + public boolean saveConceptSetItems( + @PathVariable("id") final int id, + @RequestBody ConceptSetItem[] items) { getConceptSetItemRepository().deleteByConceptSetId(id); for (ConceptSetItem csi : items) { @@ -424,11 +419,9 @@ 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 { + @GetMapping(value = "/exportlist") + public ResponseEntity exportConceptSetList( + @RequestParam("conceptsets") final String conceptSetList) throws Exception { ArrayList conceptSetIds = new ArrayList<>(); try { String[] conceptSetItems = conceptSetList.split("\\+"); @@ -445,7 +438,6 @@ public Response exportConceptSetList(@QueryParam("conceptsets") final String con ByteArrayOutputStream baos; Source source = sourceService.getPriorityVocabularySource(); ArrayList cs = new ArrayList<>(); - Response response = null; try { // Load all of the concept sets requested for (int i = 0; i < conceptSetIds.size(); i++) { @@ -455,16 +447,17 @@ 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) - .header("Content-Disposition", "attachment; filename=\"conceptSetExport.zip\"") - .build(); + 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; } /** @@ -475,11 +468,8 @@ 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 { + @GetMapping(value = "/{id}/export") + public ResponseEntity exportConceptSetToCSV(@PathVariable("id") final String id) throws Exception { return this.exportConceptSetList(id); } @@ -490,11 +480,9 @@ 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 */ - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) + @PostMapping(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); @@ -502,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); } @@ -512,15 +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 */ - @GET - @Path("/{id}/copy-name") - @Produces(MediaType.APPLICATION_JSON) - public Map getNameForCopy (@PathParam("id") 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); @@ -544,26 +530,24 @@ public List getNamesLike(String copyName) { * @return The * @throws Exception */ - @Path("/{id}") - @PUT - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) + @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(@PathParam("id") 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) { - 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); 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()); @@ -583,7 +567,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); @@ -605,10 +589,8 @@ 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) { + @GetMapping(value = "/{id}/generationinfo", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getConceptSetGenerationInfo(@PathVariable("id") final int id) { return this.conceptSetGenerationInfoRepository.findAllByConceptSetId(id); } @@ -618,11 +600,10 @@ 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) { + @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); @@ -665,12 +646,13 @@ 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/") + @PostMapping(value = "/{id}/tag/", consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public void assignTag(@PathParam("id") 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); } @@ -683,12 +665,13 @@ 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}") + @DeleteMapping(value = "/{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) { + @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); } @@ -701,11 +684,11 @@ 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/") + @PostMapping(value = "/{id}/protectedtag/", consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public void assignPermissionProtectedTag(@PathParam("id") final int id, final int tagId) { + public void assignPermissionProtectedTag( + @PathVariable("id") final int id, + @RequestBody final int tagId) { assignTag(id, tagId); } @@ -717,12 +700,12 @@ 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}") + @DeleteMapping(value = "/{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) { + @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); } @@ -736,12 +719,9 @@ 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) + @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)); } @@ -753,11 +733,9 @@ 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/") + @GetMapping(value = "/{id}/version/", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public List getVersions(@PathParam("id") 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)) @@ -773,11 +751,11 @@ 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}") + @GetMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - public ConceptSetVersionFullDTO getVersion(@PathParam("id") final int id, @PathParam("version") 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); @@ -794,12 +772,12 @@ 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}") + @PutMapping(value = "/{id}/version/{version}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public VersionDTO updateVersion(@PathParam("id") final int id, @PathParam("version") 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); @@ -814,13 +792,13 @@ public VersionDTO updateVersion(@PathParam("id") final int id, @PathParam("versi * @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 */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/version/{version}") + @DeleteMapping(value = "/{id}/version/{version}") @Transactional - public void deleteVersion(@PathParam("id") final int id, @PathParam("version") 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); } @@ -835,12 +813,12 @@ 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") + @PutMapping(value = "/{id}/version/{version}/createAsset", produces = MediaType.APPLICATION_JSON_VALUE) @Transactional - @CacheEvict(cacheNames = CachingSetup.CONCEPT_SET_LIST_CACHE, allEntries = true) - public ConceptSetDTO copyAssetFromVersion(@PathParam("id") final int id, @PathParam("version") 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); @@ -863,11 +841,8 @@ 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) { + @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(); } @@ -915,11 +890,11 @@ 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) + @PutMapping(value = "/{id}/annotation", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Transactional - public boolean saveConceptSetAnnotation(@PathParam("id") 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() @@ -957,11 +932,9 @@ private void removeAnnotations(int id, SaveConceptSetAnnotationsRequest request) } } } - @POST - @Path("/copy-annotations") - @Produces(MediaType.APPLICATION_JSON) + @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())) @@ -988,11 +961,8 @@ 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) { + @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) @@ -1025,15 +995,16 @@ 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) { + @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 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..85c51b4674 100644 --- a/src/main/java/org/ohdsi/webapi/service/DDLService.java +++ b/src/main/java/org/ohdsi/webapi/service/DDLService.java @@ -28,20 +28,18 @@ 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; import org.ohdsi.webapi.sqlrender.TranslatedStatement; -import org.ohdsi.webapi.util.SessionUtils; -import org.springframework.stereotype.Component; - -@Path("/ddl/") -@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"; @@ -132,15 +130,13 @@ public class DDLService { * @param tempSchema * @return SQL to create tables in results schema */ - @GET - @Path("results") - @Produces("text/plain") + @GetMapping(value = "/results", produces = MediaType.TEXT_PLAIN_VALUE) 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) { + @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); @@ -171,10 +167,10 @@ 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) { + @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); @@ -190,13 +186,11 @@ public String generateCemResultSQL(@QueryParam("dialect") String dialect, @Defau * @param resultSchema results schema * @return SQL */ - @GET - @Path("achilles") - @Produces("text/plain") + @GetMapping(value = "/achilles", produces = MediaType.TEXT_PLAIN_VALUE) public String generateAchillesSQL( - @QueryParam("dialect") String dialect, - @DefaultValue("vocab") @QueryParam("vocabSchema") String vocabSchema, - @DefaultValue("results") @QueryParam("schema") 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 c225acea8f..afa1264412 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; @@ -65,9 +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.web.bind.annotation.*; /** * Provides REST services for querying the Common Evidence Model @@ -75,8 +68,8 @@ * @summary REST services for querying the Common Evidence Model See * https://github.com/OHDSI/CommonEvidenceModel */ -@Path("/evidence") -@Component +@RestController +@RequestMapping("/evidence") public class EvidenceService extends AbstractDaoService implements GeneratesNotification { private static final String NAME = "negativeControlsAnalysisJob"; @@ -152,10 +145,8 @@ 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) { + @GetMapping(value = "/study/{cohortId}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getCohortStudyMapping(@PathVariable("cohortId") int cohortId) { return cohortStudyMappingRepository.findByCohortDefinitionId(cohortId); } @@ -168,10 +159,8 @@ 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) { + @GetMapping(value = "/mapping/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getConceptCohortMapping(@PathVariable("conceptId") int conceptId) { return mappingRepository.findByConceptId(conceptId); } @@ -186,10 +175,8 @@ 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) { + @GetMapping(value = "/conceptofinterest/{conceptId}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getConceptOfInterest(@PathVariable("conceptId") int conceptId) { return conceptOfInterestMappingRepository.findAllByConceptId(conceptId); } @@ -205,10 +192,8 @@ 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) { + @GetMapping(value = "/label/{setid}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getDrugLabel(@PathVariable("setid") String setid) { return drugLabelRepository.findAllBySetid(setid); } @@ -221,10 +206,8 @@ 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) { + @GetMapping(value = "/labelsearch/{searchTerm}", produces = MediaType.APPLICATION_JSON_VALUE) + public Collection searchDrugLabels(@PathVariable("searchTerm") String searchTerm) { return drugLabelRepository.searchNameContainsTerm(searchTerm); } @@ -236,10 +219,8 @@ 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) { + @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"; @@ -269,11 +250,8 @@ 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) { + @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) -> { @@ -306,10 +284,8 @@ 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) { + @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) -> { @@ -345,10 +321,8 @@ 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) { + @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) -> { @@ -384,11 +358,8 @@ 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) { + @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); } @@ -402,10 +373,8 @@ 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) { + @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) -> { @@ -447,13 +416,13 @@ 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) { + @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 Response.ok(evidence).header("Warning: 299", warningMessage).build(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(evidence); } /** @@ -465,10 +434,8 @@ 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) { + @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) -> { @@ -510,13 +477,13 @@ 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) { + @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 Response.ok(evidenceSummary).header("Warning: 299", warningMessage).build(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(evidenceSummary); } /** @@ -532,17 +499,17 @@ 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) + @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 Response.ok(evidenceDetails).header("Warning: 299", warningMessage).build(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(evidenceDetails); } /** @@ -556,14 +523,13 @@ 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 { + @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 Response.ok(returnVal).header("Warning: 299", warningMessage).build(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(returnVal); } /** @@ -577,14 +543,13 @@ 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 { + @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 Response.ok(returnVal).header("Warning: 299", warningMessage).build(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(returnVal); } /** @@ -598,14 +563,13 @@ 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 { + @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 Response.ok(returnVal).header("Warning: 299", warningMessage).build(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Warning", "299 - " + warningMessage); + return ResponseEntity.ok().headers(headers).body(returnVal); } /** @@ -618,11 +582,8 @@ 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 { + @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; } @@ -683,7 +644,7 @@ public JobExecutionResource queueNegativeControlsJob(@PathParam("sourceKey") Str 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); @@ -693,7 +654,7 @@ public JobExecutionResource queueNegativeControlsJob(@PathParam("sourceKey") Str 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); @@ -719,11 +680,8 @@ 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 { + @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()); @@ -737,13 +695,11 @@ 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) { + @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 9235f00930..29c67ccea0 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; @@ -84,25 +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) */ -@Path("/feasibility/") -@Component +@RestController +@RequestMapping("/feasibility") public class FeasibilityService extends AbstractDaoService { @Autowired @@ -135,7 +127,6 @@ public class FeasibilityService extends AbstractDaoService { @Autowired private SourceService sourceService; - @Context ServletContext context; private StudyGenerationInfo findStudyGenerationInfoBySourceId(Collection infoList, Integer sourceId) { @@ -379,13 +370,13 @@ public FeasibilityStudyDTO feasibilityStudyToDTO(FeasibilityStudy study) { /** * DO NOT USE - * + * * @summary DO NOT USE * @deprecated * @return List */ - @GET - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @Deprecated public List getFeasibilityStudyList() { return getTransactionTemplate().execute(transactionStatus -> { @@ -408,17 +399,19 @@ public List getFeasibilityStudyList /** * Creates the feasibility study - * + * * @summary DO NOT USE * @deprecated * @param study The feasibility study * @return Feasibility study */ - @PUT - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) + @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(); @@ -471,18 +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 */ - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) + @GetMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE + ) @Transactional(readOnly = true) - public FeasibilityService.FeasibilityStudyDTO getStudy(@PathParam("id") final int id) { + @Deprecated + public FeasibilityService.FeasibilityStudyDTO getStudy(@PathVariable("id") final int id) { return getTransactionTemplate().execute(transactionStatus -> { FeasibilityStudy s = this.feasibilityStudyRepository.findOneWithDetail(id); @@ -492,18 +487,20 @@ public FeasibilityService.FeasibilityStudyDTO getStudy(@PathParam("id") final in /** * Update the feasibility study - * + * * @summary DO NOT USE * @deprecated * @param id The study ID * @param study The study information * @return The updated study information */ - @PUT - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) + @PutMapping( + value = "/{id}", + produces = MediaType.APPLICATION_JSON_VALUE + ) @Transactional - public FeasibilityService.FeasibilityStudyDTO saveStudy(@PathParam("id") 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()); @@ -555,18 +552,20 @@ public FeasibilityService.FeasibilityStudyDTO saveStudy(@PathParam("id") final i /** * Generate the feasibility study - * + * * @summary DO NOT USE * @deprecated * @param study_id The study ID * @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) { + @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); @@ -663,17 +662,19 @@ public JobExecutionResource performStudy(@PathParam("study_id") final int study_ /** * Get simulation information - * + * * @summary DO NOT USE * @deprecated * @param id The study ID * @return List */ - @GET - @Path("/{id}/info") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping( + value = "/{id}/info", + produces = MediaType.APPLICATION_JSON_VALUE + ) @Transactional(readOnly = true) - public List getSimulationInfo(@PathParam("id") final int id) { + @Deprecated + public List getSimulationInfo(@PathVariable("id") final int id) { FeasibilityStudy study = this.feasibilityStudyRepository.findById(id).orElse(null); List result = new ArrayList<>(); @@ -688,18 +689,20 @@ public List getSimulationInfo(@PathParam("id") final int id) { /** * Get simulation report - * + * * @summary DO NOT USE * @deprecated * @param id The study ID * @param sourceKey The source key * @return FeasibilityReport */ - @GET - @Path("/{id}/report/{sourceKey}") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping( + value = "/{id}/report/{sourceKey}", + produces = MediaType.APPLICATION_JSON_VALUE + ) @Transactional - public FeasibilityReport getSimulationReport(@PathParam("id") final int id, @PathParam("sourceKey") final String sourceKey) { + @Deprecated + public FeasibilityReport getSimulationReport(@PathVariable("id") final int id, @PathVariable("sourceKey") final String sourceKey) { Source source = this.getSourceRepository().findBySourceKey(sourceKey); @@ -723,11 +726,13 @@ 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") + @GetMapping( + value = "/{id}/copy", + produces = MediaType.APPLICATION_JSON_VALUE + ) @jakarta.transaction.Transactional - public FeasibilityStudyDTO copy(@PathParam("id") 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); @@ -737,31 +742,35 @@ public FeasibilityStudyDTO copy(@PathParam("id") final int id) { /** * Deletes the specified feasibility study - * + * * @summary DO NOT USE * @deprecated * @param id The study ID */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}") - public void delete(@PathParam("id") 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 */ - @DELETE - @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}/info/{sourceKey}") - @Transactional - public void deleteInfo(@PathParam("id") final int id, @PathParam("sourceKey") 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/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..7b07f77dd1 100644 --- a/src/main/java/org/ohdsi/webapi/service/JobService.java +++ b/src/main/java/org/ohdsi/webapi/service/JobService.java @@ -25,17 +25,15 @@ 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 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; @@ -48,11 +46,11 @@ /** * REST Services related to working with the Spring Batch jobs - * + * * @summary Jobs */ -@Path("/job/") -@Component +@RestController +@RequestMapping("/job") public class JobService extends AbstractDaoService { private final JobExplorer jobExplorer; @@ -75,15 +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 */ - @GET - @Path("{jobId}") - @Produces(MediaType.APPLICATION_JSON) - public JobInstanceResource findJob(@PathParam("jobId") 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 @@ -93,35 +89,34 @@ public JobInstanceResource findJob(@PathParam("jobId") 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 */ - @GET - @Path("/type/{jobType}/name/{jobName}") - @Produces(MediaType.APPLICATION_JSON) - public JobExecutionResource findJobByName(@PathParam("jobName") final String jobName, @PathParam("jobType") 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 - */ - @GET - @Path("{jobId}/execution/{executionId}") - @Produces(MediaType.APPLICATION_JSON) - public JobExecutionResource findJobExecution(@PathParam("jobId") final Long jobId, - @PathParam("executionId") 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); } @@ -132,10 +127,8 @@ 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) { + @GetMapping(value = "/execution/{executionId}", produces = MediaType.APPLICATION_JSON_VALUE) + public JobExecutionResource findJobExecutionById(@PathVariable("executionId") final Long executionId) { return service(null, executionId); } @@ -150,14 +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 */ - @GET - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public List findJobNames() { return this.jobExplorer.getJobNames(); } @@ -178,13 +170,12 @@ 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) + @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/service/SSOService.java b/src/main/java/org/ohdsi/webapi/service/SSOService.java new file mode 100644 index 0000000000..a634c4e947 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/service/SSOService.java @@ -0,0 +1,70 @@ +package org.ohdsi.webapi.service; + +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.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; + +/** + * SSO Service providing SAML metadata and logout functionality + */ +@RestController +@RequestMapping("/saml") +public class SSOService { + + @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 + */ + @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 + * + * @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/service/SqlRenderService.java b/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java index aa6c9bc58b..882bce8c83 100644 --- a/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java +++ b/src/main/java/org/ohdsi/webapi/service/SqlRenderService.java @@ -5,34 +5,36 @@ 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; 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 */ -@Path("/sqlrender/") +@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 */ - @Path("translate") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 51de0648c9..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,10 +9,9 @@ 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 jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; import java.util.*; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -23,8 +21,8 @@ * @author gennadiy.anisimov */ -@Path("/") -@Component +@RestController +@RequestMapping("") public class UserService { @Autowired @@ -97,18 +95,14 @@ public int compareTo(Permission o) { } } - @GET - @Path("user") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE) public ArrayList getUsers() { Iterable userEntities = this.authorizer.getUsers(); ArrayList users = convertUsers(userEntities); return users; } - @GET - @Path("user/me") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(value = "/user/me", produces = MediaType.APPLICATION_JSON_VALUE) public User getCurrentUser() throws Exception { UserEntity currentUser = this.authorizer.getCurrentUser(); @@ -125,31 +119,24 @@ 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 { + @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; } - @GET - @Path("user/{userId}/roles") - @Produces(MediaType.APPLICATION_JSON) - public ArrayList getUserRoles(@PathParam("userId") 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; } - @POST - @Path("role") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - 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( @@ -161,11 +148,8 @@ public Role createRole(Role role) throws Exception { 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 { + @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"); @@ -176,45 +160,39 @@ public Role updateRole(@PathParam("roleId") Long id, Role role) throws Exception return new Role(roleEntity); } - @GET - @Path("role") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(value = "/role", produces = MediaType.APPLICATION_JSON_VALUE) public ArrayList getRoles( - @DefaultValue("false") @QueryParam("include_personal") boolean includePersonalRoles) { + @RequestParam(value = "include_personal", defaultValue = "false") 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) { + @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; } - @DELETE - @Path("role/{roleId}") - public void removeRole(@PathParam("roleId") 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)); } - @GET - @Path("role/{roleId}/permissions") - @Produces(MediaType.APPLICATION_JSON) - public List getRolePermissions(@PathParam("roleId") 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; } - @PUT - @Path("role/{roleId}/permissions/{permissionIdList}") - public void addPermissionToRole(@PathParam("roleId") Long roleId, @PathParam("permissionIdList") 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); @@ -223,9 +201,10 @@ public void addPermissionToRole(@PathParam("roleId") Long roleId, @PathParam("pe } } - @DELETE - @Path("role/{roleId}/permissions/{permissionIdList}") - public void removePermissionFromRole(@PathParam("roleId") Long roleId, @PathParam("permissionIdList") 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); @@ -234,19 +213,18 @@ public void removePermissionFromRole(@PathParam("roleId") Long roleId, @PathPara } } - @GET - @Path("role/{roleId}/users") - @Produces(MediaType.APPLICATION_JSON) - public ArrayList getRoleUsers(@PathParam("roleId") 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; } - @PUT - @Path("role/{roleId}/users/{userIdList}") - public void addUserToRole(@PathParam("roleId") Long roleId, @PathParam("userIdList") 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); @@ -255,9 +233,10 @@ public void addUserToRole(@PathParam("roleId") Long roleId, @PathParam("userIdLi } } - @DELETE - @Path("role/{roleId}/users/{userIdList}") - public void removeUserFromRole(@PathParam("roleId") Long roleId, @PathParam("userIdList") 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 a12c2b8a2b..3ddffe94dd 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; @@ -79,19 +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.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; import org.ohdsi.webapi.vocabulary.MappedRelatedConcept; /** * Provides REST services for working with * the OMOP standardized vocabularies - * + * * @summary Vocabulary */ -@Path("vocabulary/") -@Component +@RestController +@RequestMapping("/vocabulary") public class VocabularyService extends AbstractDaoService { //create cache @@ -179,7 +170,7 @@ public Source getPriorityVocabularySource() { Source source = sourceService.getPriorityVocabularySource(); if (Objects.isNull(source)) { - throw new ForbiddenException(); + throw new ResponseStatusException(HttpStatus.FORBIDDEN); } return source; } @@ -195,22 +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} */ - @Path("{sourceKey}/lookup/identifiers/ancestors") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Map> calculateAscendants(@PathParam("sourceKey") 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); @@ -254,7 +246,7 @@ public Map> calculateAscendants(@PathParam("sourceKey") String ); } - private static class Ids { + public static class Ids { public List ancestors; public List descendants; } @@ -272,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 @@ -280,11 +272,12 @@ 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) { + @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); } @@ -318,23 +311,22 @@ 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 */ - @Path("lookup/identifiers") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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) - 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); } @@ -355,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 @@ -363,11 +355,12 @@ 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) { + @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<>(); } @@ -388,42 +381,42 @@ 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 */ - @Path("lookup/sourcecodes") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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) - 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); } /** - * 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 */ - @Path("{sourceKey}/lookup/mapped") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection executeMappedLookup(@PathParam("sourceKey") 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); } @@ -458,24 +451,23 @@ 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 */ - @Path("lookup/mapped") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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) - 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); } @@ -496,17 +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 */ - @Path("{sourceKey}/search") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection executeSearch(@PathParam("sourceKey") 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); @@ -657,53 +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 */ - @Path("search") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 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); + 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 */ - @Path("{sourceKey}/search/{query}") - @GET - @Produces(MediaType.APPLICATION_JSON) - public Collection executeSearch(@PathParam("sourceKey") String sourceKey, @PathParam("query") 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 */ - @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) { + @GetMapping(value = "/{sourceKey}/search", + 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); @@ -736,40 +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 */ - @Path("search/{query}") - @GET - @Produces(MediaType.APPLICATION_JSON) - public Collection executeSearch(@PathParam("query") 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 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); } /** - * 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 */ - @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) { + @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); @@ -780,7 +776,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; } @@ -788,40 +784,40 @@ public Concept getConcept(@PathParam("sourceKey") final String sourceKey, @PathP /** * 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 */ - @GET - @Path("concept/{id}") - @Produces(MediaType.APPLICATION_JSON) - public Concept getConcept(@PathParam("id") 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 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); - + } /** - * 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 */ - @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) { + @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"; @@ -835,11 +831,11 @@ 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) { + @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"; @@ -898,26 +894,27 @@ 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); } } }); } /** - * 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 */ - @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) { + @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"; @@ -934,38 +931,38 @@ public Collection getConceptAncestorAndDescendant(@PathParam("so /** * 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 */ - @GET - @Path("concept/{id}/related") - @Produces(MediaType.APPLICATION_JSON) - public Collection getRelatedConcepts(@PathParam("id") 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 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); } /** * 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 */ - @POST - @Path("{sourceKey}/commonAncestors") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getCommonAncestors(@PathParam("sourceKey") 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<>(); @@ -993,39 +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 */ - @POST - @Path("/commonAncestors") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 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); } /** - * 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 */ - @POST - @Path("{sourceKey}/resolveConceptSetExpression") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection resolveConceptSetExpression(@PathParam("sourceKey") 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<>(); @@ -1040,40 +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 */ - @POST - @Path("resolveConceptSetExpression") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 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); } /** * 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 */ - @POST - @Path("{sourceKey}/included-concepts/count") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Integer countIncludedConceptSets(@PathParam("sourceKey") 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(); @@ -1082,21 +1079,20 @@ public Integer countIncludedConceptSets(@PathParam("sourceKey") String sourceKey /** * 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 */ - @POST - @Path("included-concepts/count") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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)) { - 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); } @@ -1105,35 +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 */ - @POST - @Path("conceptSetExpressionSQL") - @Produces(MediaType.TEXT_PLAIN) - @Consumes(MediaType.APPLICATION_JSON) - 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 */ - @GET - @Path("{sourceKey}/concept/{id}/descendants") - @Produces(MediaType.APPLICATION_JSON) - public Collection getDescendantConcepts(@PathParam("sourceKey") String sourceKey, @PathParam("id") 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"; @@ -1153,35 +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 */ - @GET - @Path("concept/{id}/descendants") - @Produces(MediaType.APPLICATION_JSON) - public Collection getDescendantConcepts(@PathParam("id") 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 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); } /** - * 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 */ - @GET - @Path("{sourceKey}/domains") - @Produces(MediaType.APPLICATION_JSON) - public Collection getDomains(@PathParam("sourceKey") 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"; @@ -1200,36 +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 */ - @GET - @Path("domains") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(value = "/domains", + produces = MediaType.APPLICATION_JSON_VALUE) 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); } /** - * 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 */ - @GET - @Path("{sourceKey}/vocabularies") - @Produces(MediaType.APPLICATION_JSON) - public Collection getVocabularies(@PathParam("sourceKey") 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); @@ -1250,21 +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 */ - @GET - @Path("vocabularies") - @Produces(MediaType.APPLICATION_JSON) + @GetMapping(value = "/vocabularies", + produces = MediaType.APPLICATION_JSON_VALUE) 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); } @@ -1301,15 +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 */ - @GET - @Path("{sourceKey}/info") - @Produces(MediaType.APPLICATION_JSON) - public VocabularyInfo getInfo(@PathParam("sourceKey") String sourceKey) { + @GetMapping(value = "/{sourceKey}/info", + produces = MediaType.APPLICATION_JSON_VALUE) + public VocabularyInfo getInfo(@PathVariable("sourceKey") String sourceKey) { if (vocabularyInfoCache == null) { vocabularyInfoCache = new Hashtable<>(); } @@ -1348,21 +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 */ - @POST - @Path("{sourceKey}/descendantofancestor") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getDescendantOfAncestorConcepts(@PathParam("sourceKey") 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); @@ -1382,42 +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 */ - @POST - @Path("descendantofancestor") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 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); } /** - * 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 */ - @Path("{sourceKey}/relatedconcepts") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getRelatedConcepts(@PathParam("sourceKey") 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); @@ -1450,22 +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 */ - @Path("relatedconcepts") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 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); } @@ -1473,17 +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 */ - @Path("{sourceKey}/conceptlist/descendants") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getDescendantConceptsByList(@PathParam("sourceKey") 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); @@ -1499,21 +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 */ - @Path("conceptlist/descendants") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 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); } @@ -1529,17 +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 */ - @Path("{sourceKey}/lookup/recommended") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection getRecommendedConceptsByList(@PathParam("sourceKey") 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 } @@ -1588,17 +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 */ - @Path("{sourceKey}/compare") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Collection compareConceptSets(@PathParam("sourceKey") 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."); } @@ -1624,13 +1615,12 @@ 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, - 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."); @@ -1658,20 +1648,20 @@ public Collection compareConceptSetsCsv(final @PathParam(" /** * 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 */ - @Path("compare") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 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); } @@ -1680,21 +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 */ - @Path("optimize") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - 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 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); } @@ -1702,17 +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 */ - @Path("{sourceKey}/optimize") - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public ConceptSetOptimizationResult optimizeConceptSet(@PathParam("sourceKey") 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/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) { 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 { 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/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/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/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/UpdateAccessTokenFilter.java b/src/main/java/org/ohdsi/webapi/shiro/filters/UpdateAccessTokenFilter.java index 4c78d6025d..22d1d63677 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; @@ -169,7 +169,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/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 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; 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); } 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..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 JAX-RS @ApplicationPath("/WebAPI") - 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); 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 b2b2a714ad..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 - */ - @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); - // Explicit endpoint call bypasses checkConnection flag - sourceService.forceCheckConnection(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/source/SourceService.java b/src/main/java/org/ohdsi/webapi/source/SourceService.java index 6c4cc3d404..6fddf4e074 100644 --- a/src/main/java/org/ohdsi/webapi/source/SourceService.java +++ b/src/main/java/org/ohdsi/webapi/source/SourceService.java @@ -1,86 +1,125 @@ 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 org.springframework.context.annotation.Lazy; 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, + @Lazy 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 +137,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 +451,6 @@ public void forceCheckConnection(Source source) { } public Source getPrioritySourceForDaimon(SourceDaimon.DaimonType daimonType) { - List sourcesByDaimonPriority = sourceRepository.findAllSortedByDiamonPrioirty(daimonType); for (Source source : sourcesByDaimonPriority) { @@ -152,7 +464,6 @@ public Source getPrioritySourceForDaimon(SourceDaimon.DaimonType daimonType) { } public Map getPriorityDaimons() { - class SourceValidator { private Map checkedSources = new HashMap<>(); @@ -165,7 +476,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 +485,6 @@ private boolean isSourceAvaialble(Source source) { } public Source getPriorityVocabularySource() { - return getPrioritySourceForDaimon(SourceDaimon.DaimonType.Vocabulary); } @@ -187,12 +496,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 +566,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/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/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/TagController.java b/src/main/java/org/ohdsi/webapi/tag/TagController.java deleted file mode 100644 index 53dd0ac497..0000000000 --- a/src/main/java/org/ohdsi/webapi/tag/TagController.java +++ /dev/null @@ -1,159 +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 - @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 - @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..1ca5e430e2 100644 --- a/src/main/java/org/ohdsi/webapi/tag/TagService.java +++ b/src/main/java/org/ohdsi/webapi/tag/TagService.java @@ -1,46 +1,52 @@ package org.ohdsi.webapi.tag; -import org.apache.shiro.SecurityUtils; -import org.glassfish.jersey.internal.util.Producer; +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; + private final ArrayList>> infoProducers; @Autowired 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(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(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()); @@ -171,9 +222,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); @@ -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/ToolController.java b/src/main/java/org/ohdsi/webapi/tool/ToolController.java deleted file mode 100644 index 774d8b8b80..0000000000 --- a/src/main/java/org/ohdsi/webapi/tool/ToolController.java +++ /dev/null @@ -1,59 +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 - @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 - @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 - @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/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/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/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 cec7d74d4b..0000000000 --- a/src/main/java/org/ohdsi/webapi/user/importer/UserImportJobController.java +++ /dev/null @@ -1,161 +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 - @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 - @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/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..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 @@ -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,27 @@ 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; 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 +63,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 +86,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, + @Lazy @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 +123,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/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/main/resources/application.properties b/src/main/resources/application.properties index 70003061ca..ba02cddc5b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -75,8 +75,10 @@ jersey.config.server.wadl.disableWadl=true 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 @@ -98,7 +100,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/tagging/ReusableTaggingTest.java b/src/test/java/org/ohdsi/webapi/tagging/ReusableTaggingTest.java index e119c5cb1a..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.ReusableController; +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 ReusableController 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); + initialDTO = service.create(dto); } @Override protected ReusableDTO doCopyData(ReusableDTO def) { - return controller.copy(def.getId()); + 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); + 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); + return service.listByTags(requestDTO); } } 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/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 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/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 77c4a365a4..4600e23773 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 @@ -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