diff --git a/.github/workflows/ebrains.yml b/.github/workflows/ebrains.yml index 5e79e98d3..232d5e757 100644 --- a/.github/workflows/ebrains.yml +++ b/.github/workflows/ebrains.yml @@ -2,9 +2,9 @@ name: Mirror to Ebrains on: push: - branches: [ master ] + branches: [master] tags: - - '*' + - "*" jobs: to_ebrains: @@ -13,14 +13,14 @@ jobs: - name: syncmaster uses: wei/git-sync@v3 with: - source_repo: "HBPMedical/portal-backend" + source_repo: "HBPMedical/platform-backend" source_branch: "master" destination_repo: "https://ghpusher:${{ secrets.EBRAINS_GITLAB_ACCESS_TOKEN }}@gitlab.ebrains.eu/hbp-mip/portal-backend.git" destination_branch: "master" - name: synctags uses: wei/git-sync@v3 with: - source_repo: "HBPMedical/portal-backend" + source_repo: "HBPMedical/platform-backend" source_branch: "refs/tags/*" destination_repo: "https://ghpusher:${{ secrets.EBRAINS_GITLAB_ACCESS_TOKEN }}@gitlab.ebrains.eu/hbp-mip/portal-backend.git" destination_branch: "refs/tags/*" diff --git a/.github/workflows/publish_images.yml b/.github/workflows/publish_images.yml index 562e9a955..3658976b1 100644 --- a/.github/workflows/publish_images.yml +++ b/.github/workflows/publish_images.yml @@ -33,13 +33,13 @@ jobs: uses: docker/metadata-action@v4 with: images: | - hbpmip/portal-backend - docker-registry.ebrains.eu/medical-informatics-platform/portal-backend + hbpmip/platform-backend + docker-registry.ebrains.eu/medical-informatics-platform/platform-backend - name: Load cached image uses: actions/cache@v3 with: - path: /tmp/.buildx-cache/portal-backend + path: /tmp/.buildx-cache/platform-backend key: buildx-backend restore-keys: buildx-backend @@ -50,8 +50,8 @@ jobs: file: ./Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} - cache-from: type=local,src=/tmp/.buildx-cache/portal-backend - cache-to: type=local,dest=/tmp/.buildx-cache-new/portal-backend + cache-from: type=local,src=/tmp/.buildx-cache/platform-backend + cache-to: type=local,dest=/tmp/.buildx-cache-new/platform-backend # Temp fix # https://github.com/docker/build-push-action/issues/252 @@ -60,4 +60,3 @@ jobs: run: | rm -rf /tmp/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache - diff --git a/.github/workflows/publish_testing_images.yml b/.github/workflows/publish_testing_images.yml deleted file mode 100644 index 244f64885..000000000 --- a/.github/workflows/publish_testing_images.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Publish testing images - - -on: - workflow_dispatch: - -jobs: - build_and_push: - name: Build image and push to dockerhub - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Load cached image - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache/portal-backend - key: buildx-backend - restore-keys: buildx-backend - - - name: Build and Push image to dockerhub - uses: docker/build-push-action@v3 - with: - context: . - file: ./Dockerfile - push: true - tags: hbpmip/portal-backend:testing - cache-from: type=local,src=/tmp/.buildx-cache/portal-backend - cache-to: type=local,dest=/tmp/.buildx-cache-new/portal-backend - - # Temp fix - # https://github.com/docker/build-push-action/issues/252 - # https://github.com/moby/buildkit/issues/1896 - - name: Move Docker images cache - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache - diff --git a/Dockerfile b/Dockerfile index 33be0d872..1ad71384a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,18 +2,19 @@ # Build the spring boot maven project ####################################################### FROM maven:3.9.3-amazoncorretto-17 AS mvn-build-env -MAINTAINER Thanasis Karampatsis +LABEL maintainer="Thanasis Karampatsis " ENV CODE_PATH="/opt/code" WORKDIR $CODE_PATH -COPY pom.xml $CODE_PATH +COPY pom.xml $CODE_PATH/ -RUN mvn clean compile test +# Pre-fetch dependencies first to improve build cache efficiency. +RUN mvn -B -ntp dependency:go-offline COPY src/ $CODE_PATH/src -RUN mvn clean package +RUN mvn -B -ntp clean package ####################################################### # Setup the running container @@ -33,7 +34,7 @@ ENV APP_CONFIG_TEMPLATE="/opt/config/application.tmpl" ENV APP_CONFIG_LOCATION="/opt/config/application.yml" ENV SPRING_CONFIG_LOCATION="file:/opt/config/application.yml" -ENV SERVICE="portal-backend" +ENV SERVICE="platform-backend" ENV FEDERATION="default" ENV LOG_LEVEL="INFO" ENV FRAMEWORK_LOG_LEVEL="INFO" @@ -54,18 +55,18 @@ RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSI ####################################################### # Prepare the spring boot application files ####################################################### -COPY /config/application.tmpl $APP_CONFIG_TEMPLATE -COPY --from=mvn-build-env /opt/code/target/portal-backend.jar /usr/share/jars/ +COPY config/application.tmpl $APP_CONFIG_TEMPLATE +COPY --from=mvn-build-env /opt/code/target/platform-backend.jar /usr/share/jars/ ####################################################### # Configuration for the backend config files ####################################################### -ENV DISABLED_ALGORITHMS_CONFIG_PATH="/opt/portal/algorithms/disabledAlgorithms.json" -COPY /config/disabledAlgorithms.json $DISABLED_ALGORITHMS_CONFIG_PATH -VOLUME /opt/portal/api +ENV DISABLED_ALGORITHMS_CONFIG_PATH="/opt/platform/algorithms/disabledAlgorithms.json" +COPY config/disabledAlgorithms.json $DISABLED_ALGORITHMS_CONFIG_PATH +VOLUME /opt/platform/api -ENTRYPOINT ["sh", "-c", "dockerize -template $APP_CONFIG_TEMPLATE:$APP_CONFIG_LOCATION java -Daeron.term.buffer.length -jar /usr/share/jars/portal-backend.jar"] +ENTRYPOINT ["sh", "-ec", "exec dockerize -template ${APP_CONFIG_TEMPLATE}:${APP_CONFIG_LOCATION} java -Daeron.term.buffer.length -jar /usr/share/jars/platform-backend.jar"] EXPOSE 8080 -HEALTHCHECK --start-period=60s CMD curl -v --silent http://localhost:8080/services/actuator/health 2>&1 | grep UP +HEALTHCHECK --start-period=60s CMD curl --fail --silent --show-error http://localhost:8080/services/actuator/health | grep -q '"status":"UP"' diff --git a/README.md b/README.md index 7c6e53148..fd9829384 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ [![CHUV](https://img.shields.io/badge/CHUV-LREN-AF4C64.svg)](https://www.unil.ch/lren/en/home.html) [![License](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.html) -[![DockerHub](https://img.shields.io/badge/docker-hbpmip%2Fportal--backend-008bb8.svg)](https://hub.docker.com/r/hbpmip/portal-backend/) +[![DockerHub](https://img.shields.io/badge/docker-hbpmip%2Fplatform--backend-008bb8.svg)](https://hub.docker.com/r/hbpmip/platform-backend/) -# Backend for the MIP portal +# Backend for the MIP platform ## DEV Deployment To run the backend using an IDE for development, such as IntelliJ, you need a running instance of PostgreSQL. ## Deployment (using a Docker image) -Build the image: `docker build -t hbpmip/portal-backend:testing .` +Build the image: `docker build -t hbpmip/platform-backend:testing .` To use this image, you need a running instance of PostgreSQL and to configure the software using the following environment variables. @@ -19,14 +19,13 @@ To use this image, you need a running instance of PostgreSQL and to configure th * AUTHENTICATION: true for production, false for development. #### DATABASE CONFIGURATION ### -* PORTAL_DB_URL: JDBC URL to connect to the portal database, default value is "jdbc:postgresql://127.0.0.1:5432/portal". -* PORTAL_DB_SCHEMA: Database schema, default value is "public". -* PORTAL_DB_USER: User to use when connecting to the portal database, default value is "postgres". -* PORTAL_DB_PASSWORD: Password to use when connecting to the portal database. +* PLATFORM_DB_URL: JDBC URL to connect to the platform database, default value is "jdbc:postgresql://127.0.0.1:5432/platform". +* PLATFORM_DB_SCHEMA: Database schema, default value is "public". +* PLATFORM_DB_USER: User to use when connecting to the platform database, default value is "postgres". +* PLATFORM_DB_PASSWORD: Password to use when connecting to the platform database. #### EXTERNAL SERVICES ### -* EXAREME_URL: URL to Exareme server. Default is "http://localhost:9090" . -* EXAREME2_URL: URL to Exareme2 server. Default is "http://localhost:5000" . +* EXAFLOW_URL: URL to Exaflow server. Default is "http://localhost:5000" . #### KEYCLOAK ### * KEYCLOAK_AUTH_URL: Keycloak authentication URL. diff --git a/config/application.tmpl b/config/application.tmpl index 9e95b8255..45773e69b 100644 --- a/config/application.tmpl +++ b/config/application.tmpl @@ -1,4 +1,4 @@ -# Configuration template for the portal running inside a Docker container +# Configuration template for the platform running inside a Docker container ### EMBEDDED SERVER CONFIGURATION ### @@ -12,9 +12,9 @@ server: ### DATABASE CONFIGURATION ### spring: datasource: - url: {{ default .Env.PORTAL_DB_URL "jdbc:postgresql://172.17.0.1:5433/portal" }} - username: {{ default .Env.PORTAL_DB_USER "portal" }} - password: {{ default .Env.PORTAL_DB_PASSWORD "portalpwd" }} + url: {{ default .Env.PLATFORM_DB_URL "jdbc:postgresql://172.17.0.1:5433/portal" }} + username: {{ default .Env.PLATFORM_DB_USER "portal" }} + password: {{ default .Env.PLATFORM_DB_PASSWORD "portalpwd" }} driver-class-name: org.postgresql.Driver data: jpa: @@ -42,6 +42,10 @@ spring: keycloak: issuer-uri: {{ .Env.KEYCLOAK_AUTH_URL }}realms/{{ .Env.KEYCLOAK_REALM }} user-name-attribute: preferred_username + # Allow API clients (e.g. notebooks) to authenticate with Bearer JWTs. + resourceserver: + jwt: + issuer-uri: {{ .Env.KEYCLOAK_AUTH_URL }}realms/{{ .Env.KEYCLOAK_REALM }} ### AUTHENTICATION ### @@ -55,15 +59,18 @@ authentication: ### EXTERNAL SERVICES ### services: algorithmsUpdateInterval: {{ .Env.ALGORITHM_UPDATE_INTERVAL}} - exareme2: - algorithmsUrl: {{ .Env.EXAREME2_URL}}/algorithms - attributesUrl: {{ .Env.EXAREME2_URL}}/data_models_attributes - cdesMetadataUrl: {{ .Env.EXAREME2_URL}}/cdes_metadata - datasets_variables: {{ .Env.EXAREME2_URL}}/datasets_variables + exaflow: + algorithmsUrl: {{ .Env.EXAFLOW_URL}}/algorithms + attributesUrl: {{ .Env.EXAFLOW_URL}}/data_models_attributes + cdesMetadataUrl: {{ .Env.EXAFLOW_URL}}/cdes_metadata + datasets_variables: {{ .Env.EXAFLOW_URL}}/datasets_variables ### EXTERNAL FILES ### # Files are imported when building the docker image files: - pathologies_json: "file:/opt/portal/api/pathologies.json" disabledAlgorithms_json: "file:{{ .Env.DISABLED_ALGORITHMS_CONFIG_PATH}}" + + +frontend: + base-url: {{ .Env.FRONTEND_BASE_URL }} diff --git a/pom.xml b/pom.xml index 84ac0a42d..a99187e0c 100644 --- a/pom.xml +++ b/pom.xml @@ -3,10 +3,10 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - portal-backend - Medical Informatics Platform - portal-backend + platform-backend + Medical Informatics Platform - platform-backend hbp.mip - portal-backend + platform-backend 1.0.0 jar @@ -19,7 +19,6 @@ UTF-8 17 - 6.1.20 42.6.1 3.1.0 6.2.7.Final @@ -27,19 +26,17 @@ 2.10.1 2.9.0 2.1.0 - 1.18.28 + 1.18.34 org.springframework spring-orm - ${spring-context.version} org.springframework spring-context - ${spring-context.version} org.springframework.boot @@ -71,6 +68,10 @@ org.springframework.boot spring-boot-starter-oauth2-client + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + org.springframework.data spring-data-commons @@ -122,7 +123,7 @@ - portal-backend + platform-backend src/main/resources diff --git a/src/main/java/hbp/mip/algorithm/AlgorithmRequestDTO.java b/src/main/java/hbp/mip/algorithm/AlgorithmRequestDTO.java new file mode 100644 index 000000000..935146081 --- /dev/null +++ b/src/main/java/hbp/mip/algorithm/AlgorithmRequestDTO.java @@ -0,0 +1,31 @@ +package hbp.mip.algorithm; + +import hbp.mip.experiment.ExperimentExecutionDTO; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; + +@Slf4j +public record AlgorithmRequestDTO( + String request_id, + InputDataRequestDTO inputdata, + Map parameters, + Map preprocessing +) { + + public static AlgorithmRequestDTO create(UUID experimentUUID,ExperimentExecutionDTO.AlgorithmExecutionDTO algorithmExecutionDTO){ + return new AlgorithmRequestDTO( + experimentUUID.toString(), + algorithmExecutionDTO.inputdata(), + algorithmExecutionDTO.parameters(), + algorithmExecutionDTO.preprocessing() + ); + } + + public record InputDataRequestDTO(String data_model, List datasets, List x, List y, + FilterRequestDTO filters) { + } + + public record FilterRequestDTO(String condition, List rules) { + } +} diff --git a/src/main/java/hbp/mip/algorithm/AlgorithmService.java b/src/main/java/hbp/mip/algorithm/AlgorithmService.java index 5aa713623..c4d0c17f9 100644 --- a/src/main/java/hbp/mip/algorithm/AlgorithmService.java +++ b/src/main/java/hbp/mip/algorithm/AlgorithmService.java @@ -5,6 +5,7 @@ import hbp.mip.utils.CustomResourceLoader; import hbp.mip.utils.HTTPUtil; import hbp.mip.utils.Logger; +import hbp.mip.utils.Exceptions.InternalServerError; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.scheduling.annotation.Async; @@ -17,7 +18,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import static hbp.mip.utils.InputStreamConverter.convertInputStreamToString; @@ -27,67 +27,68 @@ public class AlgorithmService { private static final Gson gson = new Gson(); - private final Exareme2AlgorithmsSpecs exareme2AlgorithmsSpecs; + private final AlgorithmsSpecs algorithmsSpecs; private final CustomResourceLoader resourceLoader; @Value("${files.disabledAlgorithms_json}") private String disabledAlgorithmsFilePath; - @Value("${services.exareme2.algorithmsUrl}") - private String exareme2AlgorithmsUrl; + @Value("${services.exaflow.algorithmsUrl}") + private String exaflowAlgorithmsUrl; - public AlgorithmService(Exareme2AlgorithmsSpecs exareme2AlgorithmsSpecs, CustomResourceLoader resourceLoader) { - this.exareme2AlgorithmsSpecs = exareme2AlgorithmsSpecs; + public AlgorithmService(AlgorithmsSpecs algorithmsSpecs, CustomResourceLoader resourceLoader) { + this.algorithmsSpecs = algorithmsSpecs; this.resourceLoader = resourceLoader; } public List getAlgorithms(Logger logger) { - // Fetch exareme2 algorithm specifications and convert to generic algorithm specifications. - ArrayList exaremeAlgorithms = new ArrayList<>(); - getExareme2Algorithms(logger).forEach(algorithm -> exaremeAlgorithms.add(new AlgorithmSpecificationDTO(algorithm))); + List exaflowAlgorithms = getExaflowAlgorithms(logger); List disabledAlgorithms = getDisabledAlgorithms(logger); logger.debug("Disabled algorithms: " + disabledAlgorithms); // Remove any disabled algorithm ArrayList enabledAlgorithms = new ArrayList<>(); - for (AlgorithmSpecificationDTO algorithm : exaremeAlgorithms) { + for (AlgorithmSpecificationDTO algorithm : exaflowAlgorithms) { if (!disabledAlgorithms.contains(algorithm.name())) { enabledAlgorithms.add(algorithm); } } - logger.debug("Disabled " + (exaremeAlgorithms.size() - enabledAlgorithms.size()) + " algorithms."); + logger.debug("Disabled " + (exaflowAlgorithms.size() - enabledAlgorithms.size()) + " algorithms."); return enabledAlgorithms; } /** - * This method gets all the available exareme2 algorithms and removes the disabled. + * This method gets all the available exaflow algorithms and removes the + * disabled. * - * @return a list of Exareme2AlgorithmSpecificationDTO or null if something fails + * @return a list of ExaflowAlgorithmSpecificationDTO or null if something + * fails */ - private List getExareme2Algorithms(Logger logger) { - List algorithms; + private List getExaflowAlgorithms(Logger logger) { + List algorithms; StringBuilder response = new StringBuilder(); try { - HTTPUtil.sendGet(exareme2AlgorithmsUrl, response); + HTTPUtil.sendGet(exaflowAlgorithmsUrl, response); algorithms = gson.fromJson( response.toString(), - new TypeToken>() { - }.getType() - ); + new TypeToken>() { + }.getType()); } catch (Exception e) { - logger.error("Could not fetch exareme2 algorithms: " + e.getMessage()); - return Collections.emptyList(); + logger.error("Could not fetch exaflow algorithms: " + e.getMessage()); + throw new InternalServerError("Could not fetch exaflow algorithms."); + } + + if (algorithms == null || algorithms.isEmpty()) { + String errorMessage = "Exaflow algorithms response was empty."; + logger.error(errorMessage); + throw new InternalServerError(errorMessage); } - // Filter out algorithms with type "flower" - algorithms = algorithms.stream() - .filter(algorithm -> "exareme2".equals(algorithm.type())) - .collect(Collectors.toList()); - logger.debug("Fetched " + algorithms.size() + " exareme2 algorithms."); - exareme2AlgorithmsSpecs.setAlgorithms(algorithms); + logger.debug("Fetched " + algorithms.size() + " exaflow algorithms."); + algorithmsSpecs.setAlgorithms(algorithms); return algorithms; } @@ -96,15 +97,17 @@ public static class AlgorithmAggregator { private final AlgorithmService algorithmService; - public AlgorithmAggregator(AlgorithmService algorithmService){ + public AlgorithmAggregator(AlgorithmService algorithmService) { this.algorithmService = algorithmService; } + @Async @Scheduled(fixedDelayString = "${services.algorithmsUpdateInterval}000") public void scheduleFixedRateTaskAsync() { - algorithmService.getExareme2Algorithms(new Logger("AlgorithmAggregator","(GET) /algorithms")); + algorithmService.getExaflowAlgorithms(new Logger("AlgorithmAggregator", "(GET) /algorithms")); } } + /** * Fetches the disabled algorithms from a .json file * @@ -116,8 +119,7 @@ private List getDisabledAlgorithms(Logger logger) { return gson.fromJson( convertInputStreamToString(resource.getInputStream()), new TypeToken>() { - }.getType() - ); + }.getType()); } catch (IOException e) { logger.error("Could not load the disabled algorithms. Exception: " + e.getMessage()); return Collections.emptyList(); diff --git a/src/main/java/hbp/mip/algorithm/AlgorithmSpecificationDTO.java b/src/main/java/hbp/mip/algorithm/AlgorithmSpecificationDTO.java index 0c3c006b3..73446e0bf 100644 --- a/src/main/java/hbp/mip/algorithm/AlgorithmSpecificationDTO.java +++ b/src/main/java/hbp/mip/algorithm/AlgorithmSpecificationDTO.java @@ -1,161 +1,77 @@ package hbp.mip.algorithm; -import hbp.mip.utils.Exceptions.InternalServerError; +import com.google.gson.annotations.SerializedName; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; - +import java.util.Objects; public record AlgorithmSpecificationDTO( String name, String label, String desc, - List parameters, - List preprocessing -){ - public record TransformerSpecificationDTO(String name, String label, String desc, - List parameters) { - public TransformerSpecificationDTO(Exareme2AlgorithmSpecificationDTO.Exareme2TransformerSpecificationDTO transformerDTO) { - this( - transformerDTO.name(), - transformerDTO.label(), - transformerDTO.desc(), - getAlgorithmParameterSpecifications(transformerDTO.parameters()) - ); - } - - private static List getAlgorithmParameterSpecifications( - Map exareme2Parameters - ) { - List parameters = new ArrayList<>(); - exareme2Parameters.forEach((name, parameterDTO) - -> parameters.add(new AlgorithmParameterSpecificationDTO(name, parameterDTO))); - return parameters; - } + ExaflowAlgorithmInputdataSpecificationDTO inputdata, + Map parameters, + List preprocessing) { + @Override + public Map parameters() { + return Objects.requireNonNullElse(parameters, Collections.emptyMap()); + } + @Override + public List preprocessing() { + return Objects.requireNonNullElse(preprocessing, Collections.emptyList()); } public record AlgorithmParameterSpecificationDTO( - String name, String label, String desc, - String type, - String columnValuesSQLType, - String columnValuesIsCategorical, - String defaultValue, - String valueType, - String valueNotBlank, - String valueMultiple, - String valueMin, - String valueMax, - List valueEnumerations - ) { - public AlgorithmParameterSpecificationDTO(String name, Exareme2AlgorithmSpecificationDTO.Exareme2AlgorithmParameterSpecificationDTO parameter) { - this( - name, - parameter.label(), - parameter.desc(), - "other", - "", - "", - parameter.default_value(), - parameter.types().get(0), - parameter.notblank(), - parameter.multiple(), - parameter.min(), - parameter.max(), - parameter.enums() != null ? parameter.enums().source() : null - ); - } + List types, + String required, + String multiple, + String min, + String max, + @SerializedName("default") String default_value, + AlgorithmEnumDTO enums, + AlgorithmEnumDTO dict_keys_enums, + AlgorithmEnumDTO dict_values_enums - public AlgorithmParameterSpecificationDTO(String name, Exareme2AlgorithmSpecificationDTO.Exareme2AlgorithmInputDataDetailSpecificationDTO inputDataDetail) { - this( - name, - inputDataDetail.label(), - inputDataDetail.desc(), - getParameterType(name), - getParameterColumnValuesSQLType(name, inputDataDetail.types()), - getParameterColumnValuesIsCategorical(name, inputDataDetail.stattypes()), - "", - getParameterValueType(name, inputDataDetail.types()), - inputDataDetail.notblank(), - inputDataDetail.multiple(), - "", - "", - null - ); - } - - private static String getParameterType(String inputDataDetailName){ - if (inputDataDetailName.equals("dataset") || inputDataDetailName.equals("filter") || inputDataDetailName.equals("pathology")) { - return inputDataDetailName; - } - return "column"; - } - - private static String getParameterValueType(String inputDataDetailName, List types){ - if (inputDataDetailName.equals("dataset") || inputDataDetailName.equals("filter") || inputDataDetailName.equals("pathology")) { - return types.get(0); - } - return ""; - } - - private static String getParameterColumnValuesSQLType(String inputDataDetailName, List types){ - if (inputDataDetailName.equals("dataset") || inputDataDetailName.equals("filter") || inputDataDetailName.equals("pathology")) { - return ""; - } - return String.join(", ", types); - } - - private static String getParameterColumnValuesIsCategorical(String inputDataDetailName, List stattypes){ - if (inputDataDetailName.equals("dataset") || inputDataDetailName.equals("filter") || inputDataDetailName.equals("pathology")) { - return ""; - } - - if (stattypes.contains("nominal") && stattypes.contains("numerical")) { - return ""; - } else if (stattypes.contains("nominal")) { - return "true"; - } else if (stattypes.contains("numerical")) { - return "false"; - } else { - throw new InternalServerError("Invalid stattypes: " + stattypes); - } + ) { + public record AlgorithmEnumDTO( + String type, + List source) { } } - public AlgorithmSpecificationDTO(Exareme2AlgorithmSpecificationDTO exareme2Algorithm){ - this( - exareme2Algorithm.name(), - exareme2Algorithm.label(), - exareme2Algorithm.desc(), - getAlgorithmParameters(exareme2Algorithm), - getTransformers(exareme2Algorithm.preprocessing()) - ); + public record ExaflowAlgorithmInputdataSpecificationDTO( + AlgorithmInputDataDetailSpecificationDTO x, + AlgorithmInputDataDetailSpecificationDTO y, + AlgorithmInputDataDetailSpecificationDTO data_model, + AlgorithmInputDataDetailSpecificationDTO datasets, + AlgorithmInputDataDetailSpecificationDTO filter) { } - private static List getAlgorithmParameters(Exareme2AlgorithmSpecificationDTO exareme2Algorithm){ - List parameters = new ArrayList<>(); - if (exareme2Algorithm.inputdata().y() != null) { - parameters.add(new AlgorithmParameterSpecificationDTO("y", exareme2Algorithm.inputdata().y())); - } - if (exareme2Algorithm.inputdata().x() != null) { - parameters.add(new AlgorithmParameterSpecificationDTO("x", exareme2Algorithm.inputdata().x())); - } - parameters.add(new AlgorithmParameterSpecificationDTO("pathology", exareme2Algorithm.inputdata().data_model())); - parameters.add(new AlgorithmParameterSpecificationDTO("dataset", exareme2Algorithm.inputdata().datasets())); - parameters.add(new AlgorithmParameterSpecificationDTO("filter", exareme2Algorithm.inputdata().filter())); - exareme2Algorithm.parameters().forEach((name, parameterDTO) - -> parameters.add(new AlgorithmParameterSpecificationDTO(name, parameterDTO))); - return parameters; - } + public record AlgorithmInputDataDetailSpecificationDTO( + String label, + String desc, + List types, + List stattypes, + String required, + String multiple, + Integer enumslen - private static List getTransformers(List exareme2Transformers){ - List preprocessing = new ArrayList<>(); - exareme2Transformers.forEach(exareme2TransformerSpecificationDTO - -> preprocessing.add(new TransformerSpecificationDTO(exareme2TransformerSpecificationDTO))); - return preprocessing; + ) { } + public record TransformerSpecificationDTO( + String name, + String label, + String desc, + Map parameters) { + @Override + public Map parameters() { + return Objects.requireNonNullElse(parameters, Collections.emptyMap()); + } + } } diff --git a/src/main/java/hbp/mip/algorithm/Exareme2AlgorithmsSpecs.java b/src/main/java/hbp/mip/algorithm/AlgorithmsSpecs.java similarity index 62% rename from src/main/java/hbp/mip/algorithm/Exareme2AlgorithmsSpecs.java rename to src/main/java/hbp/mip/algorithm/AlgorithmsSpecs.java index 5dd7eb1dc..eb1447d3d 100644 --- a/src/main/java/hbp/mip/algorithm/Exareme2AlgorithmsSpecs.java +++ b/src/main/java/hbp/mip/algorithm/AlgorithmsSpecs.java @@ -10,6 +10,6 @@ @Getter @Setter @Component -public class Exareme2AlgorithmsSpecs { - private List algorithms = new ArrayList<>(); +public class AlgorithmsSpecs { + private List algorithms = new ArrayList<>(); } diff --git a/src/main/java/hbp/mip/algorithm/Exareme2AlgorithmRequestDTO.java b/src/main/java/hbp/mip/algorithm/Exareme2AlgorithmRequestDTO.java deleted file mode 100644 index 9da4a1f46..000000000 --- a/src/main/java/hbp/mip/algorithm/Exareme2AlgorithmRequestDTO.java +++ /dev/null @@ -1,212 +0,0 @@ -package hbp.mip.algorithm; - -import com.google.gson.JsonSyntaxException; -import hbp.mip.experiment.ExperimentExecutionDTO; -import hbp.mip.utils.Exceptions.InternalServerError; -import hbp.mip.utils.JsonConverters; - -import java.util.*; - -public record Exareme2AlgorithmRequestDTO( - String request_id, - Exareme2InputDataRequestDTO inputdata, - Map parameters, - Map preprocessing, - String type -) { - - public static Exareme2AlgorithmRequestDTO create( - UUID experimentUUID, - List exaremeAlgorithmRequestParamDTOs, - List exaremeTransformers, - Exareme2AlgorithmSpecificationDTO exareme2AlgorithmSpecificationDTO) { - - // List of inputDataFields - List inputDataFields = Arrays.asList("pathology", "dataset", "x", "y", "filter"); - - // Create lists to hold the separated DTOs - List inputDataDTOs = new ArrayList<>(); - List parametersDTOs = new ArrayList<>(); - - // Split the DTOs into the respective lists - for (ExperimentExecutionDTO.AlgorithmExecutionDTO.AlgorithmParameterExecutionDTO dto : exaremeAlgorithmRequestParamDTOs) { - if (inputDataFields.contains(dto.name())) { - inputDataDTOs.add(dto); - } else { - parametersDTOs.add(dto); - } - } - - // Call the constructor with the separated lists - return new Exareme2AlgorithmRequestDTO( - experimentUUID.toString(), - getInputData(inputDataDTOs), - getParameters(parametersDTOs, exareme2AlgorithmSpecificationDTO), - getPreprocessing(exaremeTransformers, exareme2AlgorithmSpecificationDTO), - "exareme2" - ); - } - - private static Exareme2InputDataRequestDTO getInputData(List exaremeAlgorithmRequestParamDTOs) { - if (exaremeAlgorithmRequestParamDTOs == null) { - return null; - } - - Map parameters = new HashMap<>(); - for (var parameter : exaremeAlgorithmRequestParamDTOs) { - parameters.put(parameter.name(), parameter.value()); - } - - // ATTENTION: - // pathology and dataset fields are mandatory and should always exist in the parameters. - // x, y, filter fields are optional. - - String data_model = parameters.get("pathology"); - - List datasets = Arrays.asList(parameters.get("dataset").split(",")); - - List x = null; - if (parameters.containsKey("x") && parameters.get("x") != null) - x = Arrays.asList(parameters.get("x").split(",")); - - List y = null; - if (parameters.containsKey("y") && parameters.get("y") != null) - y = Arrays.asList(parameters.get("y").split(",")); - - Exareme2FilterRequestDTO filters = null; - if (parameters.containsKey("filter") && parameters.get("filter") != null && !parameters.get("filter").isEmpty()) - filters = JsonConverters.convertJsonStringToObject(parameters.get("filter"), Exareme2AlgorithmRequestDTO.Exareme2FilterRequestDTO.class); - - return new Exareme2AlgorithmRequestDTO.Exareme2InputDataRequestDTO( - data_model, - datasets, - x, - y, - filters - ); - } - - private static Map getParameters(List exaremeAlgorithmRequestParamDTOs, Exareme2AlgorithmSpecificationDTO exareme2AlgorithmSpecificationDTO) { - if (exaremeAlgorithmRequestParamDTOs == null) { - return null; - } - - HashMap exareme2Parameters = new HashMap<>(); - exaremeAlgorithmRequestParamDTOs.forEach(parameter -> { - Exareme2AlgorithmSpecificationDTO.Exareme2AlgorithmParameterSpecificationDTO paramSpecDto = exareme2AlgorithmSpecificationDTO.parameters().get(parameter.name()); - if (paramSpecDto == null){ - throw new InternalServerError("Parameter " + parameter.name() + " not found in algorithm:" + exareme2AlgorithmSpecificationDTO.name()); - } - exareme2Parameters.put(parameter.name(), convertStringToProperExareme2ParameterTypeAccordingToSpecs(parameter.value(), paramSpecDto)); - }); - return exareme2Parameters; - } - - private static Map getPreprocessing(List exaremeTransformers, Exareme2AlgorithmSpecificationDTO exareme2AlgorithmSpecificationDTO) { - if (exaremeTransformers == null) { - return null; - } - - HashMap exareme2Preprocessing = new HashMap<>(); - exaremeTransformers.forEach(transformer -> { - String transformer_name = transformer.name(); - HashMap transformerParameterDTOs = new HashMap<>(); - for (ExperimentExecutionDTO.AlgorithmExecutionDTO.AlgorithmParameterExecutionDTO parameter : transformer.parameters()){ - String param_name = parameter.name(); - Optional transformerSpecificationDTO = exareme2AlgorithmSpecificationDTO.preprocessing().stream() - .filter(transformerSpec-> transformerSpec.name().equals(transformer_name)) - .findFirst(); - if (transformerSpecificationDTO.isEmpty()) throw new InternalServerError("Missing the transformer: " + transformer_name); - - Exareme2AlgorithmSpecificationDTO.Exareme2AlgorithmParameterSpecificationDTO paramSpecDto = transformerSpecificationDTO.get().parameters().get(param_name); - if (paramSpecDto == null){ - throw new InternalServerError("Parameter " + parameter.name() + " not found in transformer:" + transformerSpecificationDTO.get().name()); - } - transformerParameterDTOs.put(param_name, convertStringToProperExareme2ParameterTypeAccordingToSpecs(parameter.value(), paramSpecDto)); - } - exareme2Preprocessing.put(transformer_name, transformerParameterDTOs); - }); - - return exareme2Preprocessing; - } - - private static Object convertStringToProperExareme2ParameterTypeAccordingToSpecs(String value, Exareme2AlgorithmSpecificationDTO.Exareme2AlgorithmParameterSpecificationDTO paramSpecDto) { - if (paramSpecDto.enums() != null){ - return value; - } - return convertStringToProperExareme2ParameterType(value); - } - - private static Object convertStringToProperExareme2ParameterType(String str) { - if (isMap(str)) - return JsonConverters.convertJsonStringToObject(str, Map.class); - - String[] stringValues = str.split(","); - if (stringValues.length == 0) - return ""; - - if (stringValues.length == 1) - return convertStringToProperType(stringValues[0]); - - List values = new ArrayList<>(); - for (String value : stringValues) { - values.add(convertStringToProperType(value)); - } - - return values; - } - - private static Object convertStringToProperType(String str) { - if (isInteger(str)) - return Integer.parseInt(str); - else if (isFloat(str)) - return Double.parseDouble(str); - else - return str; - } - - private static boolean isFloat(String strNum) { - if (strNum == null) { - return false; - } - try { - Double.parseDouble(strNum); - } catch (NumberFormatException nfe) { - return false; - } - return true; - } - - private static boolean isInteger(String strNum) { - if (strNum == null) { - return false; - } - try { - Integer.parseInt(strNum); - } catch (NumberFormatException nfe) { - return false; - } - return true; - } - - private static boolean isMap(String strMap) { - if (strMap == null) { - return false; - } - try { - JsonConverters.convertJsonStringToObject(strMap, Map.class); - } catch (JsonSyntaxException e) { - return false; - } - return true; - } - - public record Exareme2InputDataRequestDTO(String data_model, List datasets, List x, List y, - Exareme2FilterRequestDTO filters) { - } - - public record Exareme2FilterRequestDTO(String condition, List rules) { - } - - -} diff --git a/src/main/java/hbp/mip/algorithm/Exareme2AlgorithmSpecificationDTO.java b/src/main/java/hbp/mip/algorithm/Exareme2AlgorithmSpecificationDTO.java deleted file mode 100644 index b987a7741..000000000 --- a/src/main/java/hbp/mip/algorithm/Exareme2AlgorithmSpecificationDTO.java +++ /dev/null @@ -1,83 +0,0 @@ -package hbp.mip.algorithm; - -import com.google.gson.annotations.SerializedName; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public record Exareme2AlgorithmSpecificationDTO( - String name, - String label, - String desc, - Exareme2AlgorithmInputdataSpecificationDTO inputdata, - Map parameters, - List preprocessing, - String type -) { - @Override - public Map parameters() { - return Objects.requireNonNullElse(parameters, Collections.EMPTY_MAP); - } - - @Override - public List preprocessing() { - return Objects.requireNonNullElse(preprocessing, Collections.EMPTY_LIST); - } - - public record Exareme2AlgorithmParameterSpecificationDTO( - String label, - String desc, - List types, - String notblank, - String multiple, - String min, - String max, - @SerializedName("default") - String default_value, - Exareme2AlgorithmEnumDTO enums, - Exareme2AlgorithmEnumDTO dict_keys_enums, - Exareme2AlgorithmEnumDTO dict_values_enums - - ) { - public record Exareme2AlgorithmEnumDTO( - String type, - List source - ) { - } - } - - public record Exareme2AlgorithmInputdataSpecificationDTO( - Exareme2AlgorithmInputDataDetailSpecificationDTO x, - Exareme2AlgorithmInputDataDetailSpecificationDTO y, - Exareme2AlgorithmInputDataDetailSpecificationDTO data_model, - Exareme2AlgorithmInputDataDetailSpecificationDTO datasets, - Exareme2AlgorithmInputDataDetailSpecificationDTO filter - ) { - } - - public record Exareme2AlgorithmInputDataDetailSpecificationDTO( - String label, - String desc, - List types, - List stattypes, - String notblank, - String multiple, - Integer enumslen - - ) { - } - - public record Exareme2TransformerSpecificationDTO( - String name, - String label, - String desc, - Map parameters - ) { - @Override - public Map parameters() { - return Objects.requireNonNullElse(parameters, Collections.EMPTY_MAP); - } - } -} diff --git a/src/main/java/hbp/mip/configurations/FrontendRedirectCaptureFilter.java b/src/main/java/hbp/mip/configurations/FrontendRedirectCaptureFilter.java new file mode 100644 index 000000000..abe6bda7b --- /dev/null +++ b/src/main/java/hbp/mip/configurations/FrontendRedirectCaptureFilter.java @@ -0,0 +1,125 @@ +package hbp.mip.configurations; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +@Component +public class FrontendRedirectCaptureFilter extends OncePerRequestFilter { + + private static final String AUTHORIZATION_PATH = "/oauth2/authorization"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (isAuthorizationRequest(request)) { + CapturedRedirect redirect = resolveRedirect(request); + if (redirect != null && StringUtils.hasText(redirect.targetPath())) { + HttpSession session = request.getSession(true); + session.setAttribute(SpaRedirectAuthenticationSuccessHandler.REDIRECT_PATH_ATTRIBUTE, redirect.targetPath()); + if (StringUtils.hasText(redirect.frontendBaseUrl())) { + session.setAttribute(SpaRedirectAuthenticationSuccessHandler.FRONTEND_BASE_URL_ATTRIBUTE, redirect.frontendBaseUrl()); + } + } + } + + filterChain.doFilter(request, response); + } + + private boolean isAuthorizationRequest(HttpServletRequest request) { + String contextPath = request.getContextPath(); + String requestUri = request.getRequestURI(); + String expectedPrefix = contextPath + AUTHORIZATION_PATH; + return requestUri.startsWith(expectedPrefix); + } + + private CapturedRedirect resolveRedirect(HttpServletRequest request) { + String explicitValue = request.getParameter("frontend_redirect"); + if (StringUtils.hasText(explicitValue)) { + URI explicitUri = parseUri(explicitValue); + if (explicitUri != null && StringUtils.hasText(explicitUri.getScheme()) && StringUtils.hasText(explicitUri.getRawAuthority())) { + // frontend_redirect can be a full URL (e.g. http://localhost:4200/#/home). + String baseUrl = explicitUri.getScheme() + "://" + explicitUri.getRawAuthority(); + String targetPath = buildPathQueryFragment(explicitUri); + return new CapturedRedirect(baseUrl, targetPath); + } + + // Otherwise treat it as a path (or hash route) and normalize. + return new CapturedRedirect(null, normalizePath(explicitValue)); + } + + URI refererUri = parseUri(request.getHeader("Referer")); + if (refererUri != null) { + String baseUrl = null; + if (StringUtils.hasText(refererUri.getScheme()) && StringUtils.hasText(refererUri.getRawAuthority())) { + baseUrl = refererUri.getScheme() + "://" + refererUri.getRawAuthority(); + } + String targetPath = buildPathQueryFragment(refererUri); + return new CapturedRedirect(baseUrl, targetPath); + } + + return null; + } + + private URI parseUri(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + try { + return new URI(value); + } catch (URISyntaxException ex) { + return null; + } + } + + private static String buildPathQueryFragment(URI uri) { + String path = uri.getRawPath(); + String query = uri.getRawQuery(); + String fragment = uri.getRawFragment(); + + if (!StringUtils.hasText(path)) { + path = "/"; + } + + String normalizedPath = normalizePath(path); + if (StringUtils.hasText(query)) { + normalizedPath += "?" + query; + } + if (StringUtils.hasText(fragment)) { + normalizedPath += "#" + fragment; + } + + return normalizedPath; + } + + private static String normalizePath(String path) { + if (!StringUtils.hasText(path)) { + return "/"; + } + + String trimmed = path.trim(); + + if (!trimmed.startsWith("/")) { + trimmed = "/" + trimmed; + } + + if (!trimmed.equals("/") && trimmed.endsWith("/")) { + trimmed = trimmed.substring(0, trimmed.length() - 1); + } + + return trimmed; + } + + private record CapturedRedirect(String frontendBaseUrl, String targetPath) { + } +} diff --git a/src/main/java/hbp/mip/configurations/OpenApiConfig.java b/src/main/java/hbp/mip/configurations/OpenApiConfig.java index c02348d23..0ee5264d3 100644 --- a/src/main/java/hbp/mip/configurations/OpenApiConfig.java +++ b/src/main/java/hbp/mip/configurations/OpenApiConfig.java @@ -12,7 +12,7 @@ public class OpenApiConfig { @Bean public OpenAPI usersMicroserviceOpenAPI() { return new OpenAPI() - .info(new Info().title("Portal-backend API") + .info(new Info().title("Platform-backend API") .version("1.0")); } } \ No newline at end of file diff --git a/src/main/java/hbp/mip/configurations/PersistenceConfiguration.java b/src/main/java/hbp/mip/configurations/PersistenceConfiguration.java index b915afdd8..c8345f426 100644 --- a/src/main/java/hbp/mip/configurations/PersistenceConfiguration.java +++ b/src/main/java/hbp/mip/configurations/PersistenceConfiguration.java @@ -22,7 +22,7 @@ public class PersistenceConfiguration { @Primary @Bean(name = "datasource") @ConfigurationProperties(prefix = "spring.datasource") - public DataSource portalDataSource() { + public DataSource platformDataSource() { return DataSourceBuilder.create().build(); } @@ -30,7 +30,7 @@ public DataSource portalDataSource() { @DependsOn("flyway") public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean(); - emfb.setDataSource(portalDataSource()); + emfb.setDataSource(platformDataSource()); JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); emfb.setJpaVendorAdapter(vendorAdapter); emfb.setPackagesToScan("hbp.mip.experiment", "hbp.mip.user"); @@ -42,13 +42,7 @@ public LocalContainerEntityManagerFactoryBean entityManagerFactory() { public Flyway migrations() { Flyway flyway = new Flyway(); flyway.setBaselineOnMigrate(true); - flyway.setDataSource(portalDataSource()); + flyway.setDataSource(platformDataSource()); return flyway; - -// TODO Flyway upgrade to latest version -// return Flyway.configure() -// .dataSource(portalDataSource()) -// .baselineOnMigrate(true) -// .load(); } } diff --git a/src/main/java/hbp/mip/configurations/SecurityConfiguration.java b/src/main/java/hbp/mip/configurations/SecurityConfiguration.java index 879168b7e..634ed2265 100644 --- a/src/main/java/hbp/mip/configurations/SecurityConfiguration.java +++ b/src/main/java/hbp/mip/configurations/SecurityConfiguration.java @@ -11,16 +11,21 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import java.util.*; import java.util.stream.Collectors; @@ -30,12 +35,26 @@ @EnableMethodSecurity public class SecurityConfiguration { + private final SpaRedirectAuthenticationSuccessHandler spaRedirectAuthenticationSuccessHandler; + private final FrontendRedirectCaptureFilter frontendRedirectCaptureFilter; + @Value("${authentication.enabled}") private boolean authenticationEnabled; - // This Bean is used when there is no authentication and there is no keycloak server running due to this bug: + @Value("${frontend.base-url:}") + private String frontendBaseUrl; + + public SecurityConfiguration(SpaRedirectAuthenticationSuccessHandler spaRedirectAuthenticationSuccessHandler, + FrontendRedirectCaptureFilter frontendRedirectCaptureFilter) { + this.spaRedirectAuthenticationSuccessHandler = spaRedirectAuthenticationSuccessHandler; + this.frontendRedirectCaptureFilter = frontendRedirectCaptureFilter; + } + + // This Bean is used when there is no authentication and there is no keycloak + // server running due to this bug: // https://github.com/spring-projects/spring-security/issues/11397#issuecomment-1655906163 - // So we overwrite the ClientRegistrationRepository Bean to avoid the IP server lookup. + // So we overwrite the ClientRegistrationRepository Bean to avoid the IP server + // lookup. @Bean @ConditionalOnProperty(prefix = "authentication", name = "enabled", havingValue = "0") public ClientRegistrationRepository clientRegistrationRepository() { @@ -54,8 +73,10 @@ public ClientRegistrationRepository clientRegistrationRepository() { } @Bean - SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepo) throws Exception { + SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, + ClientRegistrationRepository clientRegistrationRepo) throws Exception { if (authenticationEnabled) { + http.addFilterBefore(frontendRedirectCaptureFilter, OAuth2AuthorizationRequestRedirectFilter.class); http.authorizeHttpRequests(auth -> auth .requestMatchers( "/login/**", @@ -64,17 +85,24 @@ SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRegistrat "/v3/api-docs", "/v3/api-docs/**", "/swagger-ui/**", - "/swagger-ui.html" - ).permitAll() - .requestMatchers("/**").authenticated() - ); + "/swagger-ui.html") + .permitAll() + .requestMatchers("/**").authenticated()); + + http.oauth2Login(login -> login.successHandler(spaRedirectAuthenticationSuccessHandler)); - http.oauth2Login(login -> login.defaultSuccessUrl("/", true)); + // Allow API clients (e.g. notebooks) to authenticate with Bearer JWTs. + // This runs alongside oauth2Login (session-based) authentication. + http.oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))); // Open ID Logout // https://docs.spring.io/spring-security/reference/servlet/oauth2/login/advanced.html#oauth2login-advanced-oidc-logout - OidcClientInitiatedLogoutSuccessHandler successHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepo); - successHandler.setPostLogoutRedirectUri("{baseUrl}"); + OidcClientInitiatedLogoutSuccessHandler successHandler = new OidcClientInitiatedLogoutSuccessHandler( + clientRegistrationRepo); + if (StringUtils.hasText(frontendBaseUrl)) { + successHandler.setPostLogoutRedirectUri(frontendBaseUrl); + } http.logout(logout -> logout.logoutSuccessHandler(successHandler)); // ---> XSRF Token handling @@ -91,39 +119,103 @@ SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRegistrat http.csrf((csrf) -> csrf .csrfTokenRepository(tokenRepository) .csrfTokenRequestHandler(requestHandler::handle) - .ignoringRequestMatchers("/logout") - ); + // Bearer-token clients should not need CSRF (they are not cookie-authenticated). + .ignoringRequestMatchers((request) -> { + String authz = request.getHeader("Authorization"); + return authz != null && authz.startsWith("Bearer "); + }) + .ignoringRequestMatchers("/logout")); // <--- XSRF Token handling - } else { http.authorizeHttpRequests(auth -> auth - .requestMatchers("/**").permitAll() - ); + .requestMatchers("/**").permitAll()); http.csrf((csrf) -> csrf - .ignoringRequestMatchers("/**") - ); + .ignoringRequestMatchers("/**")); } return http.build(); } + private static JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter fallback = new JwtGrantedAuthoritiesConverter(); + // Do not force ROLE_ prefix; the app expects raw authority strings (e.g. research_dataset_*). + fallback.setAuthorityPrefix(""); + + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter((Jwt jwt) -> { + List out = new ArrayList<>(); + + // 1) Preferred: our realm may map roles directly into an "authorities" claim. + Object raw = jwt.getClaims().get("authorities"); + if (raw instanceof Collection col) { + for (Object v : col) { + if (v != null) { + out.add(new SimpleGrantedAuthority(v.toString())); + } + } + } + + // 2) Keycloak default: realm_access.roles + Object realmAccess = jwt.getClaims().get("realm_access"); + if (realmAccess instanceof Map m) { + Object roles = m.get("roles"); + if (roles instanceof Collection col) { + for (Object v : col) { + if (v != null) { + out.add(new SimpleGrantedAuthority(v.toString())); + } + } + } + } + + // 3) Keycloak default: resource_access..roles (collect all client roles) + Object resourceAccess = jwt.getClaims().get("resource_access"); + if (resourceAccess instanceof Map ra) { + for (Object entryVal : ra.values()) { + if (!(entryVal instanceof Map m)) { + continue; + } + Object roles = m.get("roles"); + if (!(roles instanceof Collection col)) { + continue; + } + for (Object v : col) { + if (v != null) { + out.add(new SimpleGrantedAuthority(v.toString())); + } + } + } + } + + if (!out.isEmpty()) { + return out; + } + + // Fallback to scope-based authorities if present. + return fallback.convert(jwt); + }); + return converter; + } + @Component @RequiredArgsConstructor static class GrantedAuthoritiesMapperImpl implements GrantedAuthoritiesMapper { private static Collection extractAuthorities(Map claims) { - Collection authorities = (Collection) claims.get("authorities"); - if (authorities == null) { - return Collections.emptyList(); // or throw a more informative exception if appropriate + Object rawAuthorities = claims.get("authorities"); + if (!(rawAuthorities instanceof Collection authorities)) { + return Collections.emptyList(); } return authorities.stream() + .filter(Objects::nonNull) + .map(Object::toString) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); } - @Override - public Collection mapAuthorities(Collection authorities) { + public Collection mapAuthorities( + Collection authorities) { Set mappedAuthorities = new HashSet<>(); authorities.forEach(authority -> { @@ -136,4 +228,3 @@ public Collection mapAuthorities(Collection 1 && normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + + return normalized; + } + + private static String normalizeBaseUrl(String baseUrl) { + String trimmed = baseUrl.trim(); + if (trimmed.endsWith("/")) { + trimmed = trimmed.substring(0, trimmed.length() - 1); + } + return trimmed; + } +} diff --git a/src/main/java/hbp/mip/datamodel/DataModelAPI.java b/src/main/java/hbp/mip/datamodel/DataModelAPI.java new file mode 100644 index 000000000..c51cc9280 --- /dev/null +++ b/src/main/java/hbp/mip/datamodel/DataModelAPI.java @@ -0,0 +1,40 @@ +package hbp.mip.datamodel; + +import hbp.mip.user.ActiveUserService; +import hbp.mip.utils.Logger; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +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; + +@RestController +@RequestMapping(value = "/data-models") +public class DataModelAPI { + + private final DataModelService dataModelService; + + private final ActiveUserService activeUserService; + + public DataModelAPI(ActiveUserService activeUserService, DataModelService dataModelService) { + this.activeUserService = activeUserService; + this.dataModelService = dataModelService; + } + + @GetMapping + public ResponseEntity> getDataModels(Authentication authentication) { + var logger = new Logger(activeUserService.getActiveUser(authentication).username(), "(GET) /dataModels"); + logger.info("Request for dataModels."); + var dataModels = dataModelService.getDataModels(authentication, logger); + + String userDataModelsSTR = dataModels.stream().map(DataModelDTO::code) + .collect(Collectors.joining(", ")); + logger.info("DataModels returned: " + dataModels.size() + ". [" + userDataModelsSTR + "]."); + + return ResponseEntity.ok(dataModels); + } + +} diff --git a/src/main/java/hbp/mip/datamodel/DataModelDTO.java b/src/main/java/hbp/mip/datamodel/DataModelDTO.java new file mode 100644 index 000000000..6eb2f9a18 --- /dev/null +++ b/src/main/java/hbp/mip/datamodel/DataModelDTO.java @@ -0,0 +1,184 @@ +package hbp.mip.datamodel; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public record DataModelDTO( + String code, + String version, + String label, + Boolean longitudinal, + List variables, + List groups, + List datasets, + Map> datasetsVariables +) { + + + public DataModelDTO withDatasets(Map> datasetVariablesByDataset, + List availableDatasets, + List datasetEnumerationsOverride) { + // Find the datasets enumeration if it exists in variables or groups or use override + List datasets = datasetEnumerationsOverride != null + ? datasetEnumerationsOverride + : findDatasetEnumerations(this.variables, this.groups); + List filteredDatasets = filterDatasetsByAvailability(datasets, availableDatasets); + Map> datasetsVariables = normaliseDatasetVariables(datasetVariablesByDataset, filteredDatasets); + // Return a new instance of DataModelDTO with datasets and variables set appropriately + return new DataModelDTO(this.code, this.version, this.label, this.longitudinal, this.variables, this.groups, filteredDatasets, datasetsVariables); + } + + private static List findDatasetEnumerations(List variables, List groups) { + // Check the top-level variables list for a dataset code + Optional> datasetEnumerations = findDatasetInVariables(variables); + if (datasetEnumerations.isPresent()) { + return datasetEnumerations.get(); + } + + // If not found in the top level, search recursively in the groups + if (groups == null) { + return null; // Return null if no dataset variable is found + } + + for (DataModelGroupDTO group : groups) { + if (group == null) { + continue; + } + + datasetEnumerations = findDatasetInGroup(group); + if (datasetEnumerations.isPresent()) { + return datasetEnumerations.get(); + } + } + return null; // Return null if no dataset variable is found + } + + private static Optional> findDatasetInVariables(List variables) { + if (variables == null) { + return Optional.empty(); + } + + return variables.stream() + .filter(variable -> "dataset".equals(variable.getCode())) + .map(CommonDataElementDTO::getEnumerations) + .findFirst(); + } + + private static Optional> findDatasetInGroup(DataModelGroupDTO group) { + if (group == null) { + return Optional.empty(); + } + + // First check the variables in the group + Optional> datasetEnumerations = findDatasetInVariables(group.variables()); + if (datasetEnumerations.isPresent()) { + return datasetEnumerations; + } + + // Recursively search in subgroups + List subgroups = group.groups(); + if (subgroups == null) { + return Optional.empty(); + } + + for (DataModelGroupDTO subgroup : subgroups) { + datasetEnumerations = findDatasetInGroup(subgroup); + if (datasetEnumerations.isPresent()) { + return datasetEnumerations; + } + } + return Optional.empty(); + } + + private static Map> normaliseDatasetVariables(Map> datasetVariablesByDataset, + List datasets) { + if (datasetVariablesByDataset == null || datasetVariablesByDataset.isEmpty()) { + return Collections.emptyMap(); + } + + Set allowedDatasets = datasets == null + ? Collections.emptySet() + : datasets.stream().map(EnumerationDTO::code).collect(Collectors.toCollection(HashSet::new)); + + Map> normalisedVariables = new HashMap<>(); + datasetVariablesByDataset.forEach((datasetCode, variables) -> { + if (datasetCode == null) { + return; + } + if (!allowedDatasets.isEmpty() && !allowedDatasets.contains(datasetCode)) { + return; + } + List safeVariables = (variables == null || variables.isEmpty()) + ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList<>(variables)); + normalisedVariables.put(datasetCode, safeVariables); + }); + + if (normalisedVariables.isEmpty()) { + return Collections.emptyMap(); + } + + return Collections.unmodifiableMap(normalisedVariables); + } + + private static List filterDatasetsByAvailability(List datasets, List availableDatasets) { + if (datasets == null) { + return null; + } + if (availableDatasets == null || availableDatasets.isEmpty()) { + return datasets; + } + + Set allowedCodes = new HashSet<>(availableDatasets); + List filtered = datasets.stream() + .filter(dataset -> allowedCodes.contains(dataset.code())) + .toList(); + + if (filtered.isEmpty()) { + return Collections.emptyList(); + } + + return filtered; + } + + public record DataModelGroupDTO( + String code, + String label, + List variables, + List groups + ) { + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class CommonDataElementDTO { + private String code; + private String label; + private String description; + private String sql_type; + private String is_categorical; + @Setter + private List enumerations; + private String min; + private String max; + private String type; + private String methodology; + private String units; + } + + public record EnumerationDTO(String code, String label) { + } +} diff --git a/src/main/java/hbp/mip/datamodel/DataModelService.java b/src/main/java/hbp/mip/datamodel/DataModelService.java new file mode 100644 index 000000000..687e472dc --- /dev/null +++ b/src/main/java/hbp/mip/datamodel/DataModelService.java @@ -0,0 +1,167 @@ +package hbp.mip.datamodel; + +import com.google.gson.reflect.TypeToken; +import hbp.mip.utils.ClaimUtils; +import hbp.mip.utils.Exceptions.InternalServerError; +import hbp.mip.utils.HTTPUtil; +import hbp.mip.utils.JsonConverters; +import hbp.mip.utils.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +import java.lang.reflect.Type; +import java.util.*; + +@Service +public class DataModelService { + + private final ClaimUtils claimUtils; + + @Value("${authentication.enabled}") + private boolean authenticationIsEnabled; + + @Value("${services.exaflow.attributesUrl}") + private String exaflowAttributesUrl; + + @Value("${services.exaflow.datasets_variables}") + private String exaflowDatasetsVariables; + + @Value("${services.exaflow.cdesMetadataUrl}") + private String exaflowCDEsMetadataUrl; + + public DataModelService(ClaimUtils claimUtils) { + this.claimUtils = claimUtils; + } + + public List getDataModels(Authentication authentication, Logger logger) { + + List allDataModelDTOS = getAggregatedDataModelDTOs(logger); + + if (!authenticationIsEnabled) { + return allDataModelDTOS; + } + return claimUtils.getAuthorizedDataModels(logger, authentication, allDataModelDTOS); + } + + private List getAggregatedDataModelDTOs(Logger logger) { + Map exaflowDataModelAttributes; + Map>> datasetsVariablesByDataModel; + Map> datasetEnumerationsByDataModel; + Type pathologyAttributesType = new TypeToken>(){}.getType(); + Type datasetsVariablesType = new TypeToken>>>(){}.getType(); + Type cdesMetadataType = new TypeToken>>(){}.getType(); + + try { + StringBuilder response = new StringBuilder(); + HTTPUtil.sendGet(exaflowAttributesUrl, response); + exaflowDataModelAttributes = JsonConverters.convertJsonStringToObject(response.toString(), pathologyAttributesType); + } catch (Exception e) { + logger.error("Could not fetch exaflow dataModels' metadata: " + e.getMessage()); + throw new InternalServerError(e.getMessage()); + } + + try { + StringBuilder response = new StringBuilder(); + HTTPUtil.sendGet(exaflowDatasetsVariables, response); + Map>> convertedResponse = JsonConverters.convertJsonStringToObject(response.toString(), datasetsVariablesType); + datasetsVariablesByDataModel = convertedResponse != null ? convertedResponse : Collections.emptyMap(); + } catch (Exception e) { + logger.error("Could not fetch exaflow datasets variables: " + e.getMessage()); + throw new InternalServerError(e.getMessage()); + } + + try { + StringBuilder response = new StringBuilder(); + HTTPUtil.sendGet(exaflowCDEsMetadataUrl, response); + Map> convertedResponse = JsonConverters.convertJsonStringToObject(response.toString(), cdesMetadataType); + datasetEnumerationsByDataModel = convertedResponse != null ? extractDatasetEnumerations(convertedResponse) : Collections.emptyMap(); + } catch (Exception e) { + logger.error("Could not fetch exaflow datasets availability: " + e.getMessage()); + throw new InternalServerError(e.getMessage()); + } + + List dataModelDTOs = new ArrayList<>(); + exaflowDataModelAttributes.forEach((pathology, attributes) -> { + assert attributes.properties != null; + assert attributes.properties.get("cdes") != null; + assert !attributes.properties.get("cdes").isEmpty(); + DataModelDTO dataModel = attributes.properties.get("cdes").get(0); + + Map> datasetVariables = selectDatasetVariables(datasetsVariablesByDataModel, dataModel); + List datasetEnumerations = selectDatasetEnumerations(datasetEnumerationsByDataModel, dataModel); + List datasets = datasetEnumerations != null + ? datasetEnumerations + : dataModel.datasets(); + + dataModelDTOs.add(new DataModelDTO( + dataModel.code(), + dataModel.version(), + dataModel.label(), + dataModel.longitudinal(), + dataModel.variables(), + dataModel.groups(), + datasets, + datasetVariables + )); + }); + return dataModelDTOs; + } + + private Map> selectDatasetVariables(Map>> datasetVariablesByDataModel, DataModelDTO dataModel) { + if (datasetVariablesByDataModel == null || dataModel == null) { + return Collections.emptyMap(); + } + + String versionedKey = dataModel.code() + ":" + dataModel.version(); + Map> variables = datasetVariablesByDataModel.get(versionedKey); + if (variables != null) { + return variables; + } + + variables = datasetVariablesByDataModel.get(dataModel.code()); + return variables != null ? variables : Collections.emptyMap(); + } + + private List selectDatasetEnumerations(Map> datasetEnumerationsByDataModel, DataModelDTO dataModel) { + if (datasetEnumerationsByDataModel == null || dataModel == null) { + return null; + } + + String versionedKey = dataModel.code() + ":" + dataModel.version(); + List enumerations = datasetEnumerationsByDataModel.get(versionedKey); + if (enumerations != null) { + return enumerations; + } + + return datasetEnumerationsByDataModel.get(dataModel.code()); + } + + private Map> extractDatasetEnumerations(Map> cdesMetadataByDataModel) { + Map> datasetEnumerations = new HashMap<>(); + + cdesMetadataByDataModel.forEach((dataModel, cdes) -> { + if (cdes == null || !cdes.containsKey("dataset")) { + return; + } + + CDEMetadata datasetCDE = cdes.get("dataset"); + if (datasetCDE == null || datasetCDE.enumerations == null || datasetCDE.enumerations.isEmpty()) { + datasetEnumerations.put(dataModel, Collections.emptyList()); + return; + } + + List enumerations = datasetCDE.enumerations.entrySet().stream() + .map(entry -> new DataModelDTO.EnumerationDTO(entry.getKey(), entry.getValue())) + .toList(); + datasetEnumerations.put(dataModel, Collections.unmodifiableList(enumerations)); + }); + + return datasetEnumerations; + } + + record DataModelAttributes(Map> properties, List tags){} + + record CDEMetadata(String code, Map enumerations){} + +} diff --git a/src/main/java/hbp/mip/experiment/ExperimentAPI.java b/src/main/java/hbp/mip/experiment/ExperimentAPI.java index 200ca8152..faca46b36 100644 --- a/src/main/java/hbp/mip/experiment/ExperimentAPI.java +++ b/src/main/java/hbp/mip/experiment/ExperimentAPI.java @@ -1,6 +1,5 @@ package hbp.mip.experiment; - import hbp.mip.user.ActiveUserService; import hbp.mip.utils.JsonConverters; import hbp.mip.utils.Logger; @@ -11,9 +10,8 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; - @RestController -@RequestMapping(value = "/experiments", produces = {APPLICATION_JSON_VALUE}) +@RequestMapping(value = "/experiments", produces = { APPLICATION_JSON_VALUE }) public class ExperimentAPI { private final ExperimentService experimentService; @@ -21,24 +19,24 @@ public class ExperimentAPI { public ExperimentAPI( ExperimentService experimentService, - ActiveUserService activeUserService - ) { + ActiveUserService activeUserService) { this.experimentService = experimentService; this.activeUserService = activeUserService; } @GetMapping public ResponseEntity getExperiments(Authentication authentication, - @RequestParam(name = "name", required = false) String name, - @RequestParam(name = "algorithm", required = false) String algorithm, - @RequestParam(name = "shared", required = false) Boolean shared, - @RequestParam(name = "viewed", required = false) Boolean viewed, - @RequestParam(name = "includeShared", required = false, defaultValue = "true") boolean includeShared, - @RequestParam(name = "orderBy", required = false, defaultValue = "created") String orderBy, - @RequestParam(name = "descending", required = false, defaultValue = "true") Boolean descending, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size - ) { + @RequestParam(name = "name", required = false) String name, + @RequestParam(name = "algorithm", required = false) String algorithm, + @RequestParam(name = "shared", required = false) Boolean shared, + @RequestParam(name = "viewed", required = false) Boolean viewed, + @RequestParam(name = "includeShared", required = false, defaultValue = "true") boolean includeShared, + @RequestParam(name = "mine", required = false, defaultValue = "false") boolean mine, + @RequestParam(name = "notMine", required = false, defaultValue = "false") boolean notMine, + @RequestParam(name = "orderBy", required = false, defaultValue = "created") String orderBy, + @RequestParam(name = "descending", required = false, defaultValue = "true") Boolean descending, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { var logger = new Logger(activeUserService.getActiveUser(authentication).username(), "(GET) /experiments"); logger.info( "Request for experiments with parameters: " + @@ -47,59 +45,63 @@ public ResponseEntity getExperiments(Authentication authenticati " , shared -> " + shared + " , viewed -> " + viewed + " , includeShared -> " + includeShared + + " , mine -> " + mine + + " , notMine -> " + notMine + " , orderBy -> " + orderBy + " , descending -> " + descending + " , page -> " + page + - " , size -> " + size - ); + " , size -> " + size); var experimentsDTO = experimentService.getExperiments(authentication, name, algorithm, shared, viewed, includeShared, + mine, + notMine, page, size, orderBy, descending, - logger - ); + logger); logger.info("Experiments returned: " + experimentsDTO.experiments().size()); return new ResponseEntity<>(experimentsDTO, HttpStatus.OK); } - @GetMapping(value = "/{uuid}") - public ResponseEntity getExperiment(Authentication authentication, @PathVariable("uuid") String uuid) { - var logger = new Logger(activeUserService.getActiveUser(authentication).username(), "(GET) /experiments/" + uuid); + public ResponseEntity getExperiment(Authentication authentication, + @PathVariable("uuid") String uuid) { + var logger = new Logger(activeUserService.getActiveUser(authentication).username(), + "(GET) /experiments/" + uuid); logger.info("Request for experiment with id: " + uuid); var experimentResponse = experimentService.getExperiment(authentication, uuid, logger); logger.info("Experiment returned."); return new ResponseEntity<>(experimentResponse, HttpStatus.OK); } - @PostMapping - public ResponseEntity createExperiment(Authentication authentication, @RequestBody ExperimentExecutionDTO experimentExecutionDTO) { + public ResponseEntity createExperiment(Authentication authentication, + @RequestBody ExperimentExecutionDTO experimentExecutionDTO) { var logger = new Logger(activeUserService.getActiveUser(authentication).username(), "(POST) /experiments"); - logger.info("Request for experiment creation. RequestBody: " + JsonConverters.convertObjectToJsonString(experimentExecutionDTO)); + logger.info("Request for experiment creation. RequestBody: " + + JsonConverters.convertObjectToJsonString(experimentExecutionDTO)); var experimentResponse = experimentService.createExperiment(authentication, experimentExecutionDTO, logger); logger.info("Experiment created with id: " + experimentResponse.uuid()); return new ResponseEntity<>(experimentResponse, HttpStatus.CREATED); } - @PatchMapping(value = "/{uuid}") - public ResponseEntity updateExperiment(Authentication authentication, @RequestBody ExperimentDTO experimentRequest, @PathVariable("uuid") String uuid) { + public ResponseEntity updateExperiment(Authentication authentication, + @RequestBody ExperimentDTO experimentRequest, @PathVariable("uuid") String uuid) { var user = activeUserService.getActiveUser(authentication); var logger = new Logger(user.username(), "(PATCH) /experiments/" + uuid); - logger.info("Request for experiment update with id: " + uuid + ". Request Body: " + JsonConverters.convertObjectToJsonString(experimentRequest)); + logger.info("Request for experiment update with id: " + uuid + ". Request Body: " + + JsonConverters.convertObjectToJsonString(experimentRequest)); var experimentResponse = experimentService.updateExperiment(user, uuid, experimentRequest, logger); logger.info("Experiment updated. Id: " + uuid); return new ResponseEntity<>(experimentResponse, HttpStatus.OK); } - @RequestMapping(value = "/{uuid}", method = RequestMethod.DELETE) public ResponseEntity deleteExperiment(Authentication authentication, @PathVariable("uuid") String uuid) { var user = activeUserService.getActiveUser(authentication); @@ -110,23 +112,23 @@ public ResponseEntity deleteExperiment(Authentication authentication, @P return new ResponseEntity<>(HttpStatus.OK); } - @PostMapping(value = "/transient") - public ResponseEntity createTransientExperiment(Authentication authentication, @RequestBody ExperimentExecutionDTO experimentExecutionDTO) { - var logger = new Logger(activeUserService.getActiveUser(authentication).username(), "(POST) /experiments/transient"); - logger.info("Request for transient experiment creation. RequestBody: " + JsonConverters.convertObjectToJsonString(experimentExecutionDTO)); + public ResponseEntity createTransientExperiment(Authentication authentication, + @RequestBody ExperimentExecutionDTO experimentExecutionDTO) { + var logger = new Logger(activeUserService.getActiveUser(authentication).username(), + "(POST) /experiments/transient"); + logger.info("Request for transient experiment creation. RequestBody: " + + JsonConverters.convertObjectToJsonString(experimentExecutionDTO)); var experimentResponse = experimentService.runTransientExperiment( authentication, experimentExecutionDTO, - logger - ); + logger); logger.info( "Experiment (transient) finished. " + " Status: " + experimentResponse.status() + - " Result: " + experimentResponse.result() - ); + " Result: " + experimentResponse.result()); return new ResponseEntity<>(experimentResponse, HttpStatus.OK); } diff --git a/src/main/java/hbp/mip/experiment/ExperimentDAO.java b/src/main/java/hbp/mip/experiment/ExperimentDAO.java index 2e464a8be..89455a8cc 100644 --- a/src/main/java/hbp/mip/experiment/ExperimentDAO.java +++ b/src/main/java/hbp/mip/experiment/ExperimentDAO.java @@ -1,6 +1,5 @@ package hbp.mip.experiment; -import com.google.gson.Gson; import com.google.gson.annotations.Expose; import hbp.mip.user.UserDAO; import jakarta.persistence.*; @@ -12,7 +11,6 @@ import java.util.Date; import java.util.UUID; - @Entity @Data @NoArgsConstructor @@ -20,8 +18,6 @@ @Table(name = "`experiment`") public class ExperimentDAO { - private static final Gson gson = new Gson(); - @Expose @Id @Column(columnDefinition = "uuid", updatable = false) @@ -33,7 +29,7 @@ public class ExperimentDAO { @Expose @ManyToOne - @JoinColumn(name = "created_by_username",columnDefinition = "CHARACTER VARYING") + @JoinColumn(name = "created_by_username", columnDefinition = "CHARACTER VARYING") private UserDAO createdBy; @Expose @@ -54,13 +50,13 @@ public class ExperimentDAO { private String algorithm; @Expose - @Column(columnDefinition = "TEXT") + @Column(name = "algorithm_id", columnDefinition = "TEXT") private String algorithmId; @Expose @Column(columnDefinition = "TIMESTAMP WITHOUT TIME ZONE") private Date created = new Date(); - + @Expose @Column(columnDefinition = "TIMESTAMP WITHOUT TIME ZONE") private Date updated; @@ -69,6 +65,10 @@ public class ExperimentDAO { @Column(columnDefinition = "BOOLEAN") private boolean shared = false; + @Expose + @Column(name = "mip_version", columnDefinition = "TEXT") + private String mipVersion; + // Whether the experiment's result have been viewed by its owner @Expose @Column(columnDefinition = "BOOLEAN") @@ -97,6 +97,7 @@ public String toString() { ", created=" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(this.created) + ", updated=" + this.updated + ", shared=" + this.shared + + ", mipVersion=" + this.mipVersion + ", viewed=" + this.viewed + ")"; } } diff --git a/src/main/java/hbp/mip/experiment/ExperimentDTO.java b/src/main/java/hbp/mip/experiment/ExperimentDTO.java index 013e2967e..61aa32612 100644 --- a/src/main/java/hbp/mip/experiment/ExperimentDTO.java +++ b/src/main/java/hbp/mip/experiment/ExperimentDTO.java @@ -3,40 +3,40 @@ import hbp.mip.user.UserDTO; import hbp.mip.utils.JsonConverters; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import java.util.UUID; public record ExperimentDTO( - UUID uuid, - String name, - UserDTO createdBy, - Date created, - Date updated, - Date finished, - Boolean shared, - Boolean viewed, - // Result is a list of objects because there is a limitation that java has in types. - // Exareme has result in the type of List> - // And there is no generic type that describes either an object or a list of objects - List result, - ExperimentDAO.Status status, - ExperimentExecutionDTO.AlgorithmExecutionDTO algorithm -) { - public ExperimentDTO(ExperimentDAO experimentDAO, boolean includeResult) { - this( - experimentDAO.getUuid(), - experimentDAO.getName(), - new UserDTO(experimentDAO.getCreatedBy()), - experimentDAO.getCreated(), - experimentDAO.getUpdated(), - experimentDAO.getFinished(), - experimentDAO.isShared(), - experimentDAO.isViewed(), - includeResult ? JsonConverters.convertJsonStringToObject(String.valueOf(experimentDAO.getResult()), ArrayList.class) : null, - experimentDAO.getStatus(), - JsonConverters.convertJsonStringToObject(experimentDAO.getAlgorithm(), ExperimentExecutionDTO.AlgorithmExecutionDTO.class) - ); - } + UUID uuid, + String name, + UserDTO createdBy, + Date created, + Date updated, + Date finished, + Boolean shared, + Boolean viewed, + Object result, + ExperimentDAO.Status status, + ExperimentExecutionDTO.AlgorithmExecutionDTO algorithm, + String mipVersion) { + public ExperimentDTO(ExperimentDAO experimentDAO, boolean includeResult) { + this( + experimentDAO.getUuid(), + experimentDAO.getName(), + new UserDTO(experimentDAO.getCreatedBy()), + experimentDAO.getCreated(), + experimentDAO.getUpdated(), + experimentDAO.getFinished(), + experimentDAO.isShared(), + experimentDAO.isViewed(), + includeResult + ? JsonConverters.convertJsonStringToObject( + String.valueOf(experimentDAO.getResult()), + Object.class) + : null, + experimentDAO.getStatus(), + JsonConverters.convertJsonStringToObject(experimentDAO.getAlgorithm(), + ExperimentExecutionDTO.AlgorithmExecutionDTO.class), + experimentDAO.getMipVersion()); + } } diff --git a/src/main/java/hbp/mip/experiment/ExperimentExecutionDTO.java b/src/main/java/hbp/mip/experiment/ExperimentExecutionDTO.java index 340525d83..ee088eb62 100644 --- a/src/main/java/hbp/mip/experiment/ExperimentExecutionDTO.java +++ b/src/main/java/hbp/mip/experiment/ExperimentExecutionDTO.java @@ -1,21 +1,20 @@ package hbp.mip.experiment; -import java.util.List; +import hbp.mip.algorithm.AlgorithmRequestDTO; + +import java.util.Map; +import java.util.UUID; public record ExperimentExecutionDTO( - String name, - AlgorithmExecutionDTO algorithm -) { - public record AlgorithmExecutionDTO( - String name, - List parameters, - List preprocessing - ) { - public record TransformerExecutionDTO(String name, String value, - List parameters) { - } + UUID uuid, + String name, + AlgorithmExecutionDTO algorithm, + String mipVersion) { + public record AlgorithmExecutionDTO( + String name, + AlgorithmRequestDTO.InputDataRequestDTO inputdata, + Map parameters, + Map preprocessing) { - public record AlgorithmParameterExecutionDTO(String name, String value) { } - } } diff --git a/src/main/java/hbp/mip/experiment/ExperimentRepository.java b/src/main/java/hbp/mip/experiment/ExperimentRepository.java index ec2fa54e4..cf5181942 100644 --- a/src/main/java/hbp/mip/experiment/ExperimentRepository.java +++ b/src/main/java/hbp/mip/experiment/ExperimentRepository.java @@ -14,7 +14,8 @@ import java.util.UUID; @RestResource(exported = false) -public interface ExperimentRepository extends CrudRepository, JpaSpecificationExecutor { +public interface ExperimentRepository + extends CrudRepository, JpaSpecificationExecutor { ExperimentDAO findByUuid(UUID experimentUuid); default ExperimentDAO loadExperiment(String uuid, Logger logger) { @@ -23,7 +24,7 @@ default ExperimentDAO loadExperiment(String uuid, Logger logger) { try { experimentUuid = UUID.fromString(uuid); } catch (IllegalArgumentException e) { - logger.error("Conversion of string to UUID failed:" + e.getMessage()); + logger.error("Conversion of string to UUID failed:" + e.getMessage()); throw new BadRequestException(e.getMessage()); } @@ -37,13 +38,17 @@ default ExperimentDAO loadExperiment(String uuid, Logger logger) { return experimentDAO; } - default ExperimentDAO createExperimentInTheDatabase(ExperimentExecutionDTO experimentExecutionDTO, UserDTO user, Logger logger) { + default ExperimentDAO createExperimentInTheDatabase(ExperimentExecutionDTO experimentExecutionDTO, UserDTO user, + Logger logger) { ExperimentDAO experimentDAO = new ExperimentDAO(); - experimentDAO.setUuid(UUID.randomUUID()); + UUID experimentUuid = UUID.randomUUID(); + + experimentDAO.setUuid(experimentUuid); experimentDAO.setCreatedBy(new UserDAO(user)); experimentDAO.setAlgorithm(JsonConverters.convertObjectToJsonString(experimentExecutionDTO.algorithm())); experimentDAO.setAlgorithmId(experimentExecutionDTO.algorithm().name()); experimentDAO.setName(experimentExecutionDTO.name()); + experimentDAO.setMipVersion(experimentExecutionDTO.mipVersion()); experimentDAO.setStatus(ExperimentDAO.Status.pending); try { diff --git a/src/main/java/hbp/mip/experiment/ExperimentService.java b/src/main/java/hbp/mip/experiment/ExperimentService.java index ea0d8a2aa..6335b3017 100644 --- a/src/main/java/hbp/mip/experiment/ExperimentService.java +++ b/src/main/java/hbp/mip/experiment/ExperimentService.java @@ -1,19 +1,12 @@ package hbp.mip.experiment; -import hbp.mip.algorithm.Exareme2AlgorithmsSpecs; -import hbp.mip.algorithm.Exareme2AlgorithmRequestDTO; -import hbp.mip.algorithm.Exareme2AlgorithmSpecificationDTO; +import hbp.mip.algorithm.AlgorithmRequestDTO; import hbp.mip.user.ActiveUserService; import hbp.mip.user.UserDTO; import hbp.mip.utils.*; -import hbp.mip.utils.Exceptions.BadRequestException; -import hbp.mip.utils.Exceptions.InternalServerError; -import hbp.mip.utils.Exceptions.NoContent; -import hbp.mip.utils.Exceptions.UnauthorizedException; +import hbp.mip.utils.Exceptions.*; import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.*; import org.springframework.data.jpa.domain.Specification; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; @@ -23,235 +16,248 @@ import static hbp.mip.utils.JsonConverters.convertObjectToJsonString; - @Service public class ExperimentService { - private final ActiveUserService activeUserService; + private static final int MAX_PAGE_SIZE = 50; + private static final String GENERIC_ERROR_MESSAGE = "Something went wrong. Please inform the system administrator or try again later."; + private final ActiveUserService activeUserService; private final ClaimUtils claimUtils; - private final Exareme2AlgorithmsSpecs exareme2AlgorithmsSpecs; - private final ExperimentRepository experimentRepository; - @Value("${services.exareme2.algorithmsUrl}") - private String exareme2AlgorithmsUrl; + @Value("${services.exaflow.algorithmsUrl}") + private String exaflowAlgorithmsUrl; @Value("${authentication.enabled}") private boolean authenticationIsEnabled; - public ExperimentService( - ActiveUserService activeUserService, - ClaimUtils claimUtils, - Exareme2AlgorithmsSpecs exareme2AlgorithmsSpecs, - ExperimentRepository experimentRepository - ) { + public ExperimentService(ActiveUserService activeUserService, ClaimUtils claimUtils, + ExperimentRepository experimentRepository) { this.activeUserService = activeUserService; this.claimUtils = claimUtils; - this.exareme2AlgorithmsSpecs = exareme2AlgorithmsSpecs; this.experimentRepository = experimentRepository; } - private static List convertExareme2ResponseToAlgorithmResults(Logger logger, int requestResponseCode, StringBuilder requestResponseBody) { - Object result; - if (requestResponseCode == 200) { - result = JsonConverters.convertJsonStringToObject(String.valueOf(requestResponseBody), Object.class); - } else if (requestResponseCode == 400) { - result = convertExareme2ResponseToAlgorithmResult(String.valueOf(requestResponseBody), "text/plain+error"); - } else if (requestResponseCode == 460) { - result = convertExareme2ResponseToAlgorithmResult(String.valueOf(requestResponseBody), "text/plain+user_error"); - } else if (requestResponseCode == 461) { - result = convertExareme2ResponseToAlgorithmResult(String.valueOf(requestResponseBody), "text/plain+error"); - } else if (requestResponseCode == 500) { - result = convertExareme2ResponseToAlgorithmResult("Something went wrong. Please inform the system administrator or try again later.", "text/plain+error"); - } else if (requestResponseCode == 512) { - result = convertExareme2ResponseToAlgorithmResult(String.valueOf(requestResponseBody), "text/plain+error"); - } else { - var errorMessage = "Exareme2 execution responded with an unexpected status code: " + requestResponseCode; - logger.error(errorMessage); - throw new InternalServerError(errorMessage); - } - return List.of(result); + private static Object convertResponseToAlgorithmResults(Logger logger, int code, StringBuilder responseBody) { + return switch (code) { + case 200 -> JsonConverters.convertJsonStringToObject(responseBody.toString(), Object.class); + case 400, 460, 461, 512 -> convertExaflowResponseToAlgorithmResult(responseBody.toString()); + case 500 -> convertExaflowResponseToAlgorithmResult(GENERIC_ERROR_MESSAGE); + default -> handleUnexpectedResponseCode(logger, code); + }; } - private static Map convertExareme2ResponseToAlgorithmResult(String resultBody, String resultType) { - Map exaremeAlgorithmResultElement = new HashMap<>(); - exaremeAlgorithmResultElement.put("data", resultBody); - exaremeAlgorithmResultElement.put("type", resultType); - return exaremeAlgorithmResultElement; + private static Map convertExaflowResponseToAlgorithmResult(String resultBody) { + return Map.of("data", resultBody, "type", "text/plain+error"); + } + + private static Object handleUnexpectedResponseCode(Logger logger, int code) { + String errorMessage = "Exaflow execution responded with an unexpected status code: " + code; + logger.error(errorMessage); + throw new InternalServerError(errorMessage); } /** - * The getExperiments will retrieve the experiments from database according to the filters. + * The getExperiments will retrieve the experiments from database according to + * the filters. * - * @param name is optional, in case it is required to filter the experiments by name - * @param algorithm is optional, in case it is required to filter the experiments by algorithm name - * @param shared is optional, in case it is required to filter the experiments by shared - * @param viewed is optional, in case it is required to filter the experiments by viewed - * @param includeShared is optional, in case it is required to retrieve the experiment that is shared + * @param name is optional, in case it is required to filter the + * experiments by name + * @param algorithm is optional, in case it is required to filter the + * experiments by algorithm name + * @param shared is optional, in case it is required to filter the + * experiments by shared + * @param viewed is optional, in case it is required to filter the + * experiments by viewed + * @param includeShared is optional, in case it is required to retrieve the + * experiment that is shared * @param page is the page that is required to be retrieved * @param size is the size of each page * @param orderBy is the column that is required to ordered by - * @param descending is a boolean to determine if the experiments will be ordered by descending or ascending order + * @param descending is a boolean to determine if the experiments will be + * ordered by descending or ascending order * @param logger contains username and the endpoint. * @return a map experiments */ - public ExperimentsDTO getExperiments(Authentication authentication, String name, String algorithm, Boolean shared, Boolean viewed, boolean includeShared, int page, int size, String orderBy, Boolean descending, Logger logger) { + public ExperimentsDTO getExperiments(Authentication authentication, String name, String algorithm, Boolean shared, + Boolean viewed, boolean includeShared, boolean mine, boolean notMine, int page, int size, String orderBy, + Boolean descending, + Logger logger) { + validatePageSize(size); + + Specification spec = buildExperimentSpecification(authentication, name, algorithm, shared, + viewed, includeShared, mine, notMine, orderBy, descending, logger); + Pageable pageable = PageRequest.of(page, size); + Page experimentsPage = experimentRepository.findAll(spec, pageable); + + if (experimentsPage.isEmpty()) { + throw new NoContent("No experiment found with the filters provided."); + } + + return createExperimentsDTO(experimentsPage); + } + + private void validatePageSize(int size) { + if (size > MAX_PAGE_SIZE) { + throw new BadRequestException("Invalid size input, max size is " + MAX_PAGE_SIZE); + } + } + + private Specification buildExperimentSpecification(Authentication authentication, String name, + String algorithm, Boolean shared, Boolean viewed, boolean includeShared, boolean mine, boolean notMine, + String orderBy, + Boolean descending, Logger logger) { var user = activeUserService.getActiveUser(authentication); - if (size > 50) - throw new BadRequestException("Invalid size input, max size is 50."); + boolean hasAccessRights = !authenticationIsEnabled + || claimUtils.validateAccessRightsOnALLExperiments(authentication, logger); Specification spec; - if (!authenticationIsEnabled || claimUtils.validateAccessRightsOnALLExperiments(authentication, logger)) { - spec = Specification - .where(new ExperimentSpecifications.ExperimentWithName(name)) - .and(new ExperimentSpecifications.ExperimentWithAlgorithm(algorithm)) - .and(new ExperimentSpecifications.ExperimentWithShared(shared)) - .and(new ExperimentSpecifications.ExperimentWithViewed(viewed)) - .and(new ExperimentSpecifications.ExperimentOrderBy(orderBy, descending)); + + if (mine) { + spec = Specification.where(new ExperimentSpecifications.MyExperiment(user.username())); + } else if (notMine) { + spec = Specification.where(new ExperimentSpecifications.NotMyExperiment(user.username())) + .and(new ExperimentSpecifications.SharedExperiment(true)); + } else if (hasAccessRights) { + spec = Specification.where(null); } else { - spec = Specification - .where(new ExperimentSpecifications.MyExperiment(user.username())) - .or(new ExperimentSpecifications.SharedExperiment(includeShared)) - .and(new ExperimentSpecifications.ExperimentWithAlgorithm(algorithm)) - .and(new ExperimentSpecifications.ExperimentWithShared(shared)) - .and(new ExperimentSpecifications.ExperimentWithViewed(viewed)) - .and(new ExperimentSpecifications.ExperimentWithName(name)) - .and(new ExperimentSpecifications.ExperimentOrderBy(orderBy, descending)); + spec = Specification.where(new ExperimentSpecifications.MyExperiment(user.username())) + .or(new ExperimentSpecifications.SharedExperiment(includeShared)); } - Pageable paging = PageRequest.of(page, size); - Page pageExperiments = experimentRepository.findAll(spec, paging); - List experimentDAOs = pageExperiments.getContent(); - if (experimentDAOs.isEmpty()) - throw new NoContent("No experiment found with the filters provided."); - - List experiments = new ArrayList<>(); - experimentDAOs.forEach(experimentDAO -> experiments.add(new ExperimentDTO(experimentDAO, false))); + return spec + .and(new ExperimentSpecifications.ExperimentWithName(name)) + .and(new ExperimentSpecifications.ExperimentWithAlgorithm(algorithm)) + .and(new ExperimentSpecifications.ExperimentWithShared(shared)) + .and(new ExperimentSpecifications.ExperimentWithViewed(viewed)) + .and(new ExperimentSpecifications.ExperimentOrderBy(orderBy, descending)); + } - return new ExperimentsDTO( - experiments, - pageExperiments.getNumber(), - pageExperiments.getTotalPages(), - pageExperiments.getTotalElements() - ); + private ExperimentsDTO createExperimentsDTO(Page pageExperiments) { + List experiments = pageExperiments.map(experimentDAO -> new ExperimentDTO(experimentDAO, false)) + .getContent(); + return new ExperimentsDTO(experiments, pageExperiments.getNumber(), pageExperiments.getTotalPages(), + pageExperiments.getTotalElements()); } public ExperimentDTO getExperiment(Authentication authentication, String uuid, Logger logger) { + ExperimentDAO experimentDAO = experimentRepository.loadExperiment(uuid, logger); + validateExperimentAccess(authentication, experimentDAO, uuid, logger); + + return new ExperimentDTO(experimentDAO, true); + } + + private void validateExperimentAccess(Authentication authentication, ExperimentDAO experimentDAO, String uuid, + Logger logger) { var user = activeUserService.getActiveUser(authentication); + boolean unauthorizedAccess = authenticationIsEnabled && !experimentDAO.isShared() + && !experimentDAO.getCreatedBy().getUsername().equals(user.username()) + && !claimUtils.validateAccessRightsOnALLExperiments(authentication, logger); - var experimentDAO = experimentRepository.loadExperiment(uuid, logger); - if ( - authenticationIsEnabled - && !experimentDAO.isShared() - && !experimentDAO.getCreatedBy().getUsername().equals(user.username()) - && !claimUtils.validateAccessRightsOnALLExperiments(authentication, logger) - ) { + if (unauthorizedAccess) { logger.warn("User tried to access an unauthorized experiment with id:" + uuid); throw new UnauthorizedException("You don't have access to that experiment."); } - - return new ExperimentDTO(experimentDAO, true); } - public ExperimentDTO createExperiment(Authentication authentication, ExperimentExecutionDTO experimentExecutionDTO, Logger logger) { + public ExperimentDTO createExperiment(Authentication authentication, ExperimentExecutionDTO experimentExecutionDTO, + Logger logger) { algorithmParametersLogging(experimentExecutionDTO, logger); + validateDatasetAccess(authentication, experimentExecutionDTO, logger); + + ExperimentDAO experimentDAO = experimentRepository.createExperimentInTheDatabase(experimentExecutionDTO, + activeUserService.getActiveUser(authentication), logger); + runAlgorithmInBackground(experimentDAO, experimentExecutionDTO, logger); + + return new ExperimentDTO(experimentDAO, false); + } + + private void validateDatasetAccess(Authentication authentication, ExperimentExecutionDTO experimentExecutionDTO, + Logger logger) { if (authenticationIsEnabled) { - String experimentDatasets = getExperimentDatasets(experimentExecutionDTO, logger); - claimUtils.validateAccessRightsOnDatasets(authentication, experimentDatasets, logger); + claimUtils.validateAccessRightsOnDatasets(authentication, + experimentExecutionDTO.algorithm().inputdata().datasets(), logger); } + } - var experimentDAO = experimentRepository.createExperimentInTheDatabase(experimentExecutionDTO, activeUserService.getActiveUser(authentication), logger); - + private void runAlgorithmInBackground(ExperimentDAO experimentDAO, ExperimentExecutionDTO experimentExecutionDTO, + Logger logger) { new Thread(() -> { - logger.debug("Experiment's algorithm execution started in a background thread."); try { - ExperimentAlgorithmResultDTO exaremeExperimentAlgorithmResultDTO = runExaremeAlgorithm(experimentDAO.getUuid(), experimentExecutionDTO, logger); - experimentDAO.setResult(convertObjectToJsonString(exaremeExperimentAlgorithmResultDTO.result())); - experimentDAO.setStatus((exaremeExperimentAlgorithmResultDTO.code() >= 400) - ? ExperimentDAO.Status.error : ExperimentDAO.Status.success); + logger.debug("Experiment's algorithm execution started in a background thread."); + ExperimentAlgorithmResultDTO resultDTO = runExaflowAlgorithm(experimentDAO.getUuid(), + experimentExecutionDTO, logger); + experimentDAO.setResult(convertObjectToJsonString(resultDTO.result())); + experimentDAO + .setStatus(resultDTO.code() >= 400 ? ExperimentDAO.Status.error : ExperimentDAO.Status.success); } catch (Exception e) { - logger.error("Exareme2 algorithm execution failed: " + e.getMessage() + " Stacktrace: " + Arrays.toString(e.getStackTrace())); + logger.error("Exaflow algorithm execution failed: " + e.getMessage()); experimentDAO.setStatus(ExperimentDAO.Status.error); } - experimentRepository.finishExperiment(experimentDAO, logger); - - // Experiment finished log is needed for the federation info (mip-deployment) script. logger.info("Experiment finished: " + experimentDAO); }).start(); - - return new ExperimentDTO(experimentDAO, false); } - public ExperimentDTO runTransientExperiment(Authentication authentication, ExperimentExecutionDTO experimentExecutionDTO, Logger logger) { + public ExperimentDTO runTransientExperiment(Authentication authentication, + ExperimentExecutionDTO experimentExecutionDTO, Logger logger) { algorithmParametersLogging(experimentExecutionDTO, logger); - if (authenticationIsEnabled) { - String experimentDatasets = getExperimentDatasets(experimentExecutionDTO, logger); - claimUtils.validateAccessRightsOnDatasets(authentication, experimentDatasets, logger); - } + validateDatasetAccess(authentication, experimentExecutionDTO, logger); + UUID uuid = UUID.randomUUID(); + ExperimentAlgorithmResultDTO algorithmResult = runExaflowAlgorithm(uuid, experimentExecutionDTO, logger); - var uuid = UUID.randomUUID(); - var algorithmResult = runExaremeAlgorithm(uuid, experimentExecutionDTO, logger); - - return new ExperimentDTO( - uuid, - experimentExecutionDTO.name(), - null, - null, - null, - null, - null, - null, + return new ExperimentDTO(uuid, experimentExecutionDTO.name(), null, null, null, null, null, null, algorithmResult.result(), algorithmResult.code() >= 400 ? ExperimentDAO.Status.error : ExperimentDAO.Status.success, - experimentExecutionDTO.algorithm() - ); + experimentExecutionDTO.algorithm(), + experimentExecutionDTO.mipVersion()); } public ExperimentDTO updateExperiment(UserDTO user, String uuid, ExperimentDTO experiment, Logger logger) { - var experimentDAO = experimentRepository.loadExperiment(uuid, logger); + ExperimentDAO experimentDAO = experimentRepository.loadExperiment(uuid, logger); verifyNonEditableFieldsAreNotBeingModified(experiment, logger); + checkUpdateAuthorization(user, experimentDAO, uuid, logger); - if (!experimentDAO.getCreatedBy().getUsername().equals(user.username())) { - logger.warn("User tried to modify a unauthorized experiment with uuid: " + uuid); - throw new UnauthorizedException("You don't have access to modify the experiment."); - } - - // Change modifiable fields if provided - if (experiment.name() != null && !experiment.name().isEmpty()) - experimentDAO.setName(experiment.name()); - - if (experiment.shared() != null) - experimentDAO.setShared(experiment.shared()); - - if (experiment.viewed() != null) - experimentDAO.setViewed(experiment.viewed()); - + updateModifiableFields(experiment, experimentDAO); experimentDAO.setUpdated(new Date()); try { experimentRepository.save(experimentDAO); } catch (Exception e) { logger.error("Failed to save to the database: " + e.getMessage()); - throw e; + throw new InternalServerError(e.getMessage()); } return new ExperimentDTO(experimentDAO, true); } - // /* ------------------------------- PRIVATE METHODS ----------------------------------------------------*/ + private void checkUpdateAuthorization(UserDTO user, ExperimentDAO experimentDAO, String uuid, Logger logger) { + if (!experimentDAO.getCreatedBy().getUsername().equals(user.username())) { + logger.warn("User tried to modify an unauthorized experiment with uuid: " + uuid); + throw new UnauthorizedException("You don't have access to modify the experiment."); + } + } + + private void updateModifiableFields(ExperimentDTO experiment, ExperimentDAO experimentDAO) { + if (experiment.name() != null && !experiment.name().isEmpty()) { + experimentDAO.setName(experiment.name()); + } + if (experiment.shared() != null) { + experimentDAO.setShared(experiment.shared()); + } + if (experiment.viewed() != null) { + experimentDAO.setViewed(experiment.viewed()); + } + } public void deleteExperiment(UserDTO user, String uuid, Logger logger) { ExperimentDAO experimentDAO = experimentRepository.loadExperiment(uuid, logger); - if (!experimentDAO.getCreatedBy().getUsername().equals(user.username())) { - logger.warn("User " + user.username() + " tried to delete the experiment with uuid " + uuid + " but he was unauthorized."); - throw new UnauthorizedException("You don't have access to delete the experiment."); - } + checkDeleteAuthorization(user, experimentDAO, uuid, logger); try { experimentRepository.delete(experimentDAO); @@ -261,110 +267,95 @@ public void deleteExperiment(UserDTO user, String uuid, Logger logger) { } } + private void checkDeleteAuthorization(UserDTO user, ExperimentDAO experimentDAO, String uuid, Logger logger) { + if (!experimentDAO.getCreatedBy().getUsername().equals(user.username())) { + logger.warn("User " + user.username() + " tried to delete the experiment with uuid " + uuid + + " but was unauthorized."); + throw new UnauthorizedException("You don't have access to delete the experiment."); + } + } + private void verifyNonEditableFieldsAreNotBeingModified(ExperimentDTO experimentDTO, Logger logger) { - throwNonEditableExceptionIfNotNull(experimentDTO.uuid(), "uuid", logger); - throwNonEditableExceptionIfNotNull(experimentDTO.algorithm(), "algorithm", logger); - throwNonEditableExceptionIfNotNull(experimentDTO.created(), "created", logger); - throwNonEditableExceptionIfNotNull(experimentDTO.updated(), "updated", logger); - throwNonEditableExceptionIfNotNull(experimentDTO.finished(), "finished", logger); - throwNonEditableExceptionIfNotNull(experimentDTO.createdBy(), "createdBy", logger); - throwNonEditableExceptionIfNotNull(experimentDTO.result(), "result", logger); - throwNonEditableExceptionIfNotNull(experimentDTO.status(), "status", logger); + List.of("uuid", "algorithm", "created", "updated", "finished", "createdBy", "result", "status") + .forEach(field -> throwNonEditableExceptionIfNotNull(getFieldValue(experimentDTO, field), field, + logger)); } private void throwNonEditableExceptionIfNotNull(Object field, String nonEditableField, Logger logger) { if (field != null) { - var errorMessage = "Tried to edit non editable field: " + nonEditableField; + String errorMessage = "Tried to edit non-editable field: " + nonEditableField; logger.warn(errorMessage); throw new BadRequestException(errorMessage); } } + private Object getFieldValue(ExperimentDTO experimentDTO, String fieldName) { + try { + var field = ExperimentDTO.class.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(experimentDTO); + } catch (NoSuchFieldException | IllegalAccessException e) { + return null; + } + } + private void algorithmParametersLogging(ExperimentExecutionDTO experimentExecutionDTO, Logger logger) { ExperimentExecutionDTO.AlgorithmExecutionDTO algorithm = experimentExecutionDTO.algorithm(); + StringBuilder parametersLogMessage = new StringBuilder(); - String algorithmName = algorithm.name(); + Optional.ofNullable(algorithm.parameters()).ifPresent(parameters -> parameters.forEach((paramName, + paramValue) -> parametersLogMessage.append(" ").append(paramName).append(" -> ").append(paramValue))); - StringBuilder parametersLogMessage = new StringBuilder(); - if (algorithm.parameters() != null) { - algorithm.parameters().forEach( - params -> parametersLogMessage - .append(" ") - .append(params.name()) - .append(" -> ") - .append(params.value()) - ); - } + Optional.ofNullable(algorithm.preprocessing()).ifPresent(preprocessing -> preprocessing + .forEach((name, value) -> parametersLogMessage.append(" ").append(name).append(" -> ").append(value))); - if (algorithm.preprocessing() != null) { - algorithm.preprocessing().forEach(transformer -> - { - parametersLogMessage - .append(" ") - .append(transformer.name()) - .append(" -> "); - - if (transformer.parameters() != null) { - transformer.parameters().forEach( - transformerParams -> parametersLogMessage - .append(" ") - .append(transformerParams.name()) - .append(" -> ") - .append(transformerParams.value())); - } - }); - } + if (algorithm.inputdata() != null) { + AlgorithmRequestDTO.InputDataRequestDTO inputData = algorithm.inputdata(); + parametersLogMessage.append(" Input Data Model: ").append(inputData.data_model()); - logger.debug("Algorithm " + algorithmName + " execution starting with parameters: " + parametersLogMessage); - } + Optional.ofNullable(inputData.datasets()) + .ifPresent(datasets -> parametersLogMessage.append(" Datasets: ").append(datasets)); + + Optional.ofNullable(inputData.x()) + .ifPresent(xVars -> parametersLogMessage.append(" X Variables: ").append(xVars)); - private String getExperimentDatasets(ExperimentExecutionDTO experimentExecutionDTO, Logger logger) { - String experimentDatasets = null; - for (ExperimentExecutionDTO.AlgorithmExecutionDTO.AlgorithmParameterExecutionDTO parameter : experimentExecutionDTO.algorithm().parameters()) { - if (parameter.name().equals("dataset")) { - experimentDatasets = parameter.value(); - break; + Optional.ofNullable(inputData.y()) + .ifPresent(yVars -> parametersLogMessage.append(" Y Variables: ").append(yVars)); + + if (inputData.filters() != null) { + AlgorithmRequestDTO.FilterRequestDTO filters = inputData.filters(); + parametersLogMessage.append(" Filter Condition: ").append(filters.condition()); + + Optional.ofNullable(filters.rules()) + .ifPresent(rules -> parametersLogMessage.append(" Filter Rules: ").append(rules)); } } - if (experimentDatasets == null || experimentDatasets.isEmpty()) { - logger.debug("No datasets provided in experiment execution request."); - throw new BadRequestException("Please provide at least one dataset."); - } - return experimentDatasets; + logger.debug("Algorithm " + algorithm.name() + " execution starting with parameters: " + parametersLogMessage); } - private ExperimentAlgorithmResultDTO runExaremeAlgorithm(UUID uuid, ExperimentExecutionDTO experimentExecutionDTO, Logger logger) { - String algorithmName = experimentExecutionDTO.algorithm().name(); - String algorithmEndpoint = exareme2AlgorithmsUrl + "/" + algorithmName; - Exareme2AlgorithmSpecificationDTO exareme2AlgorithmSpecificationDTO = getAlgorithmSpec(algorithmName); - var exareme2AlgorithmRequestDTO = Exareme2AlgorithmRequestDTO.create(uuid, experimentExecutionDTO.algorithm().parameters(), experimentExecutionDTO.algorithm().preprocessing(), exareme2AlgorithmSpecificationDTO); - String algorithmBody = convertObjectToJsonString(exareme2AlgorithmRequestDTO); - logger.debug("Exareme2 algorithm request, endpoint: " + algorithmEndpoint); - logger.debug("Exareme2 algorithm request, body: " + algorithmBody); - - int requestResponseCode; - var requestResponseBody = new StringBuilder(); + private ExperimentAlgorithmResultDTO runExaflowAlgorithm(UUID uuid, ExperimentExecutionDTO experimentExecutionDTO, + Logger logger) { + String algorithmEndpoint = exaflowAlgorithmsUrl + "/" + experimentExecutionDTO.algorithm().name(); + var requestBody = convertObjectToJsonString( + AlgorithmRequestDTO.create(uuid, experimentExecutionDTO.algorithm())); + + logger.debug("Exaflow algorithm request, endpoint: " + algorithmEndpoint); + logger.debug("Exaflow algorithm request, body: " + requestBody); + + int responseCode; + var responseBody = new StringBuilder(); try { - requestResponseCode = HTTPUtil.sendPost(algorithmEndpoint, algorithmBody, requestResponseBody); + responseCode = HTTPUtil.sendPost(algorithmEndpoint, requestBody, responseBody); } catch (IOException e) { - var errorMessage = "Could not run the exareme2 algorithm: " + e.getMessage(); - logger.error(errorMessage); - throw new InternalServerError(errorMessage); + logger.error("Could not run the exaflow algorithm: " + e.getMessage()); + throw new InternalServerError(e.getMessage()); } - var result = convertExareme2ResponseToAlgorithmResults(logger, requestResponseCode, requestResponseBody); - return new ExperimentAlgorithmResultDTO(requestResponseCode, result); - } - - private Exareme2AlgorithmSpecificationDTO getAlgorithmSpec(String algorithmName){ - Optional algorithmSpecification = exareme2AlgorithmsSpecs.getAlgorithms().stream() - .filter(algorithmSpec-> algorithmSpec.name().equals(algorithmName)) - .findFirst(); - if (algorithmSpecification.isEmpty()) throw new InternalServerError("Missing the algorithm: " + algorithmName); - return algorithmSpecification.get(); + Object result = convertResponseToAlgorithmResults(logger, responseCode, responseBody); + return new ExperimentAlgorithmResultDTO(responseCode, result); } - record ExperimentAlgorithmResultDTO(int code, List result) { + record ExperimentAlgorithmResultDTO(int code, Object result) { } } diff --git a/src/main/java/hbp/mip/experiment/ExperimentSpecifications.java b/src/main/java/hbp/mip/experiment/ExperimentSpecifications.java index 60ef2dde8..fecc00a9e 100644 --- a/src/main/java/hbp/mip/experiment/ExperimentSpecifications.java +++ b/src/main/java/hbp/mip/experiment/ExperimentSpecifications.java @@ -20,7 +20,8 @@ public ExperimentWithName(String name) { this.regExp = name; } - public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, @NonNull CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, + @NonNull CriteriaBuilder cb) { if (name == null) { return cb.isTrue(cb.literal(true)); } else { @@ -49,7 +50,8 @@ public ExperimentWithAlgorithm(String algorithm) { this.algorithm = algorithm; } - public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, @NonNull CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, + @NonNull CriteriaBuilder cb) { if (algorithm == null) { return cb.isTrue(cb.literal(true)); } @@ -66,7 +68,8 @@ public ExperimentWithViewed(Boolean viewed) { this.viewed = viewed; } - public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery query, @NonNull CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery query, + @NonNull CriteriaBuilder cb) { if (viewed == null) { return cb.isTrue(cb.literal(true)); // always true = no filtering } @@ -82,7 +85,8 @@ public ExperimentWithShared(Boolean shared) { this.shared = shared; } - public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, @NonNull CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, + @NonNull CriteriaBuilder cb) { if (shared == null) { return cb.isTrue(cb.literal(true)); } @@ -98,7 +102,8 @@ public MyExperiment(String username) { this.username = username; } - public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, @NonNull CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, + @NonNull CriteriaBuilder cb) { if (username == null) { return cb.isTrue(cb.literal(true)); } @@ -107,6 +112,24 @@ public Predicate toPredicate(@NonNull Root root, @NonNull Criteri } } + public static class NotMyExperiment implements Specification { + + private final String username; + + public NotMyExperiment(String username) { + this.username = username; + } + + public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, + @NonNull CriteriaBuilder cb) { + if (username == null) { + return cb.isTrue(cb.literal(true)); + } + Join experimentDAOUserDAOJoin = root.join("createdBy"); + return cb.notEqual(experimentDAOUserDAOJoin.get("username"), username); + } + } + public static class SharedExperiment implements Specification { private final boolean shared; @@ -115,7 +138,8 @@ public SharedExperiment(boolean shared) { this.shared = shared; } - public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, @NonNull CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, + @NonNull CriteriaBuilder cb) { if (!shared) { return cb.isTrue(cb.literal(false)); } @@ -136,7 +160,8 @@ public ExperimentOrderBy(String orderBy, Boolean descending) { this.descending = Objects.requireNonNullElse(descending, true); } - public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, @NonNull CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root root, @NonNull CriteriaQuery criteriaQuery, + @NonNull CriteriaBuilder cb) { if (descending) { criteriaQuery.orderBy(cb.desc(root.get(orderBy))); } else { @@ -162,4 +187,3 @@ public static boolean properColumnToBeOrderedBy(String column) { } } } - diff --git a/src/main/java/hbp/mip/pathology/Exareme2PathologyCommonDataElementDTO.java b/src/main/java/hbp/mip/pathology/Exareme2PathologyCommonDataElementDTO.java deleted file mode 100644 index df5c853f2..000000000 --- a/src/main/java/hbp/mip/pathology/Exareme2PathologyCommonDataElementDTO.java +++ /dev/null @@ -1,17 +0,0 @@ -package hbp.mip.pathology; - - -public record Exareme2PathologyCommonDataElementDTO( - String code, - String label, - String description, - String sql_type, - Boolean is_categorical, - Object enumerations, - String min, - String max, - String units, - String type, - String methodology -) { -} diff --git a/src/main/java/hbp/mip/pathology/PathologiesAPI.java b/src/main/java/hbp/mip/pathology/PathologiesAPI.java deleted file mode 100644 index 5282f3100..000000000 --- a/src/main/java/hbp/mip/pathology/PathologiesAPI.java +++ /dev/null @@ -1,40 +0,0 @@ -package hbp.mip.pathology; - -import hbp.mip.user.ActiveUserService; -import hbp.mip.utils.Logger; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -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; - -@RestController -@RequestMapping(value = "/pathologies") -public class PathologiesAPI { - - private final PathologyService pathologyService; - - private final ActiveUserService activeUserService; - - public PathologiesAPI(ActiveUserService activeUserService, PathologyService pathologyService) { - this.activeUserService = activeUserService; - this.pathologyService = pathologyService; - } - - @GetMapping - public ResponseEntity> getPathologies(Authentication authentication) { - var logger = new Logger(activeUserService.getActiveUser(authentication).username(), "(GET) /pathologies"); - logger.info("Request for pathologies."); - var pathologies = pathologyService.getPathologies(authentication, logger); - - String userPathologiesSTR = pathologies.stream().map(PathologyDTO::code) - .collect(Collectors.joining(", ")); - logger.info("Pathologies returned: " + pathologies.size() + ". [" + userPathologiesSTR + "]."); - - return ResponseEntity.ok(pathologies); - } - -} diff --git a/src/main/java/hbp/mip/pathology/PathologyDTO.java b/src/main/java/hbp/mip/pathology/PathologyDTO.java deleted file mode 100644 index fac1d0cbf..000000000 --- a/src/main/java/hbp/mip/pathology/PathologyDTO.java +++ /dev/null @@ -1,18 +0,0 @@ -package hbp.mip.pathology; - -import java.util.List; -import java.util.Map; - - -public record PathologyDTO( - String code, - String version, - String label, - Boolean longitudinal, - PathologyMetadataDTO metadataHierarchy, - List datasets, - Map> datasetsVariables -) { - public record EnumerationDTO(String code, String label) { - } -} diff --git a/src/main/java/hbp/mip/pathology/PathologyMetadataDTO.java b/src/main/java/hbp/mip/pathology/PathologyMetadataDTO.java deleted file mode 100644 index 77ff07fc7..000000000 --- a/src/main/java/hbp/mip/pathology/PathologyMetadataDTO.java +++ /dev/null @@ -1,45 +0,0 @@ -package hbp.mip.pathology; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -public record PathologyMetadataDTO( - String code, - String version, - String label, - Boolean longitudinal, - List variables, - List groups -) { - public record PathologyMetadataGroupDTO( - String code, - String label, - List variables, - List groups - ) { - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class CommonDataElementDTO { - private String code; - private String label; - private String description; - private String sql_type; - private String is_categorical; - private List enumerations; - private String min; - private String max; - private String type; - private String methodology; - private String units; - - public void setEnumerations(List enumerations) { - this.enumerations = enumerations; - } - } -} \ No newline at end of file diff --git a/src/main/java/hbp/mip/pathology/PathologyService.java b/src/main/java/hbp/mip/pathology/PathologyService.java deleted file mode 100644 index 3411dd080..000000000 --- a/src/main/java/hbp/mip/pathology/PathologyService.java +++ /dev/null @@ -1,221 +0,0 @@ -package hbp.mip.pathology; - -import com.google.gson.reflect.TypeToken; -import hbp.mip.utils.ClaimUtils; -import hbp.mip.utils.Exceptions.InternalServerError; -import hbp.mip.utils.HTTPUtil; -import hbp.mip.utils.JsonConverters; -import hbp.mip.utils.Logger; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Service; - -import java.lang.reflect.Type; -import java.util.*; - -@Service -public class PathologyService { - - private final ClaimUtils claimUtils; - - @Value("${authentication.enabled}") - private boolean authenticationIsEnabled; - - @Value("${services.exareme2.attributesUrl}") - private String exareme2AttributesUrl; - - @Value("${services.exareme2.cdesMetadataUrl}") - private String exareme2CDEsMetadataUrl; - - @Value("${services.exareme2.datasets_variables}") - private String exareme2DatasetsVariables; - - public PathologyService(ClaimUtils claimUtils) { - this.claimUtils = claimUtils; - } - - public List getPathologies(Authentication authentication, Logger logger) { - List allPathologyDTOs = getAggregatedPathologyDTOs(logger); - - if (!authenticationIsEnabled) { - return allPathologyDTOs; - } - return claimUtils.getAuthorizedPathologies(logger, authentication, allPathologyDTOs); - } - - /** - * This method will fetch all necessary information about pathologies from exareme and then aggregate them. - * The information that needs to be aggregated is the dataset CDE enumerations. - * Exareme returns the information about the dataset enumerations in a separate endpoint. - */ - private List getAggregatedPathologyDTOs(Logger logger) { - Map pathologiesMetadataHierarchy = getExaremePathologiesMetadataHierarchyDTO(logger); - Map> datasetsPerPathology = getExareme2DatasetsPerPathology(logger); - Map>> datasetsVariablesPerPathology = getExareme2DatasetsVariables(logger); - - List allPathologyDTOs = new ArrayList<>(); - for (String pathology : datasetsPerPathology.keySet()) { - PathologyMetadataDTO pathologyMetadata = pathologiesMetadataHierarchy.get(pathology); - assert pathologyMetadata != null; - List pathologyDatasets = datasetsPerPathology.get(pathology); - Map> pathologyDatasetVariables = Collections.emptyMap(); - Map> rawDatasetVariables = datasetsVariablesPerPathology.get(pathology); - if (rawDatasetVariables != null && !rawDatasetVariables.isEmpty()) { - Set datasetCodes = new HashSet<>(); - for (PathologyDTO.EnumerationDTO datasetDTO : pathologyDatasets) { - datasetCodes.add(datasetDTO.code()); - } - - Map> copy = new HashMap<>(); - rawDatasetVariables.forEach((datasetCode, variables) -> { - if (datasetCodes.contains(datasetCode)) { - List safeVariables = variables != null - ? Collections.unmodifiableList(new ArrayList<>(variables)) - : Collections.emptyList(); - copy.put(datasetCode, safeVariables); - } - }); - - if (!copy.isEmpty()) { - pathologyDatasetVariables = Collections.unmodifiableMap(copy); - } - } - - // Exareme collects the dataset CDE enumerations automatically from the nodes when there is an addition/deletion. - // Exareme provides that information in a separate endpoint from the rest of the pathologies' metadata. - // We need to manually update the dataset CDE enumerations in each pathology's metadata in order to - // return the latest information in the frontend, without the need for data aggregation. - if (!hasDatasetCDE(pathologyMetadata.variables(), pathologyMetadata.groups())) - throw new InternalServerError("CommonDataElement 'dataset' was not present in the pathology's metadata:" + pathologyMetadata); - updateDatasetCDEEnumerations(pathologyMetadata.variables(), pathologyMetadata.groups(), pathologyDatasets); - - allPathologyDTOs.add( - new PathologyDTO( - pathologyMetadata.code(), - pathologyMetadata.version(), - pathologyMetadata.label(), - pathologyMetadata.longitudinal(), - pathologyMetadata, - pathologyDatasets, - pathologyDatasetVariables - ) - ); - } - return allPathologyDTOs; - } - - private Map> getExareme2DatasetsPerPathology(Logger logger) { - Map> exareme2CDEsMetadata; - Type exaremeCDEsMetadataType = new TypeToken>>(){}.getType(); - try { - StringBuilder response = new StringBuilder(); - HTTPUtil.sendGet(exareme2CDEsMetadataUrl, response); - exareme2CDEsMetadata = JsonConverters.convertJsonStringToObject(response.toString(),exaremeCDEsMetadataType); - } catch (Exception e) { - logger.error("Could not fetch exareme2 datasets: " + e.getMessage()); - throw new InternalServerError(e.getMessage()); - } - - // Get the datasets for each pathology - Map> datasetsPerPathology = new HashMap<>(); - exareme2CDEsMetadata.forEach((pathology, cdePerDataset) -> { - List pathologyDatasetDTOS = new ArrayList<>(); - Map datasetEnumerations = (Map) cdePerDataset.get("dataset").enumerations(); - datasetEnumerations.forEach((code, label) -> pathologyDatasetDTOS.add(new PathologyDTO.EnumerationDTO(code, label))); - datasetsPerPathology.put(pathology, pathologyDatasetDTOS); - }); - - return datasetsPerPathology; - } - - private Map>> getExareme2DatasetsVariables(Logger logger) { - Map>> datasetsVariables; - Type datasetsVariablesType = new TypeToken>>>(){}.getType(); - try { - StringBuilder response = new StringBuilder(); - HTTPUtil.sendGet(exareme2DatasetsVariables, response); - datasetsVariables = JsonConverters.convertJsonStringToObject(response.toString(), datasetsVariablesType); - } catch (Exception e) { - logger.error("Could not fetch exareme2 datasets variables: " + e.getMessage()); - throw new InternalServerError(e.getMessage()); - } - - return datasetsVariables != null ? datasetsVariables : Collections.emptyMap(); - } - - private Map getExareme2PathologyAttributes(Logger logger) { - Map exareme2PathologyAttributes; - Type pathologyAttributesType = new TypeToken>(){}.getType(); - try { - StringBuilder response = new StringBuilder(); - HTTPUtil.sendGet(exareme2AttributesUrl, response); - exareme2PathologyAttributes = JsonConverters.convertJsonStringToObject(response.toString(),pathologyAttributesType); - } catch (Exception e) { - logger.error("Could not fetch exareme2 pathologies' metadata: " + e.getMessage()); - throw new InternalServerError(e.getMessage()); - } - - return exareme2PathologyAttributes; - } - - private Map getExaremePathologiesMetadataHierarchyDTO(Logger logger) { - Map pathologiesAttributes = getExareme2PathologyAttributes(logger); - - Map pathologiesHierarchies = new HashMap<>(); - pathologiesAttributes.forEach((pathology, attributes) -> { - assert attributes.properties != null; - assert attributes.properties.get("cdes") != null; - assert !attributes.properties.get("cdes").isEmpty(); - pathologiesHierarchies.put(pathology, attributes.properties.get("cdes").get(0)); - }); - - return pathologiesHierarchies; - } - - - private static boolean hasDatasetCDE( - List variables, - List groups - ) { - if (variables != null) { - for (PathologyMetadataDTO.CommonDataElementDTO variable : variables) { - if (variable.getCode().equals("dataset")){ - return true; - } - } - - } - if (groups != null) { - for (PathologyMetadataDTO.PathologyMetadataGroupDTO group: groups){ - if (hasDatasetCDE(group.variables(), group.groups())){ - return true; - } - } - } - return false; - } - - private static void updateDatasetCDEEnumerations( - List variables, - List groups, - List datasetEnumerations - ){ - if (variables != null) { - variables.stream().filter(cde -> cde.getCode().equals("dataset")). - findAny().ifPresent(cde -> cde.setEnumerations(datasetEnumerations)); - } - - if (groups != null) { - for (PathologyMetadataDTO.PathologyMetadataGroupDTO group: groups){ - updateDatasetCDEEnumerations( - group.variables(), - group.groups(), - datasetEnumerations - ); - } - } - } - - record DataModelAttributes(Map> properties, List tags){} - -} diff --git a/src/main/java/hbp/mip/user/ActiveUserAPI.java b/src/main/java/hbp/mip/user/ActiveUserAPI.java index f415d870e..475200d1c 100644 --- a/src/main/java/hbp/mip/user/ActiveUserAPI.java +++ b/src/main/java/hbp/mip/user/ActiveUserAPI.java @@ -3,18 +3,25 @@ import hbp.mip.utils.Logger; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.web.bind.annotation.*; +import org.springframework.util.StringUtils; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @RestController -@RequestMapping(value = "/activeUser", produces = {APPLICATION_JSON_VALUE}) +@RequestMapping(value = "/activeUser", produces = { APPLICATION_JSON_VALUE }) public class ActiveUserAPI { private final ActiveUserService activeUserService; + private final OAuth2AuthorizedClientService authorizedClientService; - public ActiveUserAPI(ActiveUserService activeUserService) { + public ActiveUserAPI(ActiveUserService activeUserService, OAuth2AuthorizedClientService authorizedClientService) { this.activeUserService = activeUserService; + this.authorizedClientService = authorizedClientService; } @GetMapping @@ -27,8 +34,39 @@ public ResponseEntity getTheActiveUser(Authentication authentication) { @PostMapping(value = "/agreeNDA") public ResponseEntity activeUserServiceAgreesToNDA(Authentication authentication) { - Logger logger = new Logger(activeUserService.getActiveUser(authentication).username(), "(GET) /activeUser/agreeNDA"); + Logger logger = new Logger(activeUserService.getActiveUser(authentication).username(), + "(POST) /activeUser/agreeNDA"); logger.info("User agreed to the NDA."); return ResponseEntity.ok(activeUserService.agreeToNDA(authentication)); } + + @GetMapping(value = "/token") + public ResponseEntity getActiveUserToken(Authentication authentication) { + if (authentication == null) { + Logger logger = new Logger("anonymous", "(GET) /activeUser/token"); + logger.warn("Authentication is missing."); + return ResponseEntity.status(401).body("Unauthorized"); + } + + // Bearer-token flow: JWT presented directly to this API. + if (authentication instanceof JwtAuthenticationToken jwtAuth) { + return ResponseEntity.ok(jwtAuth.getToken().getTokenValue()); + } + + // Browser session flow: return OAuth2 access token from authorized client. + if (authentication instanceof OAuth2AuthenticationToken oauth2Auth) { + OAuth2AuthorizedClient client = authorizedClientService.loadAuthorizedClient( + oauth2Auth.getAuthorizedClientRegistrationId(), + oauth2Auth.getName() + ); + if (client != null && client.getAccessToken() != null + && StringUtils.hasText(client.getAccessToken().getTokenValue())) { + return ResponseEntity.ok(client.getAccessToken().getTokenValue()); + } + } + + Logger logger = new Logger("unknown", "(GET) /activeUser/token"); + logger.warn("Could not extract access token from authentication object: " + authentication.getClass().getName()); + return ResponseEntity.status(404).body("Token not found"); + } } diff --git a/src/main/java/hbp/mip/user/ActiveUserService.java b/src/main/java/hbp/mip/user/ActiveUserService.java index 222faca20..e6a7319cd 100644 --- a/src/main/java/hbp/mip/user/ActiveUserService.java +++ b/src/main/java/hbp/mip/user/ActiveUserService.java @@ -1,18 +1,17 @@ package hbp.mip.user; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Scope; -import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Service; import java.util.Objects; @Service -@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS) public class ActiveUserService { private final UserRepository userRepository; @@ -20,8 +19,6 @@ public class ActiveUserService { private boolean authenticationIsEnabled; - private UserDTO activeUserDetails; - public ActiveUserService(UserRepository userRepository) { this.userRepository = userRepository; } @@ -33,17 +30,14 @@ public ActiveUserService(UserRepository userRepository) { * @return the userDAO */ public UserDTO getActiveUser(Authentication authentication) { - if (activeUserDetails != null) - return activeUserDetails; - UserDAO activeUserDAO; if (authenticationIsEnabled) { // If Authentication is ON, get user details from authentication info. - OidcUserInfo userinfo = ((DefaultOidcUser) authentication.getPrincipal()).getUserInfo(); - activeUserDAO = new UserDAO(userinfo.getPreferredUsername(), userinfo.getFullName(), userinfo.getEmail(), userinfo.getSubject()); + activeUserDAO = buildUserFromAuthentication(authentication); UserDAO activeUserDatabaseDetails = userRepository.findByUsername(activeUserDAO.getUsername()); if (activeUserDatabaseDetails != null) { + boolean changed = false; if ((!Objects.equals(activeUserDAO.getEmail(), activeUserDatabaseDetails.getEmail())) || !Objects.equals(activeUserDAO.getFullname(), activeUserDatabaseDetails.getFullname()) ) { @@ -51,20 +45,31 @@ public UserDTO getActiveUser(Authentication authentication) { // username is the PK in our database and subjectid is the PK in keycloak activeUserDatabaseDetails.setFullname(activeUserDAO.getFullname()); activeUserDatabaseDetails.setEmail(activeUserDAO.getEmail()); + changed = true; + } + if (changed) { + userRepository.save(activeUserDatabaseDetails); } activeUserDAO = activeUserDatabaseDetails; + } else { + userRepository.save(activeUserDAO); } - userRepository.save(activeUserDAO); } else { - // If Authentication is OFF, create anonymous user with accepted NDA - activeUserDAO = new UserDAO("anonymous", "anonymous", "anonymous@anonymous.com", "anonymousId"); - activeUserDAO.setAgreeNDA(true); - userRepository.save(activeUserDAO); + // If Authentication is OFF, ensure anonymous user exists with accepted NDA. + UserDAO anonymous = userRepository.findByUsername("anonymous"); + if (anonymous == null) { + anonymous = new UserDAO("anonymous", "anonymous", "anonymous@anonymous.com", "anonymousId"); + anonymous.setAgreeNDA(true); + userRepository.save(anonymous); + } else if (!Boolean.TRUE.equals(anonymous.getAgreeNDA())) { + anonymous.setAgreeNDA(true); + userRepository.save(anonymous); + } + activeUserDAO = anonymous; } - activeUserDetails = new UserDTO(activeUserDAO); - return activeUserDetails; + return new UserDTO(activeUserDAO); } public UserDTO agreeToNDA(Authentication authentication) { @@ -74,7 +79,60 @@ public UserDTO agreeToNDA(Authentication authentication) { userDAO.setAgreeNDA(true); userRepository.save(userDAO); - activeUserDetails = new UserDTO(userDAO); - return activeUserDetails; + return new UserDTO(userDAO); + } + + private static UserDAO buildUserFromAuthentication(Authentication authentication) { + Object principal = authentication != null ? authentication.getPrincipal() : null; + + // Browser login flow (session-based): OIDC user. + if (principal instanceof DefaultOidcUser oidcUser) { + OidcUserInfo userinfo = oidcUser.getUserInfo(); + return new UserDAO( + userinfo.getPreferredUsername(), + userinfo.getFullName(), + userinfo.getEmail(), + userinfo.getSubject() + ); + } + + // API client flow (token-based): JWT bearer. + Jwt jwt = null; + if (authentication instanceof JwtAuthenticationToken jwtAuth) { + jwt = jwtAuth.getToken(); + } else if (principal instanceof Jwt pJwt) { + jwt = pJwt; + } + + if (jwt != null) { + String username = firstNonBlank( + jwt.getClaimAsString("preferred_username"), + jwt.getClaimAsString("username"), + jwt.getSubject() + ); + String fullName = firstNonBlank( + jwt.getClaimAsString("name"), + jwt.getClaimAsString("given_name") + ); + String email = firstNonBlank(jwt.getClaimAsString("email"), username + "@unknown.local"); + String subject = firstNonBlank(jwt.getSubject(), username); + return new UserDAO(username, fullName, email, subject); + } + + // Fallback: keep behavior explicit rather than ClassCastException. + String fallbackName = authentication != null ? authentication.getName() : "unknown"; + return new UserDAO(fallbackName, fallbackName, fallbackName + "@unknown.local", fallbackName); + } + + private static String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String v : values) { + if (v != null && !v.trim().isEmpty()) { + return v; + } + } + return null; } } diff --git a/src/main/java/hbp/mip/user/UserDAO.java b/src/main/java/hbp/mip/user/UserDAO.java index d6f0074c9..ea29599ee 100644 --- a/src/main/java/hbp/mip/user/UserDAO.java +++ b/src/main/java/hbp/mip/user/UserDAO.java @@ -1,6 +1,7 @@ package hbp.mip.user; import com.google.gson.annotations.Expose; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; @@ -20,6 +21,7 @@ public class UserDAO { private String username; @Expose + @Column(name = "subject_id") private String subjectId; @Expose @@ -29,6 +31,7 @@ public class UserDAO { private String email; @Expose + @Column(name = "agree_nda") private Boolean agreeNDA; public UserDAO(String username, String fullname, String email, String subjectId) { diff --git a/src/main/java/hbp/mip/utils/ClaimUtils.java b/src/main/java/hbp/mip/utils/ClaimUtils.java index 540a8008a..e872aea8b 100644 --- a/src/main/java/hbp/mip/utils/ClaimUtils.java +++ b/src/main/java/hbp/mip/utils/ClaimUtils.java @@ -1,6 +1,7 @@ package hbp.mip.utils; -import hbp.mip.pathology.PathologyDTO; +import hbp.mip.datamodel.DataModelDTO; +import hbp.mip.utils.Exceptions.BadRequestException; import hbp.mip.utils.Exceptions.UnauthorizedException; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; @@ -45,14 +46,18 @@ private static ArrayList getAuthorityRoles(Authentication authentication } public void validateAccessRightsOnDatasets(Authentication authentication, - String experimentDatasets, Logger logger) { + List experimentDatasets, Logger logger) { + + if (experimentDatasets == null || experimentDatasets.isEmpty()) { + String errorMessage = "At least one dataset must be provided."; + logger.warn(errorMessage); + throw new BadRequestException(errorMessage); + } ArrayList authorities = getAuthorityRoles(authentication); - // Don't check for dataset claims if "super" claim exists allowing everything if (!hasRoleAccess(authorities, allDatasetsAllowedClaim, logger)) { - - for (String dataset : experimentDatasets.split(",")) { + for (String dataset : experimentDatasets) { String datasetRole = getDatasetClaim(dataset); if (!hasRoleAccess(authorities, datasetRole, logger)) { logger.warn("You are not allowed to use dataset: " + dataset); @@ -68,49 +73,64 @@ public boolean validateAccessRightsOnALLExperiments(Authentication authenticatio return hasRoleAccess(authorities, allExperimentsAllowedClaim, logger); } - public List getAuthorizedPathologies(Logger logger, Authentication authentication, - List allPathologies) { + public List getAuthorizedDataModels(Logger logger, Authentication authentication, + List allDataModels) { ArrayList authorities = getAuthorityRoles(authentication); if (hasRoleAccess(authorities, allDatasetsAllowedClaim, logger)) { - return allPathologies; + return allDataModels; } - List userPathologies = new ArrayList<>(); - for (PathologyDTO curPathology : allPathologies) { - List userPathologyDatasets = new ArrayList<>(); + List userDataModels = new ArrayList<>(); + for (DataModelDTO curDataModel : allDataModels) { + List dataModelDatasets = curDataModel.datasets(); + if (dataModelDatasets == null || dataModelDatasets.isEmpty()) { + continue; + } + + List userDataModelDatasets = new ArrayList<>(); Map> filteredDatasetVariables = new HashMap<>(); - Map> originalDatasetVariables = curPathology.datasetsVariables(); - for (PathologyDTO.EnumerationDTO dataset : curPathology.datasets()) { - if (hasRoleAccess(authorities, getDatasetClaim(dataset.code()), logger)) { - userPathologyDatasets.add(dataset); - List variables = originalDatasetVariables != null - ? originalDatasetVariables.get(dataset.code()) - : null; - List safeVariables = variables != null - ? Collections.unmodifiableList(new ArrayList<>(variables)) - : Collections.emptyList(); - filteredDatasetVariables.put(dataset.code(), safeVariables); + Map> originalDatasetVariables = curDataModel.datasetsVariables(); + + for (DataModelDTO.EnumerationDTO dataset : dataModelDatasets) { + if (!hasRoleAccess(authorities, getDatasetClaim(dataset.code()), logger)) { + continue; + } + + userDataModelDatasets.add(dataset); + if (originalDatasetVariables == null) { + continue; } + + List variables = originalDatasetVariables.get(dataset.code()); + List safeVariables = variables != null + ? Collections.unmodifiableList(new ArrayList<>(variables)) + : Collections.emptyList(); + filteredDatasetVariables.put(dataset.code(), safeVariables); } - if (!userPathologyDatasets.isEmpty()) { - Map> userDatasetsVariables = filteredDatasetVariables.isEmpty() - ? Collections.emptyMap() - : Collections.unmodifiableMap(filteredDatasetVariables); - PathologyDTO userPathology = new PathologyDTO( - curPathology.code(), - curPathology.version(), - curPathology.label(), - curPathology.longitudinal(), - curPathology.metadataHierarchy(), - userPathologyDatasets, - userDatasetsVariables - ); - userPathologies.add(userPathology); + if (userDataModelDatasets.isEmpty()) { + continue; } + + List safeDatasets = Collections.unmodifiableList(new ArrayList<>(userDataModelDatasets)); + Map> userDatasetsVariables = filteredDatasetVariables.isEmpty() + ? Collections.emptyMap() + : Collections.unmodifiableMap(filteredDatasetVariables); + + DataModelDTO userDataModel = new DataModelDTO( + curDataModel.code(), + curDataModel.version(), + curDataModel.label(), + curDataModel.longitudinal(), + curDataModel.variables(), + curDataModel.groups(), + safeDatasets, + userDatasetsVariables + ); + userDataModels.add(userDataModel); } - return userPathologies; + return userDataModels; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index acce438f9..dfc15bd46 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,18 +32,21 @@ spring: registration: keycloak: authorization-grant-type: authorization_code - client-id: MIP - client-secret: dae83a6b-c769-4186-8383-f0984c6edf05 + client-id: replaceme + client-secret: replaceme provider: keycloak scope: openid provider: keycloak: user-name-attribute: preferred_username - issuer-uri: http://172.17.0.1/auth/realms/MIP + issuer-uri: https://iam.ebrains.eu/auth/realms/MIP + resourceserver: + jwt: + issuer-uri: https://iam.ebrains.eu/auth/realms/MIP ### AUTHENTICATION ### authentication: - enabled: 0 + enabled: 1 all_datasets_allowed_claim: research_dataset_all all_experiments_allowed_claim: research_experiment_all dataset_claim_prefix: research_dataset_ @@ -51,13 +54,16 @@ authentication: ### EXTERNAL SERVICES ### services: algorithmsUpdateInterval: 100 - exareme2: + exaflow: algorithmsUrl: "http://127.0.0.1:5000/algorithms" attributesUrl: "http://127.0.0.1:5000/data_models_attributes" - cdesMetadataUrl: "http://127.0.0.1:5000/cdes_metadata" datasets_variables: "http://127.0.0.1:5000/datasets_variables" + cdesMetadataUrl: "http://127.0.0.1:5000/cdes_metadata" ### EXTERNAL FILES ### # Files are loaded from the resources files: disabledAlgorithms_json: "classPath:/disabledAlgorithms.json" + +frontend: + base-url: "http://localhost:4200" diff --git a/src/main/resources/db/migration/V1_1__Create.sql b/src/main/resources/db/migration/V1_1__Create.sql index aa755700a..303540c2a 100644 --- a/src/main/resources/db/migration/V1_1__Create.sql +++ b/src/main/resources/db/migration/V1_1__Create.sql @@ -1,948 +1,22 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 9.5.3 --- Dumped by pg_dump version 9.5.3 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SET check_function_bodies = false; -SET client_min_messages = warning; -SET row_security = off; - --- --- Name: postgres; Type: COMMENT; Schema: -; Owner: postgres --- - -COMMENT ON DATABASE postgres IS 'default administrative connection database'; - - --- --- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: --- - -CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; - - --- --- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; - - -SET search_path = public, pg_catalog; - -SET default_tablespace = ''; - -SET default_with_oids = false; - --- --- Name: app; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE app ( - id integer NOT NULL, - author character varying(255), - category character varying(255), - description text, - email character varying(255), - image text, - link character varying(255), - name character varying(255), - ratingcount integer, - totalrating integer -); - - -ALTER TABLE app OWNER TO postgres; - --- --- Name: article; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE article ( - slug character varying(255) NOT NULL, - abstract text, - content text, - createdat timestamp without time zone, - publishedat timestamp without time zone, - status character varying(255), - title character varying(255) NOT NULL, - updatedat timestamp without time zone, - createdby_username character varying(255), - updatedby_username character varying(255) -); - - -ALTER TABLE article OWNER TO postgres; - --- --- Name: article_tag; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE article_tag ( - article_slug character varying(255) NOT NULL, - tags_name character varying(255) NOT NULL -); - - -ALTER TABLE article_tag OWNER TO postgres; - --- --- Name: config; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE config ( - id bigint NOT NULL, - hasxaxis boolean, - height integer, - type character varying(255), - xaxisvariable character varying(255) -); - - -ALTER TABLE config OWNER TO postgres; - --- --- Name: config_title; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE config_title ( - config_id bigint NOT NULL, - title character varying(255), - title_key character varying(255) NOT NULL -); - - -ALTER TABLE config_title OWNER TO postgres; - --- --- Name: config_yaxisvariables; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE config_yaxisvariables ( - config_id bigint NOT NULL, - yaxisvariables character varying(255) -); - - -ALTER TABLE config_yaxisvariables OWNER TO postgres; - --- --- Name: dataset; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE dataset ( - code character varying(255) NOT NULL, - date timestamp without time zone -); - - -ALTER TABLE dataset OWNER TO postgres; - --- --- Name: dataset_data; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE dataset_data ( - dataset_code character varying(255) NOT NULL, - data bytea, - data_key character varying(255) NOT NULL -); - - -ALTER TABLE dataset_data OWNER TO postgres; - --- --- Name: dataset_grouping; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE dataset_grouping ( - dataset_code character varying(255) NOT NULL, - "grouping" character varying(255) -); - - -ALTER TABLE dataset_grouping OWNER TO postgres; - --- --- Name: dataset_header; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE dataset_header ( - dataset_code character varying(255) NOT NULL, - header character varying(255) -); - - -ALTER TABLE dataset_header OWNER TO postgres; - --- --- Name: dataset_variable; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE dataset_variable ( - dataset_code character varying(255) NOT NULL, - variable character varying(255) -); - - -ALTER TABLE dataset_variable OWNER TO postgres; - --- --- Name: experiment; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE experiment ( - uuid uuid NOT NULL, - algorithms text, - created timestamp without time zone, - finished timestamp without time zone, - haserror boolean NOT NULL, - hasservererror boolean NOT NULL, - name text, - result text, - resultsviewed boolean NOT NULL, - shared boolean NOT NULL, - validations text, - createdby_username character varying(255), - model_slug character varying(255) -); - - -ALTER TABLE experiment OWNER TO postgres; - --- --- Name: filter; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE filter ( - id bigint NOT NULL, - operator character varying(255), - variable_code character varying(255) -); - - -ALTER TABLE filter OWNER TO postgres; - --- --- Name: filter_values; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE filter_values ( - filter_id bigint NOT NULL, - "values" character varying(255) -); - - -ALTER TABLE filter_values OWNER TO postgres; - --- --- Name: group; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE "group" ( - code character varying(255) NOT NULL, - label character varying(255), - parent_code character varying(255) -); - - -ALTER TABLE "group" OWNER TO postgres; - --- --- Name: group_group; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE group_group ( - group_code character varying(255) NOT NULL, - groups_code character varying(255) NOT NULL -); - - -ALTER TABLE group_group OWNER TO postgres; - --- --- Name: hibernate_sequence; Type: SEQUENCE; Schema: public; Owner: postgres --- - -CREATE SEQUENCE hibernate_sequence - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - -ALTER TABLE hibernate_sequence OWNER TO postgres; - --- --- Name: model; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE model ( - slug character varying(255) NOT NULL, - createdat timestamp without time zone, - description character varying(255), - textquery text, - title character varying(255), - updatedat timestamp without time zone, - valid boolean, - config_id bigint, - createdby_username character varying(255), - dataset_code character varying(255), - query_id bigint, - updatedby_username character varying(255) -); - - -ALTER TABLE model OWNER TO postgres; - --- --- Name: query; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE query ( - id bigint NOT NULL, - request character varying(255) -); - - -ALTER TABLE query OWNER TO postgres; - --- --- Name: query_covariable; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE query_covariable ( - id bigint NOT NULL, - code character varying(255) NOT NULL -); - - -ALTER TABLE query_covariable OWNER TO postgres; - --- --- Name: query_filter; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE query_filter ( - query_id bigint NOT NULL, - filters_id bigint NOT NULL -); - - -ALTER TABLE query_filter OWNER TO postgres; - --- --- Name: query_grouping; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE query_grouping ( - id bigint NOT NULL, - code character varying(255) NOT NULL -); - - -ALTER TABLE query_grouping OWNER TO postgres; - --- --- Name: query_variable; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE query_variable ( - id bigint NOT NULL, - code character varying(255) NOT NULL -); - - -ALTER TABLE query_variable OWNER TO postgres; - --- --- Name: tag; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE tag ( - name character varying(255) NOT NULL -); - - -ALTER TABLE tag OWNER TO postgres; - --- --- Name: user; Type: TABLE; Schema: public; Owner: postgres --- - CREATE TABLE "user" ( - username character varying(255) NOT NULL, - agreenda boolean, - apikey character varying(255), - birthday character varying(255), - city character varying(255), - country character varying(255), - email character varying(255), - firstname character varying(255), - fullname character varying(255), - gender character varying(255), - isactive boolean, - lastname character varying(255), - password character varying(255), - phone character varying(255), - picture character varying(255), - team character varying(255), - web character varying(255) -); - - -ALTER TABLE "user" OWNER TO postgres; - --- --- Name: user_languages; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE user_languages ( - user_username character varying(255) NOT NULL, - languages character varying(255) -); - - -ALTER TABLE user_languages OWNER TO postgres; - --- --- Name: user_roles; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE user_roles ( - user_username character varying(255) NOT NULL, - roles character varying(255) -); - - -ALTER TABLE user_roles OWNER TO postgres; - --- --- Name: value; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE value ( - code character varying(255) NOT NULL, - label character varying(255) -); - - -ALTER TABLE value OWNER TO postgres; - --- --- Name: variable; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE variable ( - code character varying(255) NOT NULL, - description text, - iscovariable boolean, - isfilter boolean, - isgrouping boolean, - isvariable boolean, - label character varying(255), - length integer, - maxvalue double precision, - minvalue double precision, - type character varying(255), - units character varying(255), - group_code character varying(255) -); - - -ALTER TABLE variable OWNER TO postgres; - --- --- Name: variable_value; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE variable_value ( - variable_code character varying(255) NOT NULL, - values_code character varying(255) NOT NULL + username CHARACTER VARYING PRIMARY KEY, + subject_id CHARACTER VARYING, + fullname CHARACTER VARYING, + email CHARACTER VARYING, + agree_nda BOOLEAN DEFAULT FALSE +); + +CREATE TABLE "experiment" ( + uuid UUID PRIMARY KEY, + name TEXT, + created_by_username CHARACTER VARYING REFERENCES "user"(username), + status TEXT CHECK (status IN ('error', 'pending', 'success')), + result TEXT, + finished TIMESTAMP WITHOUT TIME ZONE, + algorithm TEXT, + algorithm_id TEXT, + created TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP WITHOUT TIME ZONE, + shared BOOLEAN DEFAULT FALSE, + viewed BOOLEAN DEFAULT FALSE ); - - -ALTER TABLE variable_value OWNER TO postgres; - --- --- Name: vote; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE vote ( - id bigint NOT NULL, - value integer NOT NULL, - app_id integer, - user_username character varying(255) -); - - -ALTER TABLE vote OWNER TO postgres; - - --- --- Name: hibernate_sequence; Type: SEQUENCE SET; Schema: public; Owner: postgres --- - -SELECT pg_catalog.setval('hibernate_sequence', 1, false); - - --- --- Name: app_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY app - ADD CONSTRAINT app_pkey PRIMARY KEY (id); - - --- --- Name: article_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY article - ADD CONSTRAINT article_pkey PRIMARY KEY (slug); - - --- --- Name: config_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY config - ADD CONSTRAINT config_pkey PRIMARY KEY (id); - - --- --- Name: config_title_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY config_title - ADD CONSTRAINT config_title_pkey PRIMARY KEY (config_id, title_key); - - --- --- Name: dataset_data_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY dataset_data - ADD CONSTRAINT dataset_data_pkey PRIMARY KEY (dataset_code, data_key); - - --- --- Name: dataset_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY dataset - ADD CONSTRAINT dataset_pkey PRIMARY KEY (code); - - --- --- Name: experiment_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY experiment - ADD CONSTRAINT experiment_pkey PRIMARY KEY (uuid); - - --- --- Name: filter_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY filter - ADD CONSTRAINT filter_pkey PRIMARY KEY (id); - - --- --- Name: group_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY "group" - ADD CONSTRAINT group_pkey PRIMARY KEY (code); - - --- --- Name: model_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY model - ADD CONSTRAINT model_pkey PRIMARY KEY (slug); - - --- --- Name: query_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY query - ADD CONSTRAINT query_pkey PRIMARY KEY (id); - - --- --- Name: tag_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY tag - ADD CONSTRAINT tag_pkey PRIMARY KEY (name); - - --- --- Name: uk_39uwjlq8i49lng83ca3ksh4mg; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY group_group - ADD CONSTRAINT uk_39uwjlq8i49lng83ca3ksh4mg UNIQUE (groups_code); - - --- --- Name: uk_kyrua54629jbk1ir780vji7s3; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY query_filter - ADD CONSTRAINT uk_kyrua54629jbk1ir780vji7s3 UNIQUE (filters_id); - - --- --- Name: user_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY "user" - ADD CONSTRAINT user_pkey PRIMARY KEY (username); - - --- --- Name: value_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY value - ADD CONSTRAINT value_pkey PRIMARY KEY (code); - - --- --- Name: variable_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY variable - ADD CONSTRAINT variable_pkey PRIMARY KEY (code); - - --- --- Name: vote_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY vote - ADD CONSTRAINT vote_pkey PRIMARY KEY (id); - - --- --- Name: fk_1fwrodma3jyox5kjqwpfegx1f; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY query_covariable - ADD CONSTRAINT fk_1fwrodma3jyox5kjqwpfegx1f FOREIGN KEY (code) REFERENCES variable(code); - - --- --- Name: fk_1kjpgxiphmdwn94d3wswywktw; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY user_roles - ADD CONSTRAINT fk_1kjpgxiphmdwn94d3wswywktw FOREIGN KEY (user_username) REFERENCES "user"(username); - - --- --- Name: fk_27affffd93lg54wkfi0eum3tm; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY model - ADD CONSTRAINT fk_27affffd93lg54wkfi0eum3tm FOREIGN KEY (config_id) REFERENCES config(id); - - --- --- Name: fk_39uwjlq8i49lng83ca3ksh4mg; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY group_group - ADD CONSTRAINT fk_39uwjlq8i49lng83ca3ksh4mg FOREIGN KEY (groups_code) REFERENCES "group"(code); - - --- --- Name: fk_3h0auvt8ej4raxwr7m4ua788f; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY model - ADD CONSTRAINT fk_3h0auvt8ej4raxwr7m4ua788f FOREIGN KEY (query_id) REFERENCES query(id); - - --- --- Name: fk_4jo0ysx9u1qrunx8ilmty3dvb; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY query_variable - ADD CONSTRAINT fk_4jo0ysx9u1qrunx8ilmty3dvb FOREIGN KEY (id) REFERENCES query(id); - - --- --- Name: fk_4kkhjrytsb3at05o5h7dqp2en; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY query_grouping - ADD CONSTRAINT fk_4kkhjrytsb3at05o5h7dqp2en FOREIGN KEY (code) REFERENCES variable(code); - - --- --- Name: fk_4m4cg0j1mtk42t739eer0l6ie; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY article_tag - ADD CONSTRAINT fk_4m4cg0j1mtk42t739eer0l6ie FOREIGN KEY (article_slug) REFERENCES article(slug); - - --- --- Name: fk_4yrfkom9tsbomi7ddq9yaaet6; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY vote - ADD CONSTRAINT fk_4yrfkom9tsbomi7ddq9yaaet6 FOREIGN KEY (app_id) REFERENCES app(id); - - --- --- Name: fk_55phec4qkjfaf40o26ainqh04; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY query_grouping - ADD CONSTRAINT fk_55phec4qkjfaf40o26ainqh04 FOREIGN KEY (id) REFERENCES query(id); - - --- --- Name: fk_6nlwicucyu66eo2crmm7faysc; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY dataset_variable - ADD CONSTRAINT fk_6nlwicucyu66eo2crmm7faysc FOREIGN KEY (dataset_code) REFERENCES dataset(code); - - --- --- Name: fk_728j4s15p4hjh1hgi69n6odht; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY experiment - ADD CONSTRAINT fk_728j4s15p4hjh1hgi69n6odht FOREIGN KEY (model_slug) REFERENCES model(slug); - - --- --- Name: fk_7amk7wobwicj4eu9kg27ath5d; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY config_yaxisvariables - ADD CONSTRAINT fk_7amk7wobwicj4eu9kg27ath5d FOREIGN KEY (config_id) REFERENCES config(id); - - --- --- Name: fk_87oyy59jkinfuuav3fgg778b1; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY dataset_data - ADD CONSTRAINT fk_87oyy59jkinfuuav3fgg778b1 FOREIGN KEY (dataset_code) REFERENCES dataset(code); - - --- --- Name: fk_9qh6d61269tit9ylbiw1io9ss; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY model - ADD CONSTRAINT fk_9qh6d61269tit9ylbiw1io9ss FOREIGN KEY (updatedby_username) REFERENCES "user"(username); - - --- --- Name: fk_bbtsxjmsa5mb44u8bnhiru8tx; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY variable - ADD CONSTRAINT fk_bbtsxjmsa5mb44u8bnhiru8tx FOREIGN KEY (group_code) REFERENCES "group"(code); - - --- --- Name: fk_cibo3wr7epeda4ihp4mk2uj3i; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY group_group - ADD CONSTRAINT fk_cibo3wr7epeda4ihp4mk2uj3i FOREIGN KEY (group_code) REFERENCES "group"(code); - - --- --- Name: fk_cl2hlvgcj1eunkncaeg7gjmv2; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY variable_value - ADD CONSTRAINT fk_cl2hlvgcj1eunkncaeg7gjmv2 FOREIGN KEY (variable_code) REFERENCES variable(code); - - --- --- Name: fk_dsqlojxvp4ugf7lfwkmtfyoyq; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY dataset_header - ADD CONSTRAINT fk_dsqlojxvp4ugf7lfwkmtfyoyq FOREIGN KEY (dataset_code) REFERENCES dataset(code); - - --- --- Name: fk_emu14hoeeji8n0dvgy0xe1w0m; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY "group" - ADD CONSTRAINT fk_emu14hoeeji8n0dvgy0xe1w0m FOREIGN KEY (parent_code) REFERENCES "group"(code); - - --- --- Name: fk_fxr8crpi28t12c5uis744oaf5; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY config_title - ADD CONSTRAINT fk_fxr8crpi28t12c5uis744oaf5 FOREIGN KEY (config_id) REFERENCES config(id); - - --- --- Name: fk_gaanmaixpa38dt6rpkaclip0p; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY model - ADD CONSTRAINT fk_gaanmaixpa38dt6rpkaclip0p FOREIGN KEY (dataset_code) REFERENCES dataset(code); - - --- --- Name: fk_hvivt77kdqt578xw1euc4ps2p; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY article_tag - ADD CONSTRAINT fk_hvivt77kdqt578xw1euc4ps2p FOREIGN KEY (tags_name) REFERENCES tag(name); - - --- --- Name: fk_ixt1r76x6eb8571d1rejyrf6d; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY filter_values - ADD CONSTRAINT fk_ixt1r76x6eb8571d1rejyrf6d FOREIGN KEY (filter_id) REFERENCES filter(id); - - --- --- Name: fk_kyrua54629jbk1ir780vji7s3; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY query_filter - ADD CONSTRAINT fk_kyrua54629jbk1ir780vji7s3 FOREIGN KEY (filters_id) REFERENCES filter(id); - - --- --- Name: fk_l1ltbtlvoof7ff3rpk33tkygi; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY query_variable - ADD CONSTRAINT fk_l1ltbtlvoof7ff3rpk33tkygi FOREIGN KEY (code) REFERENCES variable(code); - - --- --- Name: fk_l31vqnacpxbb2jmp326n9enmw; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY variable_value - ADD CONSTRAINT fk_l31vqnacpxbb2jmp326n9enmw FOREIGN KEY (values_code) REFERENCES value(code); - - --- --- Name: fk_lfvyrnhv3soykf3kodgnkx74o; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY user_languages - ADD CONSTRAINT fk_lfvyrnhv3soykf3kodgnkx74o FOREIGN KEY (user_username) REFERENCES "user"(username); - - --- --- Name: fk_mm0iove438omw0pl3xdlxsa6l; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY filter - ADD CONSTRAINT fk_mm0iove438omw0pl3xdlxsa6l FOREIGN KEY (variable_code) REFERENCES variable(code); - - --- --- Name: fk_mq1vuehgh2leq8g9dtdp0l6nq; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY model - ADD CONSTRAINT fk_mq1vuehgh2leq8g9dtdp0l6nq FOREIGN KEY (createdby_username) REFERENCES "user"(username); - - --- --- Name: fk_op53w7utnopm4ec3j1003bk8v; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY query_filter - ADD CONSTRAINT fk_op53w7utnopm4ec3j1003bk8v FOREIGN KEY (query_id) REFERENCES query(id); - - --- --- Name: fk_piooelhlqdg5ecwucse8jmpyx; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY experiment - ADD CONSTRAINT fk_piooelhlqdg5ecwucse8jmpyx FOREIGN KEY (createdby_username) REFERENCES "user"(username); - - --- --- Name: fk_px2lsjf6f4aajnrno4e3qiiav; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY dataset_grouping - ADD CONSTRAINT fk_px2lsjf6f4aajnrno4e3qiiav FOREIGN KEY (dataset_code) REFERENCES dataset(code); - - --- --- Name: fk_qlchk27g13xbd68uy13gwmkp; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY query_covariable - ADD CONSTRAINT fk_qlchk27g13xbd68uy13gwmkp FOREIGN KEY (id) REFERENCES query(id); - - --- --- Name: fk_qqo7ebj6u7gaymh4055f3cqcu; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY article - ADD CONSTRAINT fk_qqo7ebj6u7gaymh4055f3cqcu FOREIGN KEY (updatedby_username) REFERENCES "user"(username); - - --- --- Name: fk_sj9qxyo9ebe3rs9schj6cv15l; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY article - ADD CONSTRAINT fk_sj9qxyo9ebe3rs9schj6cv15l FOREIGN KEY (createdby_username) REFERENCES "user"(username); - - --- --- Name: fk_tjxqe691ce40h4cafbek3l27q; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY vote - ADD CONSTRAINT fk_tjxqe691ce40h4cafbek3l27q FOREIGN KEY (user_username) REFERENCES "user"(username); - - --- --- Name: public; Type: ACL; Schema: -; Owner: postgres --- - -REVOKE ALL ON SCHEMA public FROM PUBLIC; -REVOKE ALL ON SCHEMA public FROM postgres; -GRANT ALL ON SCHEMA public TO postgres; -GRANT ALL ON SCHEMA public TO PUBLIC; - - --- --- PostgreSQL database dump complete --- diff --git a/src/main/resources/db/migration/V1_2__Add_mip_version.sql b/src/main/resources/db/migration/V1_2__Add_mip_version.sql new file mode 100644 index 000000000..b7ebd8b47 --- /dev/null +++ b/src/main/resources/db/migration/V1_2__Add_mip_version.sql @@ -0,0 +1 @@ +ALTER TABLE experiment ADD COLUMN mip_version TEXT; diff --git a/src/main/resources/db/migration/V2_0__AddApps.sql b/src/main/resources/db/migration/V2_0__AddApps.sql deleted file mode 100644 index 175a27442..000000000 --- a/src/main/resources/db/migration/V2_0__AddApps.sql +++ /dev/null @@ -1,13 +0,0 @@ --- --- Add app --- - -COPY app (id, author, category, description, email, image, link, name, ratingcount, totalrating) FROM stdin; -1 Linda Dib and Mirco Nasuti Data Mining Tools Choose a brain region, a gene and a time-frame to visualize gene expression and find correlations betweeen them. mirco.nasuti@gmail.com https://mip.humanbrainproject.eu/images/apps/logo-geneexpression.png https://mip.humanbrainproject.eu/genexpression Gene Expression 0 0 -2 Unknown Data Mining Tools Navigate in a 3D world of variables. \N https://mip.humanbrainproject.eu/images/apps/logo-3d-explore.png https://mip.humanbrainproject.eu/scripts/external/ViewerProject/GraphViewer.html 3D Biological Rules 0 0 -3 Unknown Data Mining Tools Categorize, cluster and classify data. \N https://mip.humanbrainproject.eu/images/apps/logo-3c.png https://mip.humanbrainproject.eu/scripts/external/3c/index.html 3C 0 0 -4 Unknown Data Exploration Tools Visualize medical data using bar graphs. \N https://mip.humanbrainproject.eu/images/apps/logo-bargraph.png https://mip.humanbrainproject.eu/graphdata/bargraph.html Bargraph 0 0 -5 Unknown Data Exploration Tools Visualize medical data using sunburst graphs. \N https://mip.humanbrainproject.eu/images/apps/logo-sunburst.png https://mip.humanbrainproject.eu/graphdata/sunburst.html Sunburst 0 0 -6 Unknown Brain Data Viewers Visualize a 3D brain. \N https://mip.humanbrainproject.eu/images/apps/logo-3d-brain.png http://hbps1.chuv.ch/webgraph/#/3d-brain 3D Brain 0 0 -7 Unknown Brain Data Viewers View 2D atlases from NIFTI files. \N https://mip.humanbrainproject.eu/images/apps/logo-2d-atlas.png https://mip.humanbrainproject.eu/scripts/external/Papaya/standard-with-atlas/index.html 2D Atlas 0 0 -\. diff --git a/src/main/resources/db/migration/V2_1__AddBioRulesApp.sql b/src/main/resources/db/migration/V2_1__AddBioRulesApp.sql deleted file mode 100644 index 8e711e35c..000000000 --- a/src/main/resources/db/migration/V2_1__AddBioRulesApp.sql +++ /dev/null @@ -1,4 +0,0 @@ -INSERT INTO app (id, author, category, description, email, image, link, name, ratingcount, totalrating) -VALUES (8, 'Mirco Nasuti', 'Data Mining Tools', '2D graphical view of biological rules', 'mirco.nasuti@chuv.ch', -'https://mip.humanbrainproject.eu/images/apps/logo-graphviewer.png', 'http://hbps1.chuv.ch/biorules/app', -'2D Biological Rules', 0, 0) diff --git a/src/main/resources/db/migration/V3_0__AddFilter.sql b/src/main/resources/db/migration/V3_0__AddFilter.sql deleted file mode 100644 index 64c4fefb7..000000000 --- a/src/main/resources/db/migration/V3_0__AddFilter.sql +++ /dev/null @@ -1,8 +0,0 @@ -ALTER TABLE query ADD COLUMN sql_filter text; - -ALTER TABLE query_filter DROP COLUMN filters_id; -DROP TABLE filter_values, filter; - -ALTER TABLE query_filter ADD COLUMN code character varying(255) NOT NULL DEFAULT ''; -ALTER TABLE ONLY query_filter - ADD CONSTRAINT fk_1 FOREIGN KEY (query_id) REFERENCES query(id); diff --git a/src/main/resources/db/migration/V4_0__UpdateAppsPaths.sql b/src/main/resources/db/migration/V4_0__UpdateAppsPaths.sql deleted file mode 100644 index a6aa304f9..000000000 --- a/src/main/resources/db/migration/V4_0__UpdateAppsPaths.sql +++ /dev/null @@ -1,12 +0,0 @@ -UPDATE app SET link='/ViewerProject/GraphViewer.html' WHERE id=2; -UPDATE app SET link='/3c/index.html' WHERE id=3; -UPDATE app SET link='/Papaya/standard-with-atlas/index.html' WHERE id=7; - -UPDATE app SET image='/logo-geneexpression.png' WHERE id=1; -UPDATE app SET image='/logo-3d-explore.png' WHERE id=2; -UPDATE app SET image='/logo-3c.png' WHERE id=3; -UPDATE app SET image='/logo-bargraph.png' WHERE id=4; -UPDATE app SET image='/logo-sunburst.png' WHERE id=5; -UPDATE app SET image='/logo-3d-brain.png' WHERE id=6; -UPDATE app SET image='/logo-2d-atlas.png' WHERE id=7; -UPDATE app SET image='/logo-graphviewer.png' WHERE id=8; diff --git a/src/main/resources/db/migration/V4_1__UpdatePapayaPath.sql b/src/main/resources/db/migration/V4_1__UpdatePapayaPath.sql deleted file mode 100644 index ea4a02048..000000000 --- a/src/main/resources/db/migration/V4_1__UpdatePapayaPath.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE app SET link='/Papaya/index.html' WHERE id=7; diff --git a/src/main/resources/db/migration/V4_2__OnlyLocalAppsPaths.sql b/src/main/resources/db/migration/V4_2__OnlyLocalAppsPaths.sql deleted file mode 100644 index cefe3ead4..000000000 --- a/src/main/resources/db/migration/V4_2__OnlyLocalAppsPaths.sql +++ /dev/null @@ -1,5 +0,0 @@ -UPDATE app SET link='/GeneExpression/index.html' WHERE id=1; -UPDATE app SET link='/Bargraph/index.html' WHERE id=4; -UPDATE app SET link='/Sunburst/index.html' WHERE id=5; -UPDATE app SET link='/3d-brain/index.html' WHERE id=6; -UPDATE app SET link='/2d-bio-rules/app/index.html' WHERE id=8; diff --git a/src/main/resources/db/migration/V4_3__UpdateBrainViewerPath.sql b/src/main/resources/db/migration/V4_3__UpdateBrainViewerPath.sql deleted file mode 100644 index b1f3fdc89..000000000 --- a/src/main/resources/db/migration/V4_3__UpdateBrainViewerPath.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE app SET link='/3d-brain/app/index.html' WHERE id=6; diff --git a/src/main/resources/db/migration/V5_0__UpdateFilters.sql b/src/main/resources/db/migration/V5_0__UpdateFilters.sql deleted file mode 100644 index aadf799bf..000000000 --- a/src/main/resources/db/migration/V5_0__UpdateFilters.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE query DROP COLUMN sql_filter; -ALTER TABLE query ADD COLUMN filters text; diff --git a/src/main/resources/db/migration/V5_1__RemoveTextQuery.sql b/src/main/resources/db/migration/V5_1__RemoveTextQuery.sql deleted file mode 100644 index e863362b7..000000000 --- a/src/main/resources/db/migration/V5_1__RemoveTextQuery.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE model DROP COLUMN textquery; diff --git a/src/main/resources/db/migration/V5_2__AddDatasetFilters.sql b/src/main/resources/db/migration/V5_2__AddDatasetFilters.sql deleted file mode 100644 index 2923d5b83..000000000 --- a/src/main/resources/db/migration/V5_2__AddDatasetFilters.sql +++ /dev/null @@ -1,37 +0,0 @@ --- --- Name: query_covariable; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE query_training_datasets ( - id bigint NOT NULL, - code character varying(255) NOT NULL -); - - -ALTER TABLE query_training_datasets OWNER TO postgres; - -ALTER TABLE ONLY query_training_datasets - ADD CONSTRAINT fk_query_training_datasets FOREIGN KEY (code) REFERENCES variable(code); - - -CREATE TABLE query_testing_datasets ( - id bigint NOT NULL, - code character varying(255) NOT NULL -); - - -ALTER TABLE query_testing_datasets OWNER TO postgres; - -ALTER TABLE ONLY query_testing_datasets - ADD CONSTRAINT fk_query_testing_datasets FOREIGN KEY (code) REFERENCES variable(code); - -CREATE TABLE query_validation_datasets ( - id bigint NOT NULL, - code character varying(255) NOT NULL -); - - -ALTER TABLE query_validation_datasets OWNER TO postgres; - -ALTER TABLE ONLY query_validation_datasets - ADD CONSTRAINT fk_query_validation_datasets FOREIGN KEY (code) REFERENCES variable(code); diff --git a/src/main/resources/db/migration/V5_3__AddPathology.sql b/src/main/resources/db/migration/V5_3__AddPathology.sql deleted file mode 100644 index 164bd059b..000000000 --- a/src/main/resources/db/migration/V5_3__AddPathology.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE query ADD COLUMN pathology text; diff --git a/src/main/resources/db/migration/V6_0__AddWorkflowDetails.sql b/src/main/resources/db/migration/V6_0__AddWorkflowDetails.sql deleted file mode 100644 index 0e508b0ef..000000000 --- a/src/main/resources/db/migration/V6_0__AddWorkflowDetails.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE experiment ADD COLUMN workflowhistoryid text; -ALTER TABLE experiment ADD COLUMN workflowstatus text; \ No newline at end of file diff --git a/src/main/resources/db/migration/V7_0__NewDatabaseStructure.sql b/src/main/resources/db/migration/V7_0__NewDatabaseStructure.sql deleted file mode 100644 index 6a3beee93..000000000 --- a/src/main/resources/db/migration/V7_0__NewDatabaseStructure.sql +++ /dev/null @@ -1,84 +0,0 @@ -UPDATE experiment -SET algorithms = - ( - SELECT SUBSTR(algorithms, 2, LENGTH(algorithms) - 2) - ); - -UPDATE experiment -SET workflowstatus = 'error' -WHERE workflowstatus IS NULL AND haserror; - -UPDATE experiment -SET workflowstatus = 'success' -WHERE workflowstatus IS NULL AND NOT haserror; - -UPDATE experiment -SET workflowstatus = 'success' -WHERE workflowstatus = 'completed'; - -UPDATE experiment -SET workflowstatus = 'pending' -WHERE workflowstatus = 'running'; - -ALTER TABLE experiment -DROP COLUMN haserror, -DROP COLUMN hasservererror, -DROP COLUMN validations, -DROP COLUMN model_slug; - -ALTER TABLE experiment -RENAME algorithms TO algorithm; -ALTER TABLE experiment -ALTER COLUMN algorithm TYPE json USING algorithm::json; -ALTER TABLE experiment -RENAME createdby_username TO created_by_username; -ALTER TABLE experiment -RENAME workflowhistoryid TO workflow_history_id; -ALTER TABLE experiment -RENAME resultsviewed TO viewed; -ALTER TABLE experiment -RENAME workflowstatus TO status; - -ALTER TABLE experiment -ADD COLUMN algorithmId text; - -UPDATE experiment -SET algorithmId = (algorithm ->> 'name'); - -ALTER TABLE experiment -ALTER COLUMN algorithm TYPE text; -ALTER TABLE experiment -ADD COLUMN updated timestamp without time zone; - -ALTER TABLE "user" -DROP COLUMN birthday, -DROP COLUMN city, -DROP COLUMN country, -DROP COLUMN firstname, -DROP COLUMN gender, -DROP COLUMN isactive, -DROP COLUMN lastname, -DROP COLUMN password, -DROP COLUMN phone, -DROP COLUMN picture, -DROP COLUMN team, -DROP COLUMN web, -DROP COLUMN apikey; - -ALTER TABLE "user" -ADD COLUMN subjectID text; - -DROP TABLE "config_title", "config_yaxisvariables"; -DROP TABLE "dataset_variable", "dataset_grouping", "dataset_data", "dataset_header"; -DROP TABLE "query_variable", "query_grouping", "query_filter", "query_covariable"; -DROP TABLE "article_tag", "tag","article"; -DROP TABLE "variable_value"; -DROP TABLE "query_training_datasets", "query_validation_datasets", "query_testing_datasets"; -DROP TABLE "variable", "value"; -DROP TABLE "group_group", "group"; -DROP TABLE "model"; -DROP TABLE "query"; -DROP TABLE "dataset"; -DROP TABLE "config"; -DROP TABLE "vote", "app"; -DROP TABLE "user_roles", "user_languages"; \ No newline at end of file diff --git a/src/main/resources/db/migration/V7_1__NewExperimentResultFormat.sql b/src/main/resources/db/migration/V7_1__NewExperimentResultFormat.sql deleted file mode 100644 index e1d5f12c4..000000000 --- a/src/main/resources/db/migration/V7_1__NewExperimentResultFormat.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE OR REPLACE FUNCTION ISJSON(p_json text) - RETURNS boolean -AS -$$ -BEGIN - RETURN (p_json::json is not null); -EXCEPTION - WHEN others THEN - RETURN false; -END; -$$ -language plpgsql -immutable; -UPDATE experiment SET result = result::json #>>'{0,result}' WHERE (algorithm::json->>'type') <> 'workflow' and ISJSON(result); -UPDATE experiment SET result = '', status = 'error' WHERE NOT ISJSON(result); diff --git a/src/main/resources/db/migration/V7_2__UpdateStatusAccordingToResult.sql b/src/main/resources/db/migration/V7_2__UpdateStatusAccordingToResult.sql deleted file mode 100644 index 21022abcc..000000000 --- a/src/main/resources/db/migration/V7_2__UpdateStatusAccordingToResult.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE experiment -SET status = 'error' -WHERE -result LIKE '%text/plain+error%' -OR -result LIKE '%text/plain+warning%' -OR -result LIKE '%text/plain+user_error%'; diff --git a/src/main/resources/db/migration/V8_0__RemoveWorkflowHistoryId.sql b/src/main/resources/db/migration/V8_0__RemoveWorkflowHistoryId.sql deleted file mode 100644 index bd9a01d2e..000000000 --- a/src/main/resources/db/migration/V8_0__RemoveWorkflowHistoryId.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE experiment DROP COLUMN workflow_history_id; \ No newline at end of file diff --git a/src/main/resources/log4j2.yml b/src/main/resources/log4j2.yml index cb06816b2..065e07be3 100644 --- a/src/main/resources/log4j2.yml +++ b/src/main/resources/log4j2.yml @@ -11,7 +11,7 @@ Configuration: name: ConsoleAppender target: SYSTEM_OUT PatternLayout: - pattern: "%d{yyyy-MM-dd HH:mm:ss,SSS} - %-5level - %logger{36} - [${env:FEDERATION:-default}] - [${env:SERVICE:-portal-backend}] - %msg%n" + pattern: "%d{yyyy-MM-dd HH:mm:ss,SSS} - %-5level - %logger{36} - [${env:FEDERATION:-default}] - [${env:SERVICE:-platform-backend}] - %msg%n" Loggers: logger: