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 extends ByteArrayOutputStream> 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