diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 011209f07cb1d8dfe265becbf234916b550c1227..a8a9c0e160044c46d7b182f1b6f0f26f9fcb6cd7 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.6.2 +current_version = 2.6.8 commit = True tag = True tag_name = {new_version} diff --git a/docker/README.md b/docker/README.md index 33262bddeb289cca0a33a980fcd462b8e3fc4d9b..6e66dafa60dc9a73a74d27303d2b3b670b4766ce 100644 --- a/docker/README.md +++ b/docker/README.md @@ -24,7 +24,6 @@ To use this image, you need a running instance of PostgreSQL and to configure th * FEATURES_DB_USER: User to use when connecting to the science database, default value is "postgres". * FEATURES_DB_PASSWORD: Password to use when connecting to the science database. * FEATURES_DB_MAIN_TABLE: Table that contains the scientific data to use, default value is "features". -* DATASETS (temporary hack): list of datasets available in the features table ### OAUTH2 LOGIN @@ -64,3 +63,10 @@ To use this image, you need a running instance of PostgreSQL and to configure th * CONTEXT_PATH: context path appended to all services running in this container. Default to "/services". * SESSION_TIMEOUT: Timeout in milliseconds for session expiration. Default to 2592000. + +### PROXY + +* HTTP_PROXY_HOST: HTTP proxy host +* HTTP_PROXY_PORT: HTTP proxy port +* HTTPS_PROXY_HOST: HTTPS proxy host +* HTTPS_PROXY_PORT: HTTPS proxy port diff --git a/docker/config/application.tmpl b/docker/config/application.tmpl index 45110f4b688d291c030c1e72df085f40bae34c8b..911af6200bda7587d08d1543ad269d969fba3c81 100644 --- a/docker/config/application.tmpl +++ b/docker/config/application.tmpl @@ -21,7 +21,6 @@ spring: username: {{ default .Env.FEATURES_DB_USER "postgres" }} password: {{ .Env.FEATURES_DB_PASSWORD }} driver-class-name: org.postgresql.Driver - datasets: {{ .Env.DATASETS }} jpa: hibernate: dialect: org.hibernate.dialect.PostgreSQL9Dialect diff --git a/docker/run.sh b/docker/run.sh index 2b1ca22d1c42ceccd4d8d6fe9ac40e363f729a43..36a069dc7bc2abcbe1391a3ef1d56007ab4e8896 100755 --- a/docker/run.sh +++ b/docker/run.sh @@ -10,4 +10,16 @@ fi if [ ! -z "$FEATURES_DB_SERVER" ]; then OPTS="$OPTS -wait tcp://$FEATURES_DB_SERVER -timeout 60s" fi -dockerize $OPTS java -jar /usr/share/jars/portal-backend.jar +if [ ! -z "$HTTP_PROXY_HOST" ]; then + JAVA_OPTIONS="$JAVA_OPTIONS -Dhttp.proxyHost=$HTTP_PROXY_HOST" +fi +if [ ! -z "$HTTP_PROXY_PORT" ]; then + JAVA_OPTIONS="$JAVA_OPTIONS -Dhttp.proxyPort=$HTTP_PROXY_PORT" +fi +if [ ! -z "$HTTPS_PROXY_HOST" ]; then + JAVA_OPTIONS="$JAVA_OPTIONS -Dhttps.proxyHost=$HTTPS_PROXY_HOST" +fi +if [ ! -z "$HTTPS_PROXY_PORT" ]; then + JAVA_OPTIONS="$JAVA_OPTIONS -Dhttps.proxyPort=$HTTPS_PROXY_PORT" +fi +dockerize $OPTS java ${JAVA_OPTIONS} -jar /usr/share/jars/portal-backend.jar diff --git a/hbp.yml b/hbp.yml index 95e575825136d6ba677d02bc320eaf7f57ac0371..e01490c0f87191ff2af7e0945146fba41c165875 100644 --- a/hbp.yml +++ b/hbp.yml @@ -58,9 +58,9 @@ testing: command: ./test.sh release_management: - current_version: 2.6.2 - current_code_release: https://github.com/HBPMedical/portal-backend/archive/2.6.2.zip - current_binary_release: https://pypi.python.org/pypi/portal-backend/2.6.2 + current_version: 2.6.8 + current_code_release: https://github.com/HBPMedical/portal-backend/archive/2.6.8.zip + current_binary_release: https://pypi.python.org/pypi/portal-backend/2.6.8 release_script: 'publish.sh' continuous_integration: @@ -75,10 +75,10 @@ continuous_integration: distribution: docker_hub: name: hbpmip/portal-backend - current_tag: 2.6.2 + current_tag: 2.6.8 url: https://hub.docker.com/r/hbpmip/portal-backend/ badge: https://img.shields.io/badge/docker-hbpmip%2Fportal--backend-008bb8.svg - command: docker pull hbpmip/portal-backend:2.6.2 + command: docker pull hbpmip/portal-backend:2.6.8 planning: github: diff --git a/pom.xml b/pom.xml index 3f71142f10524f829b335c825c5760dc5ad9d8eb..4c827b642da5b1e4aa48c6aaba539084b1b944dc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ <groupId>eu.hbp.mip</groupId> <artifactId>portal-backend</artifactId> - <version>2.6.2</version><!-- BUMP_VERSION --> + <version>2.6.8</version><!-- BUMP_VERSION --> <packaging>jar</packaging> <name>portal-backend</name> @@ -47,7 +47,7 @@ <spring-data-jpa.version>1.10.11.RELEASE</spring-data-jpa.version> <spring-boot-starter-actuator.version>1.4.7.RELEASE</spring-boot-starter-actuator.version> <aspectjweaver.version>1.8.9</aspectjweaver.version> - <woken-messages.version>2.4.9</woken-messages.version> + <woken-messages.version>2.6.3</woken-messages.version> <javax-inject.version>1</javax-inject.version> <akka.version>2.5.9</akka.version> <spring-context.version>4.3.4.RELEASE</spring-context.version> diff --git a/src/main/java/eu/hbp/mip/StartupTasks.java b/src/main/java/eu/hbp/mip/StartupTasks.java new file mode 100644 index 0000000000000000000000000000000000000000..a72e119529fb6fcd497b8ad90fc0b60d4dd7c14a --- /dev/null +++ b/src/main/java/eu/hbp/mip/StartupTasks.java @@ -0,0 +1,70 @@ +package eu.hbp.mip; + +import ch.chuv.lren.woken.messages.datasets.Dataset; +import com.google.gson.Gson; +import eu.hbp.mip.controllers.DatasetsApi; +import eu.hbp.mip.controllers.MiningApi; +import eu.hbp.mip.controllers.VariablesApi; +import eu.hbp.mip.model.Algorithm; +import eu.hbp.mip.model.MiningQuery; +import eu.hbp.mip.model.Variable; +import eu.hbp.mip.repositories.VariableRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.Collections; + +@Component +public class StartupTasks implements ApplicationListener<ApplicationReadyEvent> { + + private static final Logger LOGGER = LoggerFactory.getLogger(StartupTasks.class); + private static final Gson gson = new Gson(); + + @Autowired + private VariableRepository variableRepository; + + @Autowired + private DatasetsApi datasetsApi; + + @Autowired + private VariablesApi variablesApi; + + @Autowired + private MiningApi miningApi; + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + // Pre-fill the local variable repository with the list of datasets, interpreted here as variables + // (a bit like a categorical variable can be split into a set of variables (a.k.a one hot encoding in Data science) ) + try { + for (Dataset dataset: datasetsApi.fetchDatasets()) { + final String code = dataset.dataset().code(); + Variable v = variableRepository.findOne(code); + if (v == null) { + v = new Variable(code); + variableRepository.save(v); + } + } + } catch (Exception e) { + LOGGER.error("Cannot initialise the variable repository. Is the connection to Woken working?", e); + } + + /* + for (String variableJson: variablesApi.loadVariables()) { + String code = gson.fromJson(variableJson, Variable.class).getCode(); + MiningQuery histogram = new MiningQuery(); + histogram.setAlgorithm(new Algorithm("histogram", "histogram", false)); + histogram.setVariables(Collections.singletonList(new Variable(code))); + histogram.setCovariables(Collections.emptyList()); + histogram.setGrouping(Collections.emptyList()); + // TODO: need to get groupings from Woken + } + */ + + LOGGER.info("MIP Portal backend is ready!"); + } +} diff --git a/src/main/java/eu/hbp/mip/akka/WokenClientController.java b/src/main/java/eu/hbp/mip/akka/WokenClientController.java index 3cc463857d927b17e4210cce5013eac638939a28..2504d6468feaad808d19fffa52f29da4baeac77c 100644 --- a/src/main/java/eu/hbp/mip/akka/WokenClientController.java +++ b/src/main/java/eu/hbp/mip/akka/WokenClientController.java @@ -31,7 +31,7 @@ import java.util.function.Function; */ public abstract class WokenClientController { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + protected final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); @Autowired private ActorSystem actorSystem; @@ -44,6 +44,7 @@ public abstract class WokenClientController { private ActorRef wokenClient; + @SuppressWarnings("unused") @PostConstruct public void initClusterClient() { LOGGER.info("Start Woken client " + wokenReceptionistPath); @@ -56,7 +57,8 @@ public abstract class WokenClientController { return Collections.singleton(ActorPaths.fromString(wokenReceptionistPath)); } - protected <A, B> ResponseEntity askWoken(A message, int waitInSeconds, Function<B, ResponseEntity> handleResponse) { + @SuppressWarnings("unchecked") + protected <A, B> B askWoken(A message, int waitInSeconds) throws Exception { LOGGER.info("Akka is trying to reach remote " + wokenPath); ClusterClient.Send queryMessage = new ClusterClient.Send(wokenPath, message, true); @@ -64,35 +66,29 @@ public abstract class WokenClientController { Future<Object> future = Patterns.ask(wokenClient, queryMessage, timeout); - B result; + return (B) Await.result(future, timeout.duration()); + } + + protected <A, B> ResponseEntity requestWoken(A message, int waitInSeconds, Function<B, ResponseEntity> handleResponse) { try { - result = (B) Await.result(future, timeout.duration()); + B result = askWoken(message, waitInSeconds); + return handleResponse.apply(result); } catch (Exception e) { - LOGGER.error("Cannot receive algorithm result from woken: " + e.getMessage(), e); - return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build(); + final String msg = "Cannot receive result from woken: " + e.getMessage(); + LOGGER.error(msg, e); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(msg); } - - return handleResponse.apply(result); } - protected <A extends Query> ResponseEntity askWokenQuery(A query, int waitInSeconds, Function<QueryResult, ResponseEntity> handleResponse) { - LOGGER.info("Akka is trying to reach remote " + wokenPath); - - ClusterClient.Send queryMessage = new ClusterClient.Send(wokenPath, query, true); - Timeout timeout = new Timeout(Duration.create(waitInSeconds, "seconds")); - - Future<Object> future = Patterns.ask(wokenClient, queryMessage, timeout); - - QueryResult result; try { - result = (QueryResult) Await.result(future, timeout.duration()); + QueryResult result = askWoken(query, waitInSeconds); + return handleResponse.apply(result); } catch (Exception e) { - LOGGER.error("Cannot receive algorithm result from woken: " + e.getMessage(), e); - return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build(); + final String msg = "Cannot receive algorithm result from woken: " + e.getMessage(); + LOGGER.error(msg, e); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(msg); } - - return handleResponse.apply(result); } protected <A extends Query> Future<Object> sendWokenQuery(A query, int timeout) { @@ -103,11 +99,6 @@ public abstract class WokenClientController { return Patterns.ask(wokenClient, queryMessage, timeout); } - protected ActorRef createActor(String actorBeanName, String actorName) { - return actorSystem.actorOf(SpringExtension.SPRING_EXTENSION_PROVIDER.get(actorSystem) - .props(actorBeanName), actorName); - } - protected ExecutionContext getExecutor() { return actorSystem.dispatcher(); } diff --git a/src/main/java/eu/hbp/mip/controllers/DatasetsApi.java b/src/main/java/eu/hbp/mip/controllers/DatasetsApi.java index 052486a5e47d840b21511c1279310ab2a31d6685..99d5b8497cd9fad5b2843b1621293e26d5f6dfe8 100644 --- a/src/main/java/eu/hbp/mip/controllers/DatasetsApi.java +++ b/src/main/java/eu/hbp/mip/controllers/DatasetsApi.java @@ -5,21 +5,27 @@ package eu.hbp.mip.controllers; -import com.google.gson.Gson; -import eu.hbp.mip.model.Dataset; -import eu.hbp.mip.model.Variable; -import eu.hbp.mip.repositories.VariableRepository; +import ch.chuv.lren.woken.messages.datasets.Dataset; +import ch.chuv.lren.woken.messages.datasets.DatasetsQuery; +import ch.chuv.lren.woken.messages.datasets.DatasetsResponse; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import eu.hbp.mip.akka.WokenClientController; +import eu.hbp.mip.model.DatasetDescription; import io.swagger.annotations.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import scala.Option; -import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -27,35 +33,40 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @RestController @RequestMapping(value = "/datasets", produces = {APPLICATION_JSON_VALUE}) @Api(value = "/datasets", description = "the datasets API") -public class DatasetsApi { +public class DatasetsApi extends WokenClientController { private static final Logger LOGGER = LoggerFactory.getLogger(DatasetsApi.class); - private static final Gson gson = new Gson(); - - @Autowired - private VariableRepository variableRepository; - - @Value("#{'${spring.featuresDatasource.datasets:adni,ppmi,edsd}'}") - private String datasets; - - @PostConstruct - public void init() { - for (String dataset: datasets.split(",")) { - Variable v = variableRepository.findOne(dataset); - if (v == null) { - v = new Variable(dataset); - variableRepository.save(v); - } - } - } @ApiOperation(value = "Get dataset list", response = Dataset.class, responseContainer = "List") @RequestMapping(method = RequestMethod.GET) + @Cacheable(value = "datasets") public ResponseEntity getDatasets( ) { - LOGGER.info("Get dataset list"); + LOGGER.info("Get list of datasets"); + + try { + List<DatasetDescription> datasets = new ArrayList<>(); + for (Dataset d: fetchDatasets()) { + DatasetDescription dataset = new DatasetDescription(); + LOGGER.info("Dataset {}", d); + dataset.setCode(d.dataset().code()); + dataset.setLabel(d.label()); + dataset.setDescription(d.description()); + dataset.setAnonymisationLevel(d.anonymisationLevel().toString()); + datasets.add(dataset); + } + + return ResponseEntity.ok(datasets); + } catch (Exception e) { + final String msg = "Cannot receive datasets from woken: " + e.getMessage(); + LOGGER.error(msg, e); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(msg); + } - return ResponseEntity.ok(datasets.split(",")); } + public List<ch.chuv.lren.woken.messages.datasets.Dataset> fetchDatasets() throws Exception { + DatasetsResponse result = askWoken(new DatasetsQuery(Option.empty()), 30); + return new ArrayList<>(scala.collection.JavaConversions.asJavaCollection(result.datasets())); + } } diff --git a/src/main/java/eu/hbp/mip/controllers/MethodsApi.java b/src/main/java/eu/hbp/mip/controllers/MethodsApi.java index f25187d58f31ff93087a0c6cf2d6031e5aa92525..b275f889d2f955a8b52cdf25dde3696789a0274c 100644 --- a/src/main/java/eu/hbp/mip/controllers/MethodsApi.java +++ b/src/main/java/eu/hbp/mip/controllers/MethodsApi.java @@ -35,11 +35,11 @@ public class MethodsApi extends WokenClientController { public ResponseEntity listAvailableMethodsAndValidations() { LOGGER.info("List available methods and validations"); - return askWoken(MethodsQuery$.MODULE$, 10, r -> { + return requestWoken(MethodsQuery$.MODULE$, 10, r -> { MethodsResponse result = (MethodsResponse) r; // >> Temporary : should return result.methods() in the future - JsonObject catalog = new JsonParser().parse(result.methods()).getAsJsonObject(); + JsonObject catalog = new JsonParser().parse(result.methods().compactPrint()).getAsJsonObject(); InputStream is = MethodsApi.class.getClassLoader().getResourceAsStream(EXAREME_ALGO_JSON_FILE); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); diff --git a/src/main/java/eu/hbp/mip/controllers/MiningApi.java b/src/main/java/eu/hbp/mip/controllers/MiningApi.java index 14dda0f5ad3b7b0b8cc622359485e882c7b49819..655d159d09dbf6554efdc1d2e197a30e547d84a4 100644 --- a/src/main/java/eu/hbp/mip/controllers/MiningApi.java +++ b/src/main/java/eu/hbp/mip/controllers/MiningApi.java @@ -44,7 +44,10 @@ public class MiningApi extends WokenClientController { public String queryUrl; @ApiOperation(value = "Run an algorithm", response = String.class) - @Cacheable(value = "mining", condition = "#query != null and #query.getAlgorithm().getCode() == 'histograms'", key = "#query.toString()", unless = "#result.getStatusCode().value()!=200") + @Cacheable(value = "mining", + condition = "#query != null and (#query.getAlgorithm().getCode() == 'histograms' or #query.getAlgorithm().getCode() == 'histograms')", + key = "#query.toString()", + unless = "#result.getStatusCode().value()!=200") @RequestMapping(method = RequestMethod.POST) public ResponseEntity runAlgorithm(@RequestBody eu.hbp.mip.model.MiningQuery query) { LOGGER.info("Run an algorithm"); diff --git a/src/main/java/eu/hbp/mip/controllers/VariablesApi.java b/src/main/java/eu/hbp/mip/controllers/VariablesApi.java index c55cec4b05e2334b45df5e946ce96f598a8f1672..1c284f1bbb406e3907ee28888d88aa250757ad5d 100644 --- a/src/main/java/eu/hbp/mip/controllers/VariablesApi.java +++ b/src/main/java/eu/hbp/mip/controllers/VariablesApi.java @@ -166,7 +166,7 @@ public class VariablesApi { } - private List<String> loadVariables() { + public List<String> loadVariables() { String sqlQuery = String.format( "SELECT * FROM meta_variables where upper(target_table)='%s'", featuresMainTable.toUpperCase()); SqlRowSet data = metaJdbcTemplate.queryForRowSet(sqlQuery); diff --git a/src/main/java/eu/hbp/mip/model/DatasetDescription.java b/src/main/java/eu/hbp/mip/model/DatasetDescription.java new file mode 100644 index 0000000000000000000000000000000000000000..c77c50aeab3a4256ecadf78aa2f25f89a05a0b92 --- /dev/null +++ b/src/main/java/eu/hbp/mip/model/DatasetDescription.java @@ -0,0 +1,46 @@ +package eu.hbp.mip.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.annotations.ApiModel; + +@ApiModel +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DatasetDescription { + + private String code; + private String label; + private String description; + private String anonymisationLevel; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getAnonymisationLevel() { + return anonymisationLevel; + } + + public void setAnonymisationLevel(String anonymisationLevel) { + this.anonymisationLevel = anonymisationLevel; + } +} diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 23a49772785dc1100acb9ab6230c082ff841b80c..711776afa4ac564da86946c61b2af8c3b6c0f104 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -14,17 +14,6 @@ akka { event-stream = off } - serializers { - query-result-serializer = "ch.chuv.lren.woken.messages.query.QueryResultSerializer" - } - - serialization-bindings { - "ch.chuv.lren.woken.messages.query.QueryResult" = query-result-serializer - } - enable-additional-serialization-bindings = off - allow-java-serialization = on - warn-about-java-serializer-usage = on - } remote { diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 99c80d8e2bd87822115ae8e4387867adce99bd70..881353667065f7c1994068560e71516021dc1ab2 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -43,7 +43,7 @@ services: - db meta_db_setup: - image: "hbpmip/sample-meta-db-setup:0.4.3" + image: "hbpmip/sample-meta-db-setup:0.5.1" container_name: "meta-db-setup" restart: "no" networks: @@ -59,7 +59,7 @@ services: - db sample_db_setup: - image: "hbpmip/sample-data-db-setup:0.5.0" + image: "hbpmip/sample-data-db-setup:0.5.1" container_name: "data-db-setup" restart: "no" networks: