Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/main/java/hbp/mip/algorithm/AnalysisAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ public ResponseEntity<Object> runAnalysis(
authentication,
analysisRequest.inputdata().datasets(),
logger);

var validationDatasets = analysisRequest.inputdata().validation_datasets();
if (validationDatasets != null && !validationDatasets.isEmpty()) {
claimUtils.validateAccessRightsOnDatasets(
authentication,
validationDatasets,
logger);
}
}
AnalysisService.AnalysisResultDTO result = analysisService.runAnalysis(analysisRequest, logger);
return ResponseEntity.status(result.code()).body(result.body());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package hbp.mip.algorithm;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.annotations.SerializedName;

import java.util.List;
Expand All @@ -10,7 +11,9 @@ public record ParameterSpecificationDTO(
List<String> types,
Boolean required,
Boolean multiple,
@SerializedName("default") Object default_value,
@SerializedName("default")
@JsonProperty("default")
Object default_value,
ParameterEnumSpecificationDTO enums,
ParameterEnumSpecificationDTO dict_keys_enums,
ParameterEnumSpecificationDTO dict_values_enums,
Expand Down
11 changes: 9 additions & 2 deletions src/main/java/hbp/mip/algorithm/SpecificationsService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package hbp.mip.algorithm;

import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import hbp.mip.utils.Exceptions.InternalServerError;
import hbp.mip.utils.HTTPUtil;
Expand All @@ -8,6 +9,7 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;

Expand Down Expand Up @@ -44,13 +46,18 @@ public List<AlgorithmSpecificationDTO> getAlgorithmSpecifications(Logger logger)
private <T> T fetch(String url, Type typeOfT, String what, Logger logger) {
StringBuilder response = new StringBuilder();
try {
HTTPUtil.sendGet(url, response);
int responseCode = HTTPUtil.sendGet(url, response);
if (responseCode != 200) {
throw new InternalServerError(
"Exaflow " + what + " endpoint responded with status " + responseCode + ".");
}

T specification = JsonConverters.convertJsonStringToObject(response.toString(), typeOfT);
if (specification == null || (specification instanceof List<?> list && list.isEmpty())) {
throw new InternalServerError("Exaflow " + what + " response was empty.");
}
return specification;
} catch (Exception e) {
} catch (IOException | JsonSyntaxException e) {
logger.error("Could not fetch exaflow " + what + ": " + e.getMessage());
throw new InternalServerError("Could not fetch exaflow " + what + ".");
}
Expand Down
21 changes: 17 additions & 4 deletions src/main/java/hbp/mip/experiment/ExperimentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,14 @@ public ExperimentDTO createExperiment(Authentication authentication, ExperimentE
}

private void requireAnalysisPayload(ExperimentExecutionDTO experimentExecutionDTO, Logger logger) {
if (experimentExecutionDTO.analysis() == null) {
AnalysisRequestDTO analysis = experimentExecutionDTO.analysis();
boolean missingPayload = analysis == null
|| analysis.algorithm() == null
|| analysis.algorithm().name() == null
|| analysis.algorithm().name().isBlank()
|| analysis.inputdata() == null;

if (missingPayload) {
String errorMessage = "Missing required analysis payload.";
logger.warn(errorMessage);
throw new BadRequestException(errorMessage);
Expand All @@ -161,9 +168,15 @@ private void requireAnalysisPayload(ExperimentExecutionDTO experimentExecutionDT

private void validateDatasetAccess(Authentication authentication, ExperimentExecutionDTO experimentExecutionDTO,
Logger logger) {
if (authenticationIsEnabled) {
claimUtils.validateAccessRightsOnDatasets(authentication,
experimentExecutionDTO.analysis().inputdata().datasets(), logger);
if (!authenticationIsEnabled) {
return;
}

AnalysisRequestDTO.AnalysisInputDataDTO inputdata = experimentExecutionDTO.analysis().inputdata();
claimUtils.validateAccessRightsOnDatasets(authentication, inputdata.datasets(), logger);

if (inputdata.validation_datasets() != null && !inputdata.validation_datasets().isEmpty()) {
claimUtils.validateAccessRightsOnDatasets(authentication, inputdata.validation_datasets(), logger);
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/main/java/hbp/mip/folder/ExperimentFolderService.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public ExperimentFolderDTO createFolder(Authentication authentication, CreateExp
public ExperimentFolderDTO renameFolder(Authentication authentication, String folderId,
RenameExperimentFolderDTO request, Logger logger) {
var user = activeUserService.getActiveUser(authentication);
ExperimentFolderDAO folder = ownedFolder(folderId, user.username(), logger);
ExperimentFolderDAO folder = ownedFolderForUpdate(folderId, user.username(), logger);

String name = requireName(request == null ? null : request.name(), "Folder name", logger);
rejectDuplicateFolderName(name, folderRepository.findOwnedFolders(user.username()), folder, logger);
Expand All @@ -115,7 +115,7 @@ public ExperimentFolderDTO renameFolder(Authentication authentication, String fo
@Transactional
public void deleteFolder(Authentication authentication, String folderId, Logger logger) {
var user = activeUserService.getActiveUser(authentication);
ExperimentFolderDAO folder = ownedFolder(folderId, user.username(), logger);
ExperimentFolderDAO folder = ownedFolderForUpdate(folderId, user.username(), logger);

folderRepository.delete(folder);
logger.info("Experiment folder deleted. Id: " + folder.getId());
Expand Down Expand Up @@ -206,7 +206,7 @@ public ExperimentFolderDTO createSet(Authentication authentication, String folde
public ExperimentFolderDTO renameSet(Authentication authentication, String folderId, String setId,
RenameExperimentSetDTO request, Logger logger) {
var user = activeUserService.getActiveUser(authentication);
ExperimentFolderDAO folder = ownedFolder(folderId, user.username(), logger);
ExperimentFolderDAO folder = ownedFolderForUpdate(folderId, user.username(), logger);
ExperimentSetDAO set = ownedSet(folder, setId, logger);

String name = requireName(request == null ? null : request.name(), "Set name", logger);
Expand Down
27 changes: 13 additions & 14 deletions src/main/java/hbp/mip/utils/HTTPUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
Expand All @@ -15,8 +16,8 @@ private HTTPUtil() {
throw new IllegalAccessError("HTTPUtil class");
}

public static void sendGet(String url, StringBuilder resp) throws IOException {
sendHTTP(url, "", resp, "GET");
public static int sendGet(String url, StringBuilder resp) throws IOException {
return sendHTTP(url, "", resp, "GET");
}

public static int sendPost(String url, String query, StringBuilder resp) throws IOException {
Expand Down Expand Up @@ -45,20 +46,18 @@ private static int sendHTTP(String url, String query, StringBuilder resp, String

int respCode = con.getResponseCode();

BufferedReader in;
if (respCode == 200) {
in = new BufferedReader(new InputStreamReader(con.getInputStream()));
} else {
in = new BufferedReader(new InputStreamReader(con.getErrorStream()));
}
String inputLine;
StringBuilder response = new StringBuilder();
InputStream stream = respCode == 200 ? con.getInputStream() : con.getErrorStream();
if (stream != null) {
BufferedReader in = new BufferedReader(new InputStreamReader(stream));
String inputLine;
StringBuilder response = new StringBuilder();

while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
resp.append(response);
}
in.close();
resp.append(response);

return respCode;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
-- One-time conversion of experiment.algorithm payloads written in the old
-- AlgorithmExecutionDTO shape (top-level name/inputdata/parameters/preprocessing map)
-- into the AnalysisRequestDTO shape (top-level algorithm plus preprocessing list).
-- Rows already in the new shape are left untouched, and malformed/unrelated JSON is skipped.
DO $$
DECLARE
experiment_row RECORD;
legacy_json JSON;
legacy JSONB;
preprocessing JSONB;
migrated JSONB;
BEGIN
FOR experiment_row IN
SELECT uuid, algorithm
FROM experiment
WHERE algorithm IS NOT NULL AND btrim(algorithm) <> ''
LOOP
BEGIN
legacy_json := experiment_row.algorithm::json;
EXCEPTION WHEN OTHERS THEN
CONTINUE;
END;

legacy := legacy_json::jsonb;

-- The new shape always carries the top-level algorithm object.
IF legacy ? 'algorithm' THEN
CONTINUE;
END IF;

-- The old AlgorithmExecutionDTO always had a top-level name.
IF NOT (legacy ? 'name') THEN
CONTINUE;
END IF;

IF legacy->'preprocessing' IS NULL OR legacy->'preprocessing' = 'null'::jsonb THEN
preprocessing := NULL;
ELSIF jsonb_typeof(legacy->'preprocessing') = 'object' THEN
-- jsonb has no key order; json_each over the original json text preserves it.
SELECT COALESCE(
jsonb_agg(jsonb_build_object('name', key, 'parameters', value::jsonb)),
'[]'::jsonb)
INTO preprocessing
FROM json_each(legacy_json->'preprocessing');
ELSIF jsonb_typeof(legacy->'preprocessing') = 'array' THEN
preprocessing := legacy->'preprocessing';
ELSE
preprocessing := NULL;
END IF;

migrated := jsonb_build_object(
'request_id', NULL,
'inputdata', jsonb_build_object(
'data_model', legacy->'inputdata'->'data_model',
'datasets', legacy->'inputdata'->'datasets',
'validation_datasets', legacy->'inputdata'->'validation_datasets',
'filters', legacy->'inputdata'->'filters',
'variables', NULL
),
'preprocessing', preprocessing,
'algorithm', jsonb_build_object(
'name', legacy->'name',
'x', legacy->'inputdata'->'x',
'y', legacy->'inputdata'->'y',
'parameters', legacy->'parameters'
),
'flags', NULL
);

UPDATE experiment
SET algorithm = migrated::text
WHERE uuid = experiment_row.uuid;
END LOOP;
END $$;
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ void deserializesHeadAlgorithmSpecificationShape() throws Exception {
"types": ["dict"],
"required": false,
"multiple": false,
"default": 5,
"min": 0,
"max": 10,
"dict_keys_enums": {
Expand Down Expand Up @@ -68,12 +69,14 @@ void deserializesHeadAlgorithmSpecificationShape() throws Exception {
assertThat(algorithm.y().min_count()).isEqualTo(1);
assertThat(algorithm.y().max_count()).isEqualTo(1);
assertThat(algorithm.requires_validation_datasets()).isFalse();
assertThat(folds.default_value()).isEqualTo(5.0);
assertThat(folds.min()).isEqualTo(0.0);
assertThat(folds.max()).isEqualTo(10.0);

JsonNode serialized = objectMapper.readTree(objectMapper.writeValueAsString(algorithm));
assertThat(serialized.at("/documentation").asText()).isEqualTo("Long algorithm documentation.");
assertThat(serialized.at("/parameters/folds/dict_values_type").asText()).isEqualTo("real");
assertThat(serialized.at("/parameters/folds/default").asDouble()).isEqualTo(5.0);
assertThat(serialized.at("/requires_validation_datasets").asBoolean()).isFalse();
}

Expand Down
71 changes: 71 additions & 0 deletions src/test/java/hbp/mip/algorithm/AnalysisAPITest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package hbp.mip.algorithm;

import hbp.mip.user.ActiveUserService;
import hbp.mip.user.UserDTO;
import hbp.mip.utils.ClaimUtils;
import hbp.mip.utils.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.Authentication;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class AnalysisAPITest {

@Mock
private ActiveUserService activeUserService;

@Mock
private AnalysisService analysisService;

@Mock
private ClaimUtils claimUtils;

@Mock
private Authentication authentication;

private AnalysisAPI analysisAPI;

@BeforeEach
void setUp() {
analysisAPI = new AnalysisAPI(activeUserService, analysisService, claimUtils);
ReflectionTestUtils.setField(analysisAPI, "authenticationIsEnabled", true);
when(activeUserService.getActiveUser(authentication))
.thenReturn(new UserDTO("user", "User", "user@example.org", "subject", true));
}

@Test
void runAnalysis_validatesValidationDatasetsToo() {
var request = new AnalysisRequestDTO(
null,
new AnalysisRequestDTO.AnalysisInputDataDTO(
"dm:1",
List.of("ds1"),
List.of("dsv"),
null,
List.of("age")),
null,
new AnalysisRequestDTO.AnalysisAlgorithmDTO("histogram", null, List.of("age"), Map.of()),
null);
when(analysisService.runAnalysis(eq(request), any(Logger.class)))
.thenReturn(new AnalysisService.AnalysisResultDTO(200, Map.of("ok", true)));

var response = analysisAPI.runAnalysis(authentication, request);

assertThat(response.getStatusCode().value()).isEqualTo(200);
verify(claimUtils).validateAccessRightsOnDatasets(eq(authentication), eq(List.of("ds1")), any(Logger.class));
verify(claimUtils).validateAccessRightsOnDatasets(eq(authentication), eq(List.of("dsv")), any(Logger.class));
}
}
Loading
Loading