diff --git a/pom.xml b/pom.xml index 780a1c14..053c4d5b 100644 --- a/pom.xml +++ b/pom.xml @@ -35,8 +35,8 @@ UTF-8 17 - 3.3.1 - 1.19.8 + 3.3.3 + 1.20.1 more-project @@ -84,7 +84,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + 3.3.1 org.apache.maven.plugins @@ -408,7 +408,7 @@ co.elastic.clients elasticsearch-java - 8.13.4 + 8.14.3 com.google.firebase diff --git a/studymanager-core/pom.xml b/studymanager-core/pom.xml index 22bbcd51..3b896f55 100644 --- a/studymanager-core/pom.xml +++ b/studymanager-core/pom.xml @@ -20,7 +20,7 @@ org.slf4j slf4j-api - 2.0.13 + 2.0.16 diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/component/Observation.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/component/Observation.java index 2573d5d4..2f80b759 100644 --- a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/component/Observation.java +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/component/Observation.java @@ -9,8 +9,11 @@ package io.redlink.more.studymanager.core.component; import io.redlink.more.studymanager.core.exception.ConfigurationValidationException; +import io.redlink.more.studymanager.core.io.TimeRange; import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.core.sdk.MoreObservationSDK; +import io.redlink.more.studymanager.core.ui.DataView; +import io.redlink.more.studymanager.core.ui.DataViewInfo; public abstract class Observation extends Component { @@ -20,6 +23,29 @@ protected Observation(MoreObservationSDK sdk, C properties) throws Configuration this.sdk = sdk; } + /** + * Gets an array of DataViewInfo, which represents all possibilities to show this observation data. + * + * @return an array of DataViewInfo representing all possible views of the observation data + */ + public DataViewInfo[] listViews() { + return new DataViewInfo[0]; + } + + /** + * Retrieves a specific DataView based on the given parameters. + * + * @param viewName the name of the view to retrieve + * @param studyGroupId the ID of the study group for filter reasons + * @param participantId the ID of the participant for filter reasons + * @param timerange the time range for the data view for filter reasons + * @return the requested DataView, or null if not found + */ + public DataView getView(String viewName, Integer studyGroupId, Integer participantId, TimeRange timerange) { + return null; + } + + @Override public void activate() { // no action diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/exception/ConfigurationValidationException.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/exception/ConfigurationValidationException.java index 180cbfbc..dbea0c4c 100644 --- a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/exception/ConfigurationValidationException.java +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/exception/ConfigurationValidationException.java @@ -9,9 +9,13 @@ package io.redlink.more.studymanager.core.exception; import io.redlink.more.studymanager.core.validation.ConfigurationValidationReport; +import io.redlink.more.studymanager.core.validation.ValidationIssue; +import java.util.List; public class ConfigurationValidationException extends RuntimeException { + private final ConfigurationValidationReport report; + public ConfigurationValidationException(ConfigurationValidationReport report) { this.report = report; } @@ -24,4 +28,20 @@ public ConfigurationValidationReport getReport() { public String getMessage() { return report.toString(); } + + public static ConfigurationValidationException of(ConfigurationValidationReport report) { + return new ConfigurationValidationException(report); + } + + public static ConfigurationValidationException of(ValidationIssue issue) { + return of(ConfigurationValidationReport.of(issue)); + } + + public static ConfigurationValidationException of(List issues) { + return of(ConfigurationValidationReport.of(issues)); + } + + public static ConfigurationValidationException ofError(String message) { + return of(ConfigurationValidationReport.ofError(message)); + } } diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/io/SimpleParticipant.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/io/SimpleParticipant.java index 08628dee..06e1c85e 100644 --- a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/io/SimpleParticipant.java +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/io/SimpleParticipant.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.core.io; import java.time.Instant; diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/measurement/MeasurementSet.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/measurement/MeasurementSet.java index 6e352079..ebebb96e 100644 --- a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/measurement/MeasurementSet.java +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/measurement/MeasurementSet.java @@ -15,7 +15,7 @@ public record MeasurementSet(String id, Set values) { public MeasurementSet { if (id == null || values == null) { - throw new IllegalArgumentException("Is and values must not be null"); + throw new IllegalArgumentException("Id and values must not be null"); } } } diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/IntegerValue.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/IntegerValue.java index 4d55f688..31aa55b4 100644 --- a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/IntegerValue.java +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/IntegerValue.java @@ -30,11 +30,11 @@ public Class getValueType() { } @Override - public ValidationIssue validate(Integer integer) { - if(integer != null && (integer < getMin() || integer > getMax())) { + public ValidationIssue doValidate(Integer integer) { + if (integer != null && (integer < getMin() || integer > getMax())) { return ValidationIssue.error(this, "Value must between " + getMin() + " and " + getMax()); } - return validationFunction != null ? validationFunction.apply(integer) : ValidationIssue.NONE; + return ValidationIssue.NONE; } public int getMin() { diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/StringValue.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/StringValue.java index 6dbeb02b..b9981913 100644 --- a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/StringValue.java +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/StringValue.java @@ -8,11 +8,21 @@ */ package io.redlink.more.studymanager.core.properties.model; +import io.redlink.more.studymanager.core.validation.ValidationIssue; + public class StringValue extends Value { public StringValue(String id) { super(id); } + @Override + protected ValidationIssue doValidate(String s) { + if (isRequired() && (s == null || s.trim().isEmpty())) { + return ValidationIssue.requiredMissing(this); + } + return super.doValidate(s); + } + @Override public String getType() { return "STRING"; diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/Value.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/Value.java index a000455e..2fa6b85b 100644 --- a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/Value.java +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/properties/model/Value.java @@ -13,35 +13,54 @@ import io.redlink.more.studymanager.core.exception.ValueNonNullException; import io.redlink.more.studymanager.core.properties.ComponentProperties; import io.redlink.more.studymanager.core.validation.ValidationIssue; - import java.util.function.Function; public abstract class Value { - private String id; + private final String id; private String name; private String description; private T defaultValue; private boolean required = false; private boolean immutable = false; - protected Function validationFunction = (T t) -> null; + private Function validationFunction = (T t) -> ValidationIssue.NONE; public Value(String id) { this.id = id; } - public ValidationIssue validate(T t) { - return validationFunction != null ? validationFunction.apply(t) : ValidationIssue.NONE; + public final ValidationIssue validate(T t) { + if (required) { + if (t == null) { + return ValidationIssue.requiredMissing(this); + } + } + + final ValidationIssue subResult = doValidate(t); + if (subResult != null && subResult.getType() != ValidationIssue.Type.None) { + return subResult; + } + + final ValidationIssue result = validationFunction.apply(t); + if (result == null) { + return ValidationIssue.NONE; + } else { + return result; + } + } + + protected ValidationIssue doValidate(T t) { + return ValidationIssue.NONE; } public T getValue(ComponentProperties properties) { - if(properties.containsKey(id)) { + if (properties.containsKey(id)) { try { return getValueType().cast(properties.get(id)); } catch (ClassCastException e) { throw new ValueCastException(this, getValueType()); } } else { - if(required && defaultValue == null) { + if (required && defaultValue == null) { throw new ValueNonNullException(this); } else { return defaultValue; @@ -57,6 +76,7 @@ public T getValue(ComponentProperties properties) { public String getId() { return id; } + public String getName() { return name; } diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/sdk/MoreObservationSDK.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/sdk/MoreObservationSDK.java index f24fb5b6..3e1c51dc 100644 --- a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/sdk/MoreObservationSDK.java +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/sdk/MoreObservationSDK.java @@ -8,7 +8,11 @@ */ package io.redlink.more.studymanager.core.sdk; +import io.redlink.more.studymanager.core.io.TimeRange; import io.redlink.more.studymanager.core.properties.ObservationProperties; +import io.redlink.more.studymanager.core.ui.DataViewData; +import io.redlink.more.studymanager.core.ui.DataViewRow; +import io.redlink.more.studymanager.core.ui.ViewConfig; import java.util.Map; import java.util.Optional; @@ -22,5 +26,7 @@ public interface MoreObservationSDK extends MorePlatformSDK { void storeDataPoint(Integer participantId, String observationType, Map data); + DataViewData queryData(ViewConfig viewConfig, Integer studyGroupId, Integer participantId, TimeRange timerange); + int getObservationId(); } diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataView.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataView.java new file mode 100644 index 00000000..5f94bb0b --- /dev/null +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataView.java @@ -0,0 +1,32 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ +package io.redlink.more.studymanager.core.ui; + +/** + * Represents a data view with information, chart type, and data. + * + * @param viewInfo the information about the data view + * @param chartType the type of chart to be displayed + * @param data the data to be displayed in the view + */ +public record DataView( + DataViewInfo viewInfo, + ChartType chartType, + DataViewData data +) { + /** + * Enumeration of possible chart types. + */ + public enum ChartType { + LINE, + BAR, + PIE + } +} + diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataViewData.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataViewData.java new file mode 100644 index 00000000..472ca383 --- /dev/null +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataViewData.java @@ -0,0 +1,23 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ +package io.redlink.more.studymanager.core.ui; + +import java.util.List; + +/** + * Represents the data in a data view. + * + * @param labels the labels for the data + * @param rows the rows of data + */ +public record DataViewData( + List labels, + List rows +) { +} diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataViewInfo.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataViewInfo.java new file mode 100644 index 00000000..25bf45a0 --- /dev/null +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataViewInfo.java @@ -0,0 +1,51 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ +package io.redlink.more.studymanager.core.ui; + +public interface DataViewInfo { + /** + * Gets the name of the data view. + * The name is the identifier of a specific view. + * + * @return the name of the data view + */ + String name(); + + /** + * Gets the label of the data view. + * The label is a short indicator to differ between multiple views. + * + * @return the label of the data view + */ + String label(); + + /** + * Gets the title of the data view. + * The title is a short textual information about the observation data. + * + * @return the title of the data view + */ + String title(); + + /** + * Gets the description of the data view. + * The description explains what the given observation data shows. + * + * @return the description of the data view + */ + String description(); + + /** + * Gets the chart type of the data view. + * The chartType indicates how the given observation data is visually shown. + * + * @return the chart type of the data view + */ + DataView.ChartType chartType(); +} \ No newline at end of file diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataViewRow.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataViewRow.java new file mode 100644 index 00000000..f3ecf8fd --- /dev/null +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/DataViewRow.java @@ -0,0 +1,23 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ +package io.redlink.more.studymanager.core.ui; + +import java.util.List; + +/** + * Represents a row of data in a data view. + * + * @param label the label of the data row + * @param values the list of values in the data row + */ +public record DataViewRow( + String label, + List values +) { +} diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/ViewConfig.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/ViewConfig.java new file mode 100644 index 00000000..5d020711 --- /dev/null +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/ui/ViewConfig.java @@ -0,0 +1,77 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ +package io.redlink.more.studymanager.core.ui; + +import java.util.List; + +/** + * Represents the configuration of a data view. + * + * @param filters the list of filters applied to the data view + * @param rowAggregation the aggregation method for rows. In most cases, this will be the data-legend in the view/chart. + * @param seriesAggregation the aggregation method for series. In most cases, this will result in the x-axis of the view/chart + * @param operation the operation applied to the data + */ +public record ViewConfig( + List filters, + Aggregation rowAggregation, + Aggregation seriesAggregation, + Operation operation +) { + /** + * Represents a filter applied to the data view. + */ + public record Filter() { } + + /** + * Enumeration of possible aggregation/grouping methods. + */ + public enum Aggregation { + /** + * Group by timestamp of the observation + */ + TIME, + /** + * Group by Study-Group + */ + STUDY_GROUP, + /** + * Group by Participant + */ + PARTICIPANT, + /** + * Group by a String-Value Field of the Observation, in most cases should be used together with + * the {@link Operator#COUNT COUNT}-operator + */ + TERM_FIELD, + } + + /** + * Represents an operation applied to the data. + * + * @param operator the operator to be used + * @param field the field to which the operation is applied + */ + public record Operation( + Operator operator, + String field + ) { + } + + /** + * Enumeration of possible operators. + */ + public enum Operator { + AVG, + SUM, + MIN, + MAX, + COUNT + } +} diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/validation/ConfigurationValidationReport.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/validation/ConfigurationValidationReport.java index 4d1f8feb..5c022fef 100644 --- a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/validation/ConfigurationValidationReport.java +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/validation/ConfigurationValidationReport.java @@ -16,24 +16,27 @@ public class ConfigurationValidationReport { private final List issues; private ConfigurationValidationReport(List issues) { - this.issues = issues; + this.issues = new ArrayList<>(issues); } public static ConfigurationValidationReport init() { - return new ConfigurationValidationReport(new ArrayList<>()); + return new ConfigurationValidationReport(List.of()); } public static ConfigurationValidationReport of(List issues) { - //TODO copy return new ConfigurationValidationReport(issues); } public static ConfigurationValidationReport of(ValidationIssue issue) { - return new ConfigurationValidationReport(List.of(issue)); + return of(List.of(issue)); + } + + public static ConfigurationValidationReport ofError(String message) { + return init().error(message); } public ConfigurationValidationReport error(String message) { - this.issues.add(ValidationIssue.error(null,message)); + this.issues.add(ValidationIssue.error(null, message)); return this; } @@ -47,8 +50,9 @@ public ConfigurationValidationReport missingProperty(String property) { } public boolean isValid() { - return this.listIssues().size() == 0; + return issues.stream().noneMatch(i -> i.getType() != ValidationIssue.Type.None); } + List listIssues() { return issues; } diff --git a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/validation/ValidationIssue.java b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/validation/ValidationIssue.java index 838f19f2..23644b14 100644 --- a/studymanager-core/src/main/java/io/redlink/more/studymanager/core/validation/ValidationIssue.java +++ b/studymanager-core/src/main/java/io/redlink/more/studymanager/core/validation/ValidationIssue.java @@ -9,7 +9,6 @@ package io.redlink.more.studymanager.core.validation; import io.redlink.more.studymanager.core.properties.model.Value; - import java.util.Optional; public class ValidationIssue { @@ -19,28 +18,38 @@ public enum Type { WARNING } - public static ValidationIssue NONE = new ValidationIssue(Type.None, null, null); + public static ValidationIssue NONE = new ValidationIssue(Type.None, null, null, null); - private Type type; - private String propertyId; - private String message; + private final Type type; + private final String propertyId; + private final String message; + private String componentTitle; - private ValidationIssue(Type type, String propertyId, String message) { + private ValidationIssue(Type type, String propertyId, String message, String componentTitle) { this.type = type; this.propertyId = propertyId; this.message = message; + this.componentTitle = componentTitle; } public static boolean nonNone(ValidationIssue issue) { return issue != null && Type.None != issue.type; } - public static ValidationIssue error(Value value, String message) { - return new ValidationIssue(Type.ERROR, Optional.ofNullable(value).map(Value::getId).orElse(null), message); + public static ValidationIssue error(Value value, String message) { + return new ValidationIssue(Type.ERROR, Optional.ofNullable(value).map(Value::getId).orElse(null), message, null); + } + + public static ValidationIssue warning(Value value, String message) { + return new ValidationIssue(Type.WARNING, Optional.ofNullable(value).map(Value::getId).orElse(null), message, null); + } + + public static ValidationIssue requiredMissing(Value value) { + return error(value, "global.error.required"); } - public static ValidationIssue warning(Value value, String message) { - return new ValidationIssue(Type.WARNING, Optional.ofNullable(value).map(Value::getId).orElse(null), message); + public static ValidationIssue immutablePropertyChanged(Value value) { + return error(value, "global.error.immutable"); } public Type getType() { @@ -54,4 +63,12 @@ public String getMessage() { public String getPropertyId() { return propertyId; } + + public String getComponentTitle() { + return componentTitle; + } + + public void setComponentTitle(String componentTitle) { + this.componentTitle = componentTitle; + } } diff --git a/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/DataPointQuery.java b/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/DataPointQuery.java index 7ce7b11e..8ca59eb2 100644 --- a/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/DataPointQuery.java +++ b/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/DataPointQuery.java @@ -47,7 +47,7 @@ private String getDataSelector() { if("=".equals(operator) || "==".equals(operator)) { return "data_" + observationProperty + ":" + getSanitizedPropertyValue(); } else if("!=".equals(operator)) { - return "!(data_" + observationProperty + ":" + getSanitizedPropertyValue() + ")"; + return "NOT data_" + observationProperty + ":" + getSanitizedPropertyValue(); } else if("<".equals(operator) || ">".equals(operator) || "<=".equals(operator) || ">=".equals(operator)) { return "data_" + observationProperty + ":" + operator + propertyValue; } else { diff --git a/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/QueryObject.java b/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/QueryObject.java index 3b39a0ec..71aac55d 100644 --- a/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/QueryObject.java +++ b/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/QueryObject.java @@ -34,7 +34,7 @@ public QueryObject() { public Set parameter; public String toQueryString() { - return "(" + parameter.stream().map(DataPointQuery::toQueryString).collect(Collectors.joining(" AND ")) + ")"; + return parameter.stream().map(DataPointQuery::toQueryString).collect(Collectors.joining(" AND ")); } } diff --git a/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/ScheduledDatacheckTriggerProperties.java b/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/ScheduledDatacheckTriggerProperties.java index 37ced4f9..ba0e759b 100644 --- a/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/ScheduledDatacheckTriggerProperties.java +++ b/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/datacheck/ScheduledDatacheckTriggerProperties.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import io.redlink.more.studymanager.core.properties.TriggerProperties; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -24,7 +25,7 @@ public Optional getCronSchedule() { return Optional.ofNullable(this.getString("cronSchedule")); } - public Optional> getQueryObject() {return this.getObject("queryObject", new TypeReference<>() {});} + public Optional> getQueryObject() {return this.getObject("queryObject", new TypeReference<>() {});} public Optional getWindow() { return Optional.ofNullable(this.getLong("window")); diff --git a/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/relative/RelativeTimeTrigger.java b/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/relative/RelativeTimeTrigger.java index 3a306f93..2476b44c 100644 --- a/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/relative/RelativeTimeTrigger.java +++ b/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/relative/RelativeTimeTrigger.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.component.trigger.relative; import io.redlink.more.studymanager.core.component.Trigger; @@ -10,15 +18,14 @@ import io.redlink.more.studymanager.core.sdk.MorePlatformSDK; import io.redlink.more.studymanager.core.sdk.MoreTriggerSDK; import io.redlink.more.studymanager.core.sdk.schedule.CronSchedule; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.time.Instant; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RelativeTimeTrigger extends Trigger { diff --git a/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/relative/RelativeTimeTriggerFactory.java b/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/relative/RelativeTimeTriggerFactory.java index 9b96b0c9..da8416a3 100644 --- a/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/relative/RelativeTimeTriggerFactory.java +++ b/studymanager-intervention/src/main/java/io/redlink/more/studymanager/component/trigger/relative/RelativeTimeTriggerFactory.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.component.trigger.relative; import io.redlink.more.studymanager.core.exception.ConfigurationValidationException; @@ -6,7 +14,6 @@ import io.redlink.more.studymanager.core.properties.model.IntegerValue; import io.redlink.more.studymanager.core.properties.model.Value; import io.redlink.more.studymanager.core.sdk.MoreTriggerSDK; - import java.util.List; public class RelativeTimeTriggerFactory extends TriggerFactory { diff --git a/studymanager-observation/README.md b/studymanager-observation/README.md new file mode 100644 index 00000000..6d64e315 --- /dev/null +++ b/studymanager-observation/README.md @@ -0,0 +1,41 @@ +# More Studymanager Observation + +## Data Preview + +### How to extend a given observation to show different visualisations +If you want to modify existing data views for observations or want to add a new data view for a specific observation, +you need to edit the corresponding observation file under `studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation` + +Each Observation (e.g. `PolarVerityObservation` or `QuestionObservation`) has two methods which are necessary for the data view. +- `listViews` (Returns an array of DataViewInfo objects, which represents all possibilities to show observation data) +- `getView` (Returns a specific DataView based on the given parameters) + +A `private enum DataViewInfoType` defines all possible types as DataViewInfoType and needs to be implemented in the +corresponding `*Observation.java` file. The name of an enum type is the identifier of a specific view, +which you need for the request, when you want to receive the data for this DataView. + +In the DataViewInfoType, you need to define some information about the data view: +- The translation keys for the label, title and description. These keys needs to be defined in the studymanager-frontend Repository as well (in de.json and en.json) +- The chartType (one type of DataView.ChartType enum) which represents the visual representation (e.g. Pie, Bar or Line) +- The viewConfig which defines how the data will be fetched and aggregated from elastic search + +#### ViewConfig (detailed explanation) +1. The first argument of a ViewConfig record is a List of filters, where you can additionally define filters, which are applied in the elastic query later on (not yet implemented, because currently not needed). +2. The second argument of a ViewConfig record is an Aggregation, where you can define which data points should be shown. The Aggregation can be one of the enum values (e.g. TIME, STUDY_GROUP, PARTICIPANT or TERM_FIELD). +3. The third argument of a ViewConfig record is an Aggregation as well, where you can define the aggregation for the x-axis. +4. The fourth argument of a ViewConfig record is an Operation, where you can define which aggregation function (e.g. AVG, COUNT, SUM) should be applied on which field (e.g. "hr"), which then represents the y-axis. + +#### Example (PolarVerityObservation) +When you request the API endpoint `/studies/{studyId}/observations/{observationId}/views` with the corresponding studyId and observationId (for the Polar Verity Sensor), +you'll get back a list of all possible views, in this case `["heart_rate"]`. +Now you can fetch the observation data view information with the API endpoint `/studies/{studyId}/observations/{observationId}/views/{viewName}`, where `viewName` is `heart_rate`. + +Explanation of the example screenshot below (chart in the Frontend): +- "Heart rate" shown as a Tab in the FE, is the "label" defined in `DataViewInfoType` +- "Average heart rate per part..." shown as the title of the chart, is the "title" defined in `DataViewInfoType` +- "Compares the average heart rate of all ..." shown under the title of the chart, is the "description" defined in `DataViewInfoType` +- Each participant alias is shown in the chart legend, and its query definition is defined in the `ViewConfig.rowAggregation` +- The query definition for the x-axis labels (time) is defined in the `ViewConfig.seriesAggregration` +- The values (y-axis) for each participant is the average heart rate over time and its query definition is defined in the `ViewConfig.Operation`, where you need to define the corresponding observation data (e.g. "hr") and operation type (e.g. `ViewConfig.Operator.AVG`) + +![Heart rate chart](./images/heart_rate_chart.png) \ No newline at end of file diff --git a/studymanager-observation/images/heart_rate_chart.png b/studymanager-observation/images/heart_rate_chart.png new file mode 100644 index 00000000..74929f2f Binary files /dev/null and b/studymanager-observation/images/heart_rate_chart.png differ diff --git a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/PolarVerityObservation.java b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/PolarVerityObservation.java index 2682cd3b..ea263056 100644 --- a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/PolarVerityObservation.java +++ b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/PolarVerityObservation.java @@ -10,11 +10,94 @@ import io.redlink.more.studymanager.core.component.Observation; import io.redlink.more.studymanager.core.exception.ConfigurationValidationException; +import io.redlink.more.studymanager.core.io.TimeRange; import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.core.sdk.MoreObservationSDK; +import io.redlink.more.studymanager.core.ui.DataView; +import io.redlink.more.studymanager.core.ui.DataViewData; +import io.redlink.more.studymanager.core.ui.DataViewInfo; +import io.redlink.more.studymanager.core.ui.ViewConfig; +import java.util.List; public class PolarVerityObservation extends Observation { public PolarVerityObservation(MoreObservationSDK sdk, C properties) throws ConfigurationValidationException { super(sdk, properties); } + + private enum DataViewInfoType implements DataViewInfo { + heart_rate("avgHeartRate", + DataView.ChartType.LINE, + new ViewConfig( + List.of(), + ViewConfig.Aggregation.PARTICIPANT, + ViewConfig.Aggregation.TIME, + new ViewConfig.Operation(ViewConfig.Operator.AVG, "hr") + ) + ); + + private final String label; + private final String title; + private final String description; + private final DataView.ChartType chartType; + + private final ViewConfig viewConfig; + + DataViewInfoType(String i18nKey, DataView.ChartType chartType, ViewConfig viewConfig) { + this( + "monitoring.charts.polarVerity.%s.label".formatted(i18nKey), + "monitoring.charts.polarVerity.%s.title".formatted(i18nKey), + "monitoring.charts.polarVerity.%s.description".formatted(i18nKey), + chartType, + viewConfig + ); + } + + DataViewInfoType(String label, String title, String description, DataView.ChartType chartType, ViewConfig viewConfig) { + this.label = label; + this.title = title; + this.description = description; + this.chartType = chartType; + this.viewConfig = viewConfig; + } + + @Override + public String label() { + return this.label; + } + + @Override + public String title() { + return this.title; + } + + @Override + public String description() { + return this.description; + } + + public DataView.ChartType chartType() { + return chartType; + } + + public ViewConfig getViewConfig() { + return viewConfig; + } + } + + public DataViewInfo[] listViews() { + return DataViewInfoType.values(); + } + + @Override + public DataView getView(String viewName, Integer studyGroupId, Integer participantId, TimeRange timerange) { + final DataViewInfoType dataView = DataViewInfoType.valueOf(viewName); + final DataViewData dataViewData = sdk.queryData(dataView.getViewConfig(), studyGroupId, participantId, timerange); + + return new DataView( + dataView, + dataView.chartType(), + dataViewData + ); + + } } diff --git a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/QuestionObservation.java b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/QuestionObservation.java index 82a2c992..03034340 100644 --- a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/QuestionObservation.java +++ b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/QuestionObservation.java @@ -10,12 +10,169 @@ import io.redlink.more.studymanager.core.component.Observation; import io.redlink.more.studymanager.core.exception.ConfigurationValidationException; +import io.redlink.more.studymanager.core.io.TimeRange; import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.core.sdk.MoreObservationSDK; +import io.redlink.more.studymanager.core.ui.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class QuestionObservation extends Observation { public QuestionObservation(MoreObservationSDK sdk, C properties) throws ConfigurationValidationException { super(sdk, properties); } + + private enum DataViewInfoType implements DataViewInfo { + response_distribution( + "responseDistribution", + DataView.ChartType.PIE, + new ViewConfig( + List.of(), + null, + ViewConfig.Aggregation.TERM_FIELD, + new ViewConfig.Operation(ViewConfig.Operator.COUNT, "answer") + ) + ), + answers_by_group("responseDistributionStudyGroup", + DataView.ChartType.BAR, + new ViewConfig( + List.of(), + ViewConfig.Aggregation.TERM_FIELD, + ViewConfig.Aggregation.STUDY_GROUP, + new ViewConfig.Operation(ViewConfig.Operator.COUNT, "answer") + ) + ), + group_by_answers("responseDistributionResponse", + DataView.ChartType.BAR, + new ViewConfig( + List.of(), + ViewConfig.Aggregation.STUDY_GROUP, + ViewConfig.Aggregation.TERM_FIELD, + new ViewConfig.Operation(ViewConfig.Operator.COUNT, "answer") + ) + ), + ; + + private final String label; + private final String title; + private final String description; + private final DataView.ChartType chartType; + + private final ViewConfig viewConfig; + + DataViewInfoType(String i18nKey, DataView.ChartType chartType, ViewConfig viewConfig) { + this( + "monitoring.charts.simpleQuestion.%s.label".formatted(i18nKey), + "monitoring.charts.simpleQuestion.%s.title".formatted(i18nKey), + "monitoring.charts.simpleQuestion.%s.description".formatted(i18nKey), + chartType, + viewConfig + ); + } + + DataViewInfoType(String label, String title, String description, DataView.ChartType chartType, ViewConfig viewConfig) { + this.label = label; + this.title = title; + this.description = description; + this.chartType = chartType; + this.viewConfig = viewConfig; + } + + @Override + public String label() { + return this.label; + } + + @Override + public String title() { + return this.title; + } + + @Override + public String description() { + return this.description; + } + + @Override + public DataView.ChartType chartType() { + return chartType; + } + + public ViewConfig getViewConfig() { + return viewConfig; + } + } + + @Override + public DataViewInfo[] listViews() { + return DataViewInfoType.values(); + } + + @Override + public DataView getView(String viewName, Integer studyGroupId, Integer participantId, TimeRange timerange) { + final DataViewInfoType dataView = DataViewInfoType.valueOf(viewName); + final DataViewData dataViewData = sdk.queryData(dataView.getViewConfig(), studyGroupId, participantId, timerange); + + return new DataView( + dataView, + dataView.chartType(), + addMissingAnswerOptions(dataView, dataViewData) + ); + + } + + private DataViewData addMissingAnswerOptions(DataViewInfoType dataView, DataViewData dataViewData) { + ArrayList allPossibleAnswerOptions = (ArrayList) this.properties.get("answers"); + List labels = new ArrayList<>(dataViewData.labels()); + List rows = new ArrayList<>(dataViewData.rows()); + + switch (dataView) { + case response_distribution: + if (allPossibleAnswerOptions.size() == dataViewData.labels().size() || dataViewData.labels().isEmpty()) { + return dataViewData; + } + for (String item : allPossibleAnswerOptions) { + if (!labels.contains(item)) { + labels.add(item); + if (!rows.isEmpty()) { + rows.get(0).values().add(0.0); + } + } + } + break; + case answers_by_group: + if (allPossibleAnswerOptions.size() == dataViewData.rows().size() || dataViewData.rows().isEmpty()) { + return dataViewData; + } + for (String item : allPossibleAnswerOptions) { + if (rows.stream().noneMatch(v -> v.label().equals(item))) { + ArrayList values = Stream.generate(() -> (Double) null) + .limit(labels.size()).collect(Collectors.toCollection(ArrayList::new)); + + rows.add(new DataViewRow(item, values)); + } + } + break; + case group_by_answers: + if (allPossibleAnswerOptions.size() == dataViewData.labels().size() || dataViewData.labels().isEmpty()) { + return dataViewData; + } + + for (String item : allPossibleAnswerOptions) { + if (!labels.contains(item)) { + labels.add(item); + for (DataViewRow row : rows) { + row.values().add(0.0); + } + } + } + break; + } + + return new DataViewData(labels, rows); + } } diff --git a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/QuestionObservationFactory.java b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/QuestionObservationFactory.java index 875d56cd..92064549 100644 --- a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/QuestionObservationFactory.java +++ b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/QuestionObservationFactory.java @@ -44,6 +44,7 @@ public class QuestionObservationFactory, P extends Obse "Yes" )) ); + @Override public String getId() { return "question-observation"; diff --git a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservation.java b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservation.java index 8d672ea9..63953304 100644 --- a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservation.java +++ b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservation.java @@ -13,6 +13,10 @@ import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.core.sdk.MoreObservationSDK; import io.redlink.more.studymanager.core.sdk.MorePlatformSDK; +import io.redlink.more.studymanager.core.validation.ConfigurationValidationReport; +import io.redlink.more.studymanager.core.validation.ValidationIssue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Map; import java.util.Optional; @@ -22,6 +26,7 @@ public class LimeSurveyObservation extends Obse public static final String LIME_SURVEY_ID = "limeSurveyId"; private final LimeSurveyRequestService limeSurveyRequestService; + private static final Logger LOGGER = LoggerFactory.getLogger(LimeSurveyObservation.class); public LimeSurveyObservation(MoreObservationSDK sdk, C properties, LimeSurveyRequestService limeSurveyRequestService) throws ConfigurationValidationException { super(sdk, properties); @@ -35,15 +40,15 @@ public void activate(){ Set participantIds = sdk.participantIds(MorePlatformSDK.ParticipantFilter.ALL); participantIds.removeIf(id -> sdk.getPropertiesForParticipant(id).isPresent()); limeSurveyRequestService.activateParticipants(participantIds, surveyId) - .forEach(data -> { + .forEach(data -> sdk.setPropertiesForParticipant( Integer.parseInt(data.firstname()), new ObservationProperties( Map.of("token", data.token(), "limeUrl", limeSurveyRequestService.getBaseUrl()) ) - ); - }); + ) + ); limeSurveyRequestService.setSurveyEndUrl(surveyId, sdk.getStudyId(), sdk.getObservationId()); limeSurveyRequestService.activateSurvey(surveyId); sdk.setValue(LIME_SURVEY_ID, surveyId); @@ -52,21 +57,19 @@ public void activate(){ protected String checkAndGetSurveyId() { String newSurveyId = properties.getString(LIME_SURVEY_ID); String activeSurveyId = sdk.getValue(LIME_SURVEY_ID, String.class).orElse(null); - - if(activeSurveyId != null && !activeSurveyId.equals(newSurveyId)) { - throw new RuntimeException(String.format( + if (activeSurveyId != null && !activeSurveyId.equals(newSurveyId)) { + LOGGER.error(String.format( "SurveyId on Observation %s must not be changed: %s -> %s", sdk.getObservationId(), activeSurveyId, newSurveyId )); + throw new ConfigurationValidationException(ConfigurationValidationReport.of(ValidationIssue.immutablePropertyChanged(LimeSurveyObservationFactory.limeSurveyId))); } else { return newSurveyId; } } - - @Override public void deactivate() { // for downwards compatibility (already running studies) @@ -80,21 +83,22 @@ public void deactivate() { public boolean writeDataPoints(String token, int surveyId, int savedId) { //check if token exists, get participant and answer and store as datapoint - getParticipantForToken(token).ifPresent(participantId -> { - limeSurveyRequestService.getAnswer(token, surveyId, savedId).ifPresent(m -> { - sdk.storeDataPoint(participantId, "lime-survey-observation", m); - }); - }); + getParticipantForToken(token).ifPresent(participantId -> + limeSurveyRequestService.getAnswer(token, surveyId, savedId).ifPresent(m -> + sdk.storeDataPoint(participantId, "lime-survey-observation", m) + ) + ); return true; } public Optional getParticipantForToken(String token) { - return sdk.participantIds(MorePlatformSDK.ParticipantFilter.ALL).stream().filter(id -> { - return sdk.getPropertiesForParticipant(id) - .map(o -> o.getString("token")) - .map(t -> t.equals(token)) - .orElse(false); - }).findFirst(); - + return sdk.participantIds(MorePlatformSDK.ParticipantFilter.ALL).stream() + .filter(id -> + sdk.getPropertiesForParticipant(id) + .map(o -> o.getString("token")) + .map(t -> t.equals(token)) + .orElse(false) + ) + .findFirst(); } } diff --git a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservationFactory.java b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservationFactory.java index 7aa842a1..a4d158e4 100644 --- a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservationFactory.java +++ b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyObservationFactory.java @@ -20,13 +20,18 @@ import io.redlink.more.studymanager.core.properties.model.StringValue; import io.redlink.more.studymanager.core.properties.model.Value; import io.redlink.more.studymanager.core.sdk.MoreObservationSDK; - import java.util.List; import java.util.Optional; public class LimeSurveyObservationFactory, P extends ObservationProperties> extends ObservationFactory { + public static final Value limeSurveyId = new StringValue("limeSurveyId") + .setName("observation.factory.limeSurvey.configProps.idName") + .setDescription("observation.factory.limeSurvey.configProps.idDesc") + .setRequired(true) + .setImmutable(true); + private static final List properties = List.of( /* TODO enable Autocomplete in FE new AutocompleteValue("limeSurveyId", "surveys") @@ -34,10 +39,7 @@ public class LimeSurveyObservationFactory, P .setDescription("An existing survey") .setRequired(true) */ - new StringValue("limeSurveyId") - .setName("observation.factory.limeSurvey.configProps.idName") - .setDescription("observation.factory.limeSurvey.configProps.idDesc") - .setRequired(true) + limeSurveyId ); private LimeSurveyRequestService limeSurveyRequestService; diff --git a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyRequestService.java b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyRequestService.java index 1a63b16f..4a3232f8 100644 --- a/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyRequestService.java +++ b/studymanager-observation/src/main/java/io/redlink/more/studymanager/component/observation/lime/LimeSurveyRequestService.java @@ -140,8 +140,10 @@ private List createParticipants(Set participantIds, St String rsp = client.send(request, HttpResponse.BodyHandlers.ofString()).body(); - if(rsp.contains("Error: Invalid survey ID")) { + if (rsp.contains("Error: Invalid survey ID")) { throw new RuntimeException("Invalid survey ID: " + surveyId); + } else if (rsp.contains("Invalid session key")) { + throw new RuntimeException("Connection to Limesurvey failed"); } List data = mapper.readValue( @@ -159,17 +161,24 @@ private List createParticipants(Set participantIds, St } } - private String getSessionKey(){ + private String getSessionKey() { try { HttpRequest request = createHttpRequest( parseRequest("get_session_key", List.of(properties.get("username"), properties.get("password"))) ); - return mapper.readValue( - client.send(request, HttpResponse.BodyHandlers.ofString()).body(), - LimeSurveyObjectResponse.class) - .result().toString(); - } catch(IOException | InterruptedException e){ + var rsp = client.send(request, HttpResponse.BodyHandlers.ofString()).body(); + var values = mapper.readValue(rsp, LimeSurveyObjectResponse.class); + var result = values.result().toString(); + + if (result.contains("Invalid user name or password")) { + throw new RuntimeException("Not possible to get session key for Limesurvey because of invalid credentials."); + } else if (result.contains("You have exceeded the number of maximum login attempts. Please wait 10 minutes before trying again")) { + throw new RuntimeException("Too many login attempts for Limesurvey. Try again in 10 minutes."); + } + + return result; + } catch(IOException | InterruptedException e) { LOGGER.error("Error getting session key for Limesurvey remote control"); throw new RuntimeException(e); } diff --git a/studymanager/pom.xml b/studymanager/pom.xml index 021a90bc..dce76ae4 100644 --- a/studymanager/pom.xml +++ b/studymanager/pom.xml @@ -159,7 +159,7 @@ net.logstash.logback logstash-logback-encoder - 7.4 + 8.0 runtime @@ -227,7 +227,7 @@ org.openapitools openapi-generator-maven-plugin - 6.6.0 + 7.7.0 configurator-api @@ -235,56 +235,51 @@ generate - - true - + spring + spring-boot + ${project.basedir}/src/main/resources/openapi/StudyManagerAPI.yaml ${project.build.directory}/generated-sources/study-manager + + io.redlink.more.studymanager.api.v1 + true io.redlink.more.studymanager.api.v1.webservices + true io.redlink.more.studymanager.api.v1.model + DTO + + false + true + false + false + - + string+date-time=Instant string+time=LocalTime Instant=java.time.Instant LocalTime=java.time.LocalTime + DataExport=org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody Instant=java.time.Instant LocalTime=java.time.LocalTime + + + true + src + source + true + true + false + true + api_interface + - - spring - spring-boot - - io.redlink.more.studymanager.api.v1 - - true - io.redlink.more.studymanager.api.v1.webservices - - true - io.redlink.more.studymanager.api.v1.model - DTO - - false - true - false - false - - - true - src - source - true - false - false - true - - diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/configuration/SessionConfiguration.java b/studymanager/src/main/java/io/redlink/more/studymanager/configuration/SessionConfiguration.java index 4a363524..2e4305d3 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/configuration/SessionConfiguration.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/configuration/SessionConfiguration.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.configuration; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/configuration/WebSecurityConfiguration.java b/studymanager/src/main/java/io/redlink/more/studymanager/configuration/WebSecurityConfiguration.java index 5aa7bc21..cdf54107 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/configuration/WebSecurityConfiguration.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/configuration/WebSecurityConfiguration.java @@ -11,19 +11,24 @@ import io.redlink.more.studymanager.properties.MoreAuthProperties; import io.redlink.more.studymanager.repository.UserRepository; import io.redlink.more.studymanager.service.OAuth2AuthenticationService; -import org.springframework.boot.context.properties.EnableConfigurationProperties; - import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.core.Authentication; +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.crypto.argon2.Argon2PasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; @@ -32,9 +37,6 @@ import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -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.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.OidcIdToken; @@ -50,6 +52,7 @@ import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.IpAddressMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; @Configuration @EnableWebSecurity @@ -71,20 +74,24 @@ protected SecurityFilterChain filterChain(HttpSecurity http, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, UserRepository userRepository) throws Exception { // Basics - http.csrf() + http.csrf(csrf -> csrf .ignoringRequestMatchers("/kibana/**") - .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); - http.cors().disable(); + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + ); + http.cors(AbstractHttpConfigurer::disable); // Restricted Paths - http.authorizeHttpRequests() - .requestMatchers("/api", "/api/v1/me").permitAll() + http.authorizeHttpRequests(auth -> auth + .requestMatchers(HttpMethod.GET, "/api", "/api/v1/me").permitAll() // Allow unauthenticated access to the ui-/auth-settings .requestMatchers(HttpMethod.GET, "/api/v1/config/ui").permitAll() + // Allow unauthenticated access to the build-info + .requestMatchers(HttpMethod.GET, "/api/v1/config/buildInfo").permitAll() //TODO specific handling of temporary sidecar .requestMatchers("/api/v1/components/observation/lime-survey-observation/end.html").permitAll() - .requestMatchers("/api/v1/studies/*/export/studydata/*").permitAll() - .requestMatchers("/api/v1/studies/*/calendar.ics").permitAll() + // Study-Data-Export is authenticated internally using individual access-tokens + .requestMatchers(HttpMethod.GET, "/api/v1/studies/*/export/studydata/*").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/studies/*/calendar.ics").permitAll() .requestMatchers("/api/v1/**").authenticated() .requestMatchers("/kibana/**").authenticated() .requestMatchers("/login/init").authenticated() @@ -96,30 +103,44 @@ protected SecurityFilterChain filterChain(HttpSecurity http, ) ).permitAll() .requestMatchers("/error").authenticated() - .anyRequest().denyAll(); + .anyRequest().denyAll() + ); // API-Calls should not be redirected to the login page, but answered with a 401 - http.exceptionHandling() - .defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), new AntPathRequestMatcher("/api/**")); + http.exceptionHandling(exHandling -> exHandling + .defaultAuthenticationEntryPointFor( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + new AndRequestMatcher( + new AntPathRequestMatcher("/api/**"), + new NegatedRequestMatcher( + new AntPathRequestMatcher("/api/v1/studies/*/export/studydata/*") + ) + ) + ) + ); // Logout Config - http.logout() + http.logout(logout -> logout .logoutSuccessHandler(oidcLogoutSuccessHandler()) - .logoutSuccessUrl("/"); + .logoutSuccessUrl("/") + ); // Enable OAuth2 - http.oauth2Login() + http.oauth2Login(oauth -> oauth // register oauth2-provider under this baseurl to simplify routing - .authorizationEndpoint().baseUri("/login/oauth").and() + .authorizationEndpoint(ep -> ep.baseUri("/login/oauth")) .authorizedClientService( new UserSyncingOAuth2AuthorizedClientService(oAuth2AuthorizedClientService, oAuth2AuthenticationService, userRepository) - ); + ) + ); // Enable OAuth2 client_credentials flow (insomnia) - http.oauth2ResourceServer().jwt(); + http.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults())); //TODO maybe disable in production - http.headers().frameOptions().disable(); + http.headers(headers -> headers + .frameOptions(HeadersConfigurer.FrameOptionsConfig::disable) + ); return http.build(); } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/LoginController.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/LoginController.java index 767cf89b..5474bba6 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/LoginController.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/LoginController.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.controller; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/RequiresStudyRole.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/RequiresStudyRole.java index 13e40b9a..42637178 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/RequiresStudyRole.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/RequiresStudyRole.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2023 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.controller; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/RootController.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/RootController.java index 487180d7..8780576e 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/RootController.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/RootController.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.controller; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java index 2093ad09..31d1d4ff 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CalendarApiV1Controller.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.controller.studymanager; import io.redlink.more.studymanager.api.v1.model.StudyTimelineDTO; @@ -7,8 +15,8 @@ import io.redlink.more.studymanager.model.transformer.TimelineTransformer; import io.redlink.more.studymanager.properties.GatewayProperties; import io.redlink.more.studymanager.service.CalendarService; +import java.time.Instant; import java.time.LocalDate; -import java.time.OffsetDateTime; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -38,7 +46,7 @@ public ResponseEntity getStudyCalendar(Long studyId) { @Override @RequiresStudyRole({StudyRole.STUDY_ADMIN, StudyRole.STUDY_OPERATOR}) - public ResponseEntity getStudyTimeline(Long studyId, Integer participant, Integer studyGroup, OffsetDateTime referenceDate, LocalDate from, LocalDate to) { + public ResponseEntity getStudyTimeline(Long studyId, Integer participant, Integer studyGroup, Instant referenceDate, LocalDate from, LocalDate to) { return ResponseEntity.ok( TimelineTransformer.toStudyTimelineDTO( service.getTimeline(studyId, participant, studyGroup, referenceDate, from, to) diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CollaboratorsApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CollaboratorsApiV1Controller.java index 57242f64..9ddc8f8e 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CollaboratorsApiV1Controller.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/CollaboratorsApiV1Controller.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.controller.studymanager; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ComponentApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ComponentApiV1Controller.java index f86e082d..41b33df6 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ComponentApiV1Controller.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ComponentApiV1Controller.java @@ -9,7 +9,10 @@ package io.redlink.more.studymanager.controller.studymanager; import com.fasterxml.jackson.databind.JsonNode; -import io.redlink.more.studymanager.api.v1.model.*; +import io.redlink.more.studymanager.api.v1.model.ComponentFactoryDTO; +import io.redlink.more.studymanager.api.v1.model.ComponentFactoryMeasurementsInnerDTO; +import io.redlink.more.studymanager.api.v1.model.ValidationReportDTO; +import io.redlink.more.studymanager.api.v1.model.VisibilityDTO; import io.redlink.more.studymanager.api.v1.webservices.ComponentsApi; import io.redlink.more.studymanager.core.exception.ApiCallException; import io.redlink.more.studymanager.core.exception.ConfigurationValidationException; @@ -21,19 +24,19 @@ import io.redlink.more.studymanager.core.model.User; import io.redlink.more.studymanager.core.properties.ComponentProperties; import io.redlink.more.studymanager.core.webcomponent.WebComponent; +import io.redlink.more.studymanager.model.transformer.ValidationReportTransformer; import io.redlink.more.studymanager.service.OAuth2AuthenticationService; import io.redlink.more.studymanager.utils.MapperUtils; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - @RestController @RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE) public class ComponentApiV1Controller implements ComponentsApi { @@ -73,19 +76,11 @@ public ResponseEntity validateProperties(String componentTy f.validate((ComponentProperties) MapperUtils.MAPPER.convertValue(body, f.getPropertyClass())); return new ValidationReportDTO().valid(true); } catch (ConfigurationValidationException e) { - return new ValidationReportDTO() - .valid(false) - .errors(e.getReport().getErrors().stream() - .map(i -> new ValidationReportItemDTO().message(i.getMessage()).propertyId(i.getPropertyId()).type("error")) - .toList() - ).warnings(e.getReport().getWarnings().stream() - .map(i -> new ValidationReportItemDTO().message(i.getMessage()).propertyId(i.getPropertyId()).type("warning")) - .toList() - ); + return ValidationReportTransformer.validationReportDTO_V1(e); } }) .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); + .orElseGet(() -> ResponseEntity.notFound().build()); } @Override diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ConfigurationApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ConfigurationApiV1Controller.java index 734946d8..d26dbb1c 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ConfigurationApiV1Controller.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ConfigurationApiV1Controller.java @@ -1,12 +1,19 @@ /* - * Copyright (c) 2023 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.controller.studymanager; +import io.redlink.more.studymanager.api.v1.model.BuildInfoDTO; import io.redlink.more.studymanager.api.v1.model.FrontendConfigurationDTO; import io.redlink.more.studymanager.api.v1.model.KeycloakSettingsDTO; import io.redlink.more.studymanager.api.v1.webservices.ConfigurationApi; import io.redlink.more.studymanager.properties.FrontendConfigurationProperties; +import java.time.Instant; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -24,6 +31,18 @@ public ConfigurationApiV1Controller(FrontendConfigurationProperties uiConfig) { this.uiConfig = uiConfig; } + @Override + public ResponseEntity getBuildInfo() { + return ResponseEntity.ok( + new BuildInfoDTO( + "null", + Instant.EPOCH + ) + .branch("undefined") + .rev("unknown") + ); + } + @Override public ResponseEntity getFrontendConfig() { return ResponseEntity.ok( @@ -32,13 +51,13 @@ public ResponseEntity getFrontendConfig() { } private static FrontendConfigurationDTO transform(FrontendConfigurationProperties uiConfig) { - return new FrontendConfigurationDTO() + return new FrontendConfigurationDTO( + new KeycloakSettingsDTO( + uiConfig.keycloak().server(), + uiConfig.keycloak().realm(), + uiConfig.keycloak().clientId() + )) .title(uiConfig.title()) - .auth(new KeycloakSettingsDTO() - .server(uiConfig.keycloak().server()) - .realm(uiConfig.keycloak().realm()) - .clientId(uiConfig.keycloak().clientId()) - ) ; } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/DataApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/DataApiV1Controller.java index f884d8e1..b1d0dfc2 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/DataApiV1Controller.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/DataApiV1Controller.java @@ -9,21 +9,22 @@ package io.redlink.more.studymanager.controller.studymanager; import io.redlink.more.studymanager.api.v1.model.DataPointDTO; +import io.redlink.more.studymanager.api.v1.model.ObservationDataViewDTO; +import io.redlink.more.studymanager.api.v1.model.ObservationDataViewDataDTO; import io.redlink.more.studymanager.api.v1.model.ParticipationDataDTO; import io.redlink.more.studymanager.api.v1.webservices.DataApi; import io.redlink.more.studymanager.controller.RequiresStudyRole; import io.redlink.more.studymanager.model.StudyRole; import io.redlink.more.studymanager.model.transformer.StudyDataTransformer; import io.redlink.more.studymanager.service.DataProcessingService; - +import java.time.Instant; +import java.util.Arrays; +import java.util.List; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.time.OffsetDateTime; -import java.util.List; - @RestController @RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE) public class DataApiV1Controller implements DataApi { @@ -35,7 +36,7 @@ public class DataApiV1Controller implements DataApi { @Override @RequiresStudyRole({StudyRole.STUDY_ADMIN, StudyRole.STUDY_VIEWER}) public ResponseEntity> getDataPoints( - Long studyId, Integer size, Integer observationId, Integer participantId, OffsetDateTime date + Long studyId, Integer size, Integer observationId, Integer participantId, Instant date ) { return ResponseEntity.ok().body( dataProcessingService.getDataPoints(studyId, size, observationId, participantId, date) @@ -51,4 +52,24 @@ public ResponseEntity> getParticipationData(Long stud .toList() ); } + + @Override + @RequiresStudyRole({StudyRole.STUDY_ADMIN, StudyRole.STUDY_VIEWER}) + public ResponseEntity getObservationViewData(Long studyId, Integer observationId, String viewName, Integer studyGroupId, Integer participantId, Instant from, Instant to) { + return ResponseEntity.ok().body( + StudyDataTransformer.toObservationDataViewDataDTO( + dataProcessingService.getDataView(studyId, observationId, viewName, studyGroupId, participantId, from, to) + ) + ); + } + + @Override + @RequiresStudyRole({StudyRole.STUDY_ADMIN, StudyRole.STUDY_VIEWER}) + public ResponseEntity> listObservationViews(Long studyId, Integer observationId) { + return ResponseEntity.ok().body( + Arrays.stream(dataProcessingService.listDataViews(studyId, observationId)) + .map(StudyDataTransformer::toObservationDataViewDTO) + .toList() + ); + } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ExceptionControllerAdvice.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ExceptionControllerAdvice.java new file mode 100644 index 00000000..5f95bc96 --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ExceptionControllerAdvice.java @@ -0,0 +1,29 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ +package io.redlink.more.studymanager.controller.studymanager; + +import io.redlink.more.studymanager.api.v1.model.ValidationReportDTO; +import io.redlink.more.studymanager.core.exception.ConfigurationValidationException; +import io.redlink.more.studymanager.model.transformer.ValidationReportTransformer; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice(basePackageClasses = ExceptionControllerAdvice.class) +public class ExceptionControllerAdvice { + + @ExceptionHandler(ConfigurationValidationException.class) + public ResponseEntity handleConfigurationValidationException(ConfigurationValidationException e) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .header("Error-Message", e.getMessage()) + .body(ValidationReportTransformer.validationReportDTO_V1(e)); + } + +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ImportExportApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ImportExportApiV1Controller.java index af8a46dc..c5978075 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ImportExportApiV1Controller.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/ImportExportApiV1Controller.java @@ -21,20 +21,29 @@ import io.redlink.more.studymanager.service.ImportExportService; import io.redlink.more.studymanager.service.OAuth2AuthenticationService; import io.redlink.more.studymanager.utils.MapperUtils; -import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.io.IOException; +import java.time.Instant; +import java.util.List; import java.util.Optional; @RestController @RequestMapping(value = "/api/v1") public class ImportExportApiV1Controller implements ImportExportApi { + private static final Logger LOGGER = LoggerFactory.getLogger(StudyApiV1Controller.class); private final ImportExportService service; @@ -84,28 +93,38 @@ public ResponseEntity exportStudy(Long studyId) { } @Override - @RequiresStudyRole({StudyRole.STUDY_ADMIN, StudyRole.STUDY_OPERATOR}) - public ResponseEntity generateDownloadToken(Long studyId) { - return ResponseEntity.ok(new GenerateDownloadToken200ResponseDTO().token( - tokenRepository.createToken(studyId).getToken() - )); - } - - @RequestMapping( - method = RequestMethod.GET, - value = "/studies/{studyId}/export/studydata/{token}", - produces = { "application/json" } - ) - public void exportStudyData(@PathVariable Long studyId, @PathVariable("token") String token, HttpServletResponse response) throws IOException { + public ResponseEntity exportStudyData(Long studyId, String token, List studyGroupId, List participantId, List observationId, Instant from, Instant to) { Optional dt = tokenRepository.getToken(token).filter(t -> t.getStudyId().equals(studyId)); - if(dt.isPresent()) { - response.setHeader("Content-Disposition", "attachment;filename=" + dt.get().getFilename()); - service.exportStudyData(response.getOutputStream(), studyId); + + if (dt.isPresent()) { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.set("Content-Disposition", "attachment;filename=" + dt.get().getFilename()); + + return ResponseEntity + .ok() + .headers(responseHeaders) + .contentType(MediaType.APPLICATION_JSON) + .body(outputStream -> { + try { + service.exportStudyData(outputStream, studyId, studyGroupId, participantId, observationId, from, to); + } catch (Exception e) { + LOGGER.warn("Error exporting study data for study_{}: {}", studyId, e.getMessage(), e); + } + }); } else { - response.setStatus(403); + return ResponseEntity.notFound().build(); } } + @Override + @RequiresStudyRole({StudyRole.STUDY_ADMIN, StudyRole.STUDY_OPERATOR}) + public ResponseEntity generateDownloadToken(Long studyId, List studyGroupId, List participantId, List observationId, Instant from, Instant to) { + var token = tokenRepository.createToken(studyId).getToken(); + var uri = ServletUriComponentsBuilder.fromCurrentRequest().pathSegment(token).build(true).toUri(); + + return ResponseEntity.created(uri).body(new GenerateDownloadToken200ResponseDTO().token(token)); + } + @Override public ResponseEntity importStudy(MultipartFile file) { try { diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/StudyApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/StudyApiV1Controller.java index 74e0edf4..154b986e 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/StudyApiV1Controller.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/StudyApiV1Controller.java @@ -17,6 +17,7 @@ import io.redlink.more.studymanager.model.transformer.StudyTransformer; import io.redlink.more.studymanager.service.OAuth2AuthenticationService; import io.redlink.more.studymanager.service.StudyService; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -25,8 +26,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @RestController @RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE) public class StudyApiV1Controller implements StudiesApi { @@ -92,9 +91,12 @@ public ResponseEntity deleteStudy(Long studyId) { @Override @RequiresStudyRole(StudyRole.STUDY_ADMIN) - public ResponseEntity setStatus(Long studyId, StatusChangeDTO statusChangeDTO) { + public ResponseEntity setStatus(Long studyId, StatusChangeDTO statusChangeDTO) { final var currentUser = authService.getCurrentUser(); - service.setStatus(studyId, StudyTransformer.fromStatusChangeDTO_V1(statusChangeDTO), currentUser); - return ResponseEntity.ok().build(); + + return ResponseEntity.of( + service.setStatus(studyId, StudyTransformer.fromStatusChangeDTO_V1(statusChangeDTO), currentUser) + .map(StudyTransformer::toStudyDTO_V1) + ); } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/UserApiV1Controller.java b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/UserApiV1Controller.java index f6536330..d7e29e3f 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/UserApiV1Controller.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/controller/studymanager/UserApiV1Controller.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.controller.studymanager; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/exception/AccessDeniedException.java b/studymanager/src/main/java/io/redlink/more/studymanager/exception/AccessDeniedException.java index 3ec6206f..ccb090ec 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/exception/AccessDeniedException.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/exception/AccessDeniedException.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.exception; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/exception/DataConstraintException.java b/studymanager/src/main/java/io/redlink/more/studymanager/exception/DataConstraintException.java index 1eeebf65..fbb2f608 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/exception/DataConstraintException.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/exception/DataConstraintException.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.exception; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/AttributeMapClaimAccessor.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/AttributeMapClaimAccessor.java index 70e9846a..e25f69dc 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/AttributeMapClaimAccessor.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/AttributeMapClaimAccessor.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.model; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/IntegrationInfo.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/IntegrationInfo.java index 10964a8d..05b641ac 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/IntegrationInfo.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/IntegrationInfo.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model; public record IntegrationInfo( diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/PlatformRole.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/PlatformRole.java index 9206ff07..b9937484 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/PlatformRole.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/PlatformRole.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.model; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/StudyRole.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/StudyRole.java index 3b767c01..74d7f034 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/StudyRole.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/StudyRole.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.model; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/User.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/User.java index 9c4c9786..6a8862ba 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/User.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/User.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.model; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/ParticipationData.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/data/ParticipationData.java similarity index 97% rename from studymanager/src/main/java/io/redlink/more/studymanager/model/ParticipationData.java rename to studymanager/src/main/java/io/redlink/more/studymanager/model/data/ParticipationData.java index 5784ddfc..002b07c9 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/ParticipationData.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/data/ParticipationData.java @@ -6,7 +6,7 @@ * Förderung der wissenschaftlichen Forschung). * Licensed under the Elastic License 2.0. */ -package io.redlink.more.studymanager.model; +package io.redlink.more.studymanager.model.data; import java.time.Instant; import java.util.Comparator; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/generator/RandomTokenGenerator.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/generator/RandomTokenGenerator.java index 52f1f2ad..24ce34ed 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/generator/RandomTokenGenerator.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/generator/RandomTokenGenerator.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.model.generator; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java index 9cd086c7..a9788b70 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Duration.java @@ -1,12 +1,17 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.scheduler; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import io.redlink.more.studymanager.api.v1.model.DurationDTO; import io.redlink.more.studymanager.api.v1.model.StudyDurationDTO; - -import java.time.Instant; -import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.Comparator; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Event.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Event.java index 2f09efdf..664cf972 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Event.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/Event.java @@ -1,7 +1,14 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.scheduler; import com.fasterxml.jackson.annotation.JsonFormat; - import java.time.Instant; public class Event implements ScheduleEvent { diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RecurrenceRule.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RecurrenceRule.java index 9459a9dd..cd9fec80 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RecurrenceRule.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RecurrenceRule.java @@ -1,7 +1,14 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.scheduler; import com.fasterxml.jackson.annotation.JsonFormat; - import java.time.Instant; import java.util.List; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeDate.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeDate.java index 071beb0c..1048ec5c 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeDate.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeDate.java @@ -1,8 +1,15 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.scheduler; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; - import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeEvent.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeEvent.java index 7fe2a688..317f0532 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeEvent.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeEvent.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.scheduler; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeRecurrenceRule.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeRecurrenceRule.java index abfaf462..df3f4a9f 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeRecurrenceRule.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/RelativeRecurrenceRule.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.scheduler; public class RelativeRecurrenceRule { diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/ScheduleEvent.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/ScheduleEvent.java index 079867d0..56121c0a 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/ScheduleEvent.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/scheduler/ScheduleEvent.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.scheduler; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/InterventionTimelineEvent.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/InterventionTimelineEvent.java index bb98ffb1..cbe6e312 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/InterventionTimelineEvent.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/InterventionTimelineEvent.java @@ -1,8 +1,15 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.timeline; import io.redlink.more.studymanager.model.Intervention; import io.redlink.more.studymanager.model.Trigger; - import java.time.Instant; public record InterventionTimelineEvent( diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/ObservationTimelineEvent.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/ObservationTimelineEvent.java index fa633ae2..aa56e559 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/ObservationTimelineEvent.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/ObservationTimelineEvent.java @@ -1,7 +1,14 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.timeline; import io.redlink.more.studymanager.model.Observation; - import java.time.Instant; public record ObservationTimelineEvent( diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/StudyTimeline.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/StudyTimeline.java index f1392c7d..ad896530 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/StudyTimeline.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/StudyTimeline.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.timeline; import java.time.Instant; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/TimelineFilter.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/TimelineFilter.java index be22d630..7cd350d5 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/TimelineFilter.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/timeline/TimelineFilter.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.timeline; public record TimelineFilter ( diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ActionTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ActionTransformer.java index e8996162..b9f7f265 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ActionTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ActionTransformer.java @@ -12,6 +12,7 @@ import io.redlink.more.studymanager.core.properties.ActionProperties; import io.redlink.more.studymanager.model.Action; import io.redlink.more.studymanager.utils.MapperUtils; +import java.time.Instant; public final class ActionTransformer { @@ -26,12 +27,14 @@ public static Action fromActionDTO_V1(ActionDTO dto) { } public static ActionDTO toActionDTO_V1(Action action) { + Instant instant = action.getModified(); + Instant instant1 = action.getCreated(); return new ActionDTO() .actionId(action.getActionId()) .type(action.getType()) .properties(action.getProperties()) - .created(Transformers.toOffsetDateTime(action.getCreated())) - .modified(Transformers.toOffsetDateTime(action.getModified())); + .created(instant1) + .modified(instant); } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ContactTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ContactTransformer.java index d9e4406c..95897f79 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ContactTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ContactTransformer.java @@ -11,7 +11,7 @@ import io.redlink.more.studymanager.api.v1.model.ContactDTO; import io.redlink.more.studymanager.model.Contact; -public class ContactTransformer { +public final class ContactTransformer { public static ContactDTO toContactDTO_V1(Contact contact) { return new ContactDTO() diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EndpointTokenTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EndpointTokenTransformer.java index fd5a79d4..14b0aa54 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EndpointTokenTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EndpointTokenTransformer.java @@ -10,20 +10,21 @@ import io.redlink.more.studymanager.api.v1.model.EndpointTokenDTO; import io.redlink.more.studymanager.model.EndpointToken; - +import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Objects; -public class EndpointTokenTransformer { +public final class EndpointTokenTransformer { private EndpointTokenTransformer() {} public static EndpointToken fromEndpointTokenDTO(EndpointTokenDTO dto) { + Instant offsetDateTime = dto.getCreated(); return new EndpointToken( dto.getTokenId(), dto.getTokenLabel(), - Transformers.toInstant(dto.getCreated()), + offsetDateTime, dto.getToken() ); } @@ -42,7 +43,7 @@ public static EndpointTokenDTO toEndpointTokenDTO(EndpointToken token) { return new EndpointTokenDTO() .tokenId(token.tokenId()) .tokenLabel(token.tokenLabel()) - .created(Transformers.toOffsetDateTime(token.created())) + .created(token.created()) .token(token.token()); } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EventTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EventTransformer.java index e653a0b3..f7246864 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EventTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/EventTransformer.java @@ -8,8 +8,23 @@ */ package io.redlink.more.studymanager.model.transformer; -import io.redlink.more.studymanager.api.v1.model.*; -import io.redlink.more.studymanager.model.scheduler.*; +import io.redlink.more.studymanager.api.v1.model.DurationDTO; +import io.redlink.more.studymanager.api.v1.model.EventDTO; +import io.redlink.more.studymanager.api.v1.model.FrequencyDTO; +import io.redlink.more.studymanager.api.v1.model.ObservationScheduleDTO; +import io.redlink.more.studymanager.api.v1.model.RecurrenceRuleDTO; +import io.redlink.more.studymanager.api.v1.model.RelativeDateDTO; +import io.redlink.more.studymanager.api.v1.model.RelativeEventDTO; +import io.redlink.more.studymanager.api.v1.model.RelativeRecurrenceRuleDTO; +import io.redlink.more.studymanager.api.v1.model.WeekdayDTO; +import io.redlink.more.studymanager.model.scheduler.Duration; +import io.redlink.more.studymanager.model.scheduler.Event; +import io.redlink.more.studymanager.model.scheduler.RecurrenceRule; +import io.redlink.more.studymanager.model.scheduler.RelativeDate; +import io.redlink.more.studymanager.model.scheduler.RelativeEvent; +import io.redlink.more.studymanager.model.scheduler.RelativeRecurrenceRule; +import io.redlink.more.studymanager.model.scheduler.ScheduleEvent; +import java.time.Instant; public final class EventTransformer { @@ -20,9 +35,11 @@ public static ScheduleEvent fromObservationScheduleDTO_V1(ObservationScheduleDTO if (genericDto != null) { if(genericDto.getType() == null || Event.TYPE.equals(genericDto.getType())) { EventDTO dto = (EventDTO) genericDto; + Instant offsetDateTime = dto.getDtend(); + Instant offsetDateTime1 = dto.getDtstart(); return new Event() - .setDateStart(Transformers.toInstant(dto.getDtstart())) - .setDateEnd(Transformers.toInstant(dto.getDtend())) + .setDateStart(offsetDateTime1) + .setDateEnd(offsetDateTime) .setRRule(fromRecurrenceRuleDTO(dto.getRrule())); } else if(RelativeEvent.TYPE.equals(genericDto.getType())) { RelativeEventDTO dto = (RelativeEventDTO) genericDto; @@ -46,10 +63,12 @@ public static ObservationScheduleDTO toObservationScheduleDTO_V1(ScheduleEvent e if (event != null) if(event.getType() == null || Event.TYPE.equals(event.getType())) { Event e = (Event) event; + Instant instant = e.getDateEnd(); + Instant instant1 = e.getDateStart(); return new EventDTO() .type(Event.TYPE) - .dtstart(Transformers.toOffsetDateTime(e.getDateStart())) - .dtend(Transformers.toOffsetDateTime(e.getDateEnd())) + .dtstart(instant1) + .dtend(instant) .rrule(toRecurrenceRuleDTO(e.getRRule())); } else if(RelativeEvent.TYPE.equals(event.getType())) { RelativeEvent e = (RelativeEvent) event; @@ -69,30 +88,34 @@ public static ObservationScheduleDTO toObservationScheduleDTO_V1(ScheduleEvent e } private static RecurrenceRule fromRecurrenceRuleDTO(RecurrenceRuleDTO dto) { - if (dto != null) + if (dto != null) { + Instant offsetDateTime = dto.getUntil(); return new RecurrenceRule() .setFreq(dto.getFreq().getValue()) .setInterval(dto.getInterval()) .setCount(dto.getCount()) - .setUntil(Transformers.toInstant(dto.getUntil())) + .setUntil(offsetDateTime) .setByDay(dto.getByday() != null ? dto.getByday().stream().map(WeekdayDTO::getValue).toList() : null) .setByMonth(dto.getBymonth()) .setByMonthDay(dto.getBymonthday()) .setBySetPos(dto.getBysetpos()); + } else return null; } private static RecurrenceRuleDTO toRecurrenceRuleDTO(RecurrenceRule recurrenceRule) { - if (recurrenceRule != null) + if (recurrenceRule != null) { + Instant instant = recurrenceRule.getUntil(); return new RecurrenceRuleDTO() .freq(recurrenceRule.getFreq() != null ? FrequencyDTO.fromValue(recurrenceRule.getFreq()) : null) .interval(recurrenceRule.getInterval()) .count(recurrenceRule.getCount()) - .until(Transformers.toOffsetDateTime(recurrenceRule.getUntil())) + .until(instant) .byday(recurrenceRule.getByDay() != null ? recurrenceRule.getByDay().stream().map(WeekdayDTO::fromValue).toList() : null) .bymonth(recurrenceRule.getByMonth()) .bymonthday(recurrenceRule.getByMonthDay()) .bysetpos(recurrenceRule.getBySetPos()); + } else return null; } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ImportExportTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ImportExportTransformer.java index c166abfd..a4979f67 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ImportExportTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ImportExportTransformer.java @@ -14,12 +14,11 @@ import io.redlink.more.studymanager.api.v1.model.StudyImportExportDTO; import io.redlink.more.studymanager.model.IntegrationInfo; import io.redlink.more.studymanager.model.StudyImportExport; - import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; -public class ImportExportTransformer { +public final class ImportExportTransformer { private ImportExportTransformer() {} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/InterventionTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/InterventionTransformer.java index ef1e726f..7595dfb7 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/InterventionTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/InterventionTransformer.java @@ -10,6 +10,7 @@ import io.redlink.more.studymanager.api.v1.model.InterventionDTO; import io.redlink.more.studymanager.model.Intervention; +import java.time.Instant; public final class InterventionTransformer { @@ -27,6 +28,8 @@ public static Intervention fromInterventionDTO_V1(InterventionDTO dto) { } public static InterventionDTO toInterventionDTO_V1(Intervention intervention) { + Instant instant = intervention.getModified(); + Instant instant1 = intervention.getCreated(); return new InterventionDTO() .studyId(intervention.getStudyId()) .interventionId(intervention.getInterventionId()) @@ -34,8 +37,8 @@ public static InterventionDTO toInterventionDTO_V1(Intervention intervention) { .purpose(intervention.getPurpose()) .studyGroupId(intervention.getStudyGroupId()) .schedule(EventTransformer.toObservationScheduleDTO_V1(intervention.getSchedule())) - .created(Transformers.toOffsetDateTime(intervention.getCreated())) - .modified(Transformers.toOffsetDateTime(intervention.getModified())); + .created(instant1) + .modified(instant); } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationTransformer.java index 077ac44f..56baf62a 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ObservationTransformer.java @@ -12,6 +12,7 @@ import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.model.Observation; import io.redlink.more.studymanager.utils.MapperUtils; +import java.time.Instant; public final class ObservationTransformer { @@ -34,6 +35,8 @@ public static Observation fromObservationDTO_V1(ObservationDTO dto) { } public static ObservationDTO toObservationDTO_V1(Observation observation) { + Instant instant = observation.getModified(); + Instant instant1 = observation.getCreated(); return new ObservationDTO() .studyId(observation.getStudyId()) .observationId(observation.getObservationId()) @@ -44,8 +47,8 @@ public static ObservationDTO toObservationDTO_V1(Observation observation) { .studyGroupId(observation.getStudyGroupId()) .properties(observation.getProperties()) .schedule(EventTransformer.toObservationScheduleDTO_V1(observation.getSchedule())) - .created(Transformers.toOffsetDateTime(observation.getCreated())) - .modified(Transformers.toOffsetDateTime(observation.getModified())) + .created(instant1) + .modified(instant) .hidden(observation.getHidden()) .noSchedule(observation.getNoSchedule()); } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ParticipantTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ParticipantTransformer.java index 098774e0..751940e7 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ParticipantTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ParticipantTransformer.java @@ -11,8 +11,9 @@ import io.redlink.more.studymanager.api.v1.model.ParticipantDTO; import io.redlink.more.studymanager.api.v1.model.ParticipantStatusDTO; import io.redlink.more.studymanager.model.Participant; +import java.time.Instant; -public class ParticipantTransformer { +public final class ParticipantTransformer { private ParticipantTransformer() { @@ -27,6 +28,9 @@ public static Participant fromParticipantDTO_V1(ParticipantDTO participantDTO) { } public static ParticipantDTO toParticipantDTO_V1(Participant participant) { + Instant instant = participant.getCreated(); + Instant instant1 = participant.getModified(); + Instant instant2 = participant.getStart(); return new ParticipantDTO() .studyId(participant.getStudyId()) .participantId(participant.getParticipantId()) @@ -34,9 +38,9 @@ public static ParticipantDTO toParticipantDTO_V1(Participant participant) { .studyGroupId(participant.getStudyGroupId()) .registrationToken(participant.getRegistrationToken()) .status(ParticipantStatusDTO.fromValue(participant.getStatus().getValue())) - .start(Transformers.toOffsetDateTime(participant.getStart())) - .modified(Transformers.toOffsetDateTime(participant.getModified())) - .created(Transformers.toOffsetDateTime(participant.getCreated())); + .start(instant2) + .modified(instant1) + .created(instant); } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/RoleTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/RoleTransformer.java index 49e37209..8b25d622 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/RoleTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/RoleTransformer.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.model.transformer; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyDataTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyDataTransformer.java index 2be7363b..e6b654f2 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyDataTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyDataTransformer.java @@ -9,10 +9,18 @@ package io.redlink.more.studymanager.model.transformer; import io.redlink.more.studymanager.api.v1.model.IdTitleDTO; +import io.redlink.more.studymanager.api.v1.model.ObservationDataViewDTO; +import io.redlink.more.studymanager.api.v1.model.ObservationDataViewDataDTO; +import io.redlink.more.studymanager.api.v1.model.ObservationDataViewDataRowDTO; import io.redlink.more.studymanager.api.v1.model.ParticipationDataDTO; -import io.redlink.more.studymanager.model.ParticipationData; +import io.redlink.more.studymanager.core.ui.DataView; +import io.redlink.more.studymanager.core.ui.DataViewInfo; +import io.redlink.more.studymanager.core.ui.DataViewRow; +import io.redlink.more.studymanager.model.data.ParticipationData; +import java.util.List; +import java.util.stream.Collectors; -public class StudyDataTransformer { +public final class StudyDataTransformer { private StudyDataTransformer(){} @@ -23,7 +31,7 @@ public static ParticipationDataDTO toParticipationDataDTO_V1(ParticipationData p .participantNamedId(toIdTitleDTO_V1(participationData.participantNamedId())) .studyGroupNamedId(toIdTitleDTO_V1(participationData.studyGroupNamedId())) .dataReceived(participationData.dataReceived()) - .lastDataReceived(Transformers.toOffsetDateTime(participationData.lastDataReceived())); + .lastDataReceived(participationData.lastDataReceived()); } public static IdTitleDTO toIdTitleDTO_V1(ParticipationData.NamedId idTitle){ if(idTitle == null) @@ -32,4 +40,40 @@ public static IdTitleDTO toIdTitleDTO_V1(ParticipationData.NamedId idTitle){ .id(idTitle.id()) .title(idTitle.title()); } + + public static ObservationDataViewDataDTO toObservationDataViewDataDTO(DataView dataView){ + var observationDataViewDataDTO = new ObservationDataViewDataDTO() + .view(toObservationDataViewDTO(dataView.viewInfo())) + .chartType(toChartTypeEnumDTO(dataView.chartType())); + + if (dataView.data() != null) { + observationDataViewDataDTO + .labels(dataView.data().labels()) + .data(toObservationDataViewDataRowDTO(dataView.data().rows())); + } + + return observationDataViewDataDTO; + } + + public static ObservationDataViewDTO toObservationDataViewDTO(DataViewInfo dataViewInfo) { + return new ObservationDataViewDTO() + .name(dataViewInfo.name()) + .label(dataViewInfo.label()) + .title(dataViewInfo.title()) + .description(dataViewInfo.description()); + } + + private static ObservationDataViewDataDTO.ChartTypeEnum toChartTypeEnumDTO(DataView.ChartType chartType) { + return switch (chartType) { + case LINE -> ObservationDataViewDataDTO.ChartTypeEnum.LINE; + case BAR -> ObservationDataViewDataDTO.ChartTypeEnum.BAR; + case PIE -> ObservationDataViewDataDTO.ChartTypeEnum.PIE; + }; + } + + private static List toObservationDataViewDataRowDTO(List dataViewRow) { + return dataViewRow.stream() + .map(row -> new ObservationDataViewDataRowDTO().label(row.label()).values(row.values())) + .collect(Collectors.toList()); + } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyGroupTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyGroupTransformer.java index 7ad8a436..0ab946d4 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyGroupTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyGroupTransformer.java @@ -11,6 +11,7 @@ import io.redlink.more.studymanager.api.v1.model.StudyGroupDTO; import io.redlink.more.studymanager.model.StudyGroup; import io.redlink.more.studymanager.model.scheduler.Duration; +import java.time.Instant; public final class StudyGroupTransformer { @@ -27,13 +28,15 @@ public static StudyGroup fromStudyGroupDTO_V1(StudyGroupDTO studyGroupDTO) { } public static StudyGroupDTO toStudyGroupDTO_V1(StudyGroup studyGroup) { + Instant instant = studyGroup.getModified(); + Instant instant1 = studyGroup.getCreated(); return new StudyGroupDTO() .studyId(studyGroup.getStudyId()) .studyGroupId(studyGroup.getStudyGroupId()) .title(studyGroup.getTitle()) .purpose(studyGroup.getPurpose()) .duration(Duration.toStudyDurationDTO(studyGroup.getDuration())) - .created(Transformers.toOffsetDateTime(studyGroup.getCreated())) - .modified(Transformers.toOffsetDateTime(studyGroup.getModified())); + .created(instant1) + .modified(instant); } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java index 24ac7edd..6afb87bf 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/StudyTransformer.java @@ -15,8 +15,9 @@ import io.redlink.more.studymanager.model.Contact; import io.redlink.more.studymanager.model.Study; import io.redlink.more.studymanager.model.scheduler.Duration; +import java.time.Instant; -public class StudyTransformer { +public final class StudyTransformer { private StudyTransformer() {} @@ -41,6 +42,8 @@ public static Study fromStudyDTO_V1(StudyDTO studyDTO) { public static StudyDTO toStudyDTO_V1(Study study) { if(study.getContact() == null) study.setContact(new Contact()); + Instant instant = study.getModified(); + Instant instant1 = study.getCreated(); return new StudyDTO() .studyId(study.getStudyId()) .title(study.getTitle()) @@ -54,8 +57,8 @@ public static StudyDTO toStudyDTO_V1(Study study) { .end(study.getEndDate()) .plannedStart(study.getPlannedStartDate()) .plannedEnd(study.getPlannedEndDate()) - .created(Transformers.toOffsetDateTime(study.getCreated())) - .modified(Transformers.toOffsetDateTime(study.getModified())) + .created(instant1) + .modified(instant) .userRoles(RoleTransformer.toStudyRolesDTO(study.getUserRoles())) .contact(ContactTransformer.toContactDTO_V1(study.getContact())); } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TimelineTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TimelineTransformer.java index 6f62aef1..9aa090d5 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TimelineTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TimelineTransformer.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.model.transformer; @@ -10,12 +18,12 @@ import io.redlink.more.studymanager.model.timeline.StudyTimeline; -public class TimelineTransformer { +public final class TimelineTransformer { private TimelineTransformer() {} public static StudyTimelineDTO toStudyTimelineDTO(StudyTimeline studyTimeline) { return new StudyTimelineDTO() - .participantSignup(Transformers.toOffsetDateTime(studyTimeline.signup())) + .participantSignup(studyTimeline.signup()) .studyDuration( new StudyTimelineStudyDurationDTO() .from(studyTimeline.participationRange().getMinimum()) @@ -36,8 +44,8 @@ public static ObservationTimelineEventDTO toObservationTimelineDTO(ObservationTi .title(observationTimelineEvent.title()) .purpose(observationTimelineEvent.purpose()) .type(observationTimelineEvent.type()) - .start(Transformers.toOffsetDateTime(observationTimelineEvent.start())) - .end(Transformers.toOffsetDateTime(observationTimelineEvent.end())) + .start(observationTimelineEvent.start()) + .end(observationTimelineEvent.end()) .hidden(observationTimelineEvent.hidden()) .scheduleType(observationTimelineEvent.scheduleType()); } @@ -48,7 +56,7 @@ public static InterventionTimelineEventDTO toInterventionTimelineEventDTO(Interv .studyGroupId(interventionTimelineEvent.studyGroupId()) .title(interventionTimelineEvent.title()) .purpose(interventionTimelineEvent.purpose()) - .start(Transformers.toOffsetDateTime(interventionTimelineEvent.start())) + .start(interventionTimelineEvent.start()) .scheduleType(interventionTimelineEvent.scheduleType()); } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/Transformers.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/Transformers.java index 6e39e833..454b2e92 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/Transformers.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/Transformers.java @@ -1,11 +1,13 @@ /* - * Copyright (c) 2023 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.model.transformer; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneId; import java.util.function.Function; /** @@ -13,19 +15,9 @@ */ public final class Transformers { - private static final ZoneId HOME = ZoneId.of("Europe/Vienna"); - private Transformers() { } - public static OffsetDateTime toOffsetDateTime(Instant instant) { - return transform(instant, i -> i.atZone(HOME).toOffsetDateTime()); - } - - public static Instant toInstant(OffsetDateTime offsetDateTime) { - return transform(offsetDateTime, OffsetDateTime::toInstant); - } - /** * Performs a null-safe conversion of {@code t} using the {@code transformer}. * @param t the value to transform. diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TriggerTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TriggerTransformer.java index df6e7e22..9e0f771c 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TriggerTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/TriggerTransformer.java @@ -12,6 +12,7 @@ import io.redlink.more.studymanager.core.properties.TriggerProperties; import io.redlink.more.studymanager.model.Trigger; import io.redlink.more.studymanager.utils.MapperUtils; +import java.time.Instant; public final class TriggerTransformer { @@ -25,10 +26,12 @@ public static Trigger fromTriggerDTO_V1(TriggerDTO dto) { } public static TriggerDTO toTriggerDTO_V1(Trigger trigger) { + Instant instant = trigger.getModified(); + Instant instant1 = trigger.getCreated(); return new TriggerDTO() .type(trigger.getType()) .properties(trigger.getProperties()) - .created(Transformers.toOffsetDateTime(trigger.getCreated())) - .modified(Transformers.toOffsetDateTime(trigger.getModified())); + .created(instant1) + .modified(instant); } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/UserInfoTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/UserInfoTransformer.java index e3016c9a..a177885b 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/UserInfoTransformer.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/UserInfoTransformer.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.model.transformer; @@ -76,7 +81,7 @@ private static CollaboratorRoleDetailsDTO toCollaboratorRoleDetailsDTO(StudyUser return new CollaboratorRoleDetailsDTO() .role(RoleTransformer.toStudyRoleDTO(role.role())) .assignedBy(toUserInfoDTO(role.creator())) - .assignedAt(Transformers.toOffsetDateTime(role.created())) + .assignedAt(role.created()) ; } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ValidationReportTransformer.java b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ValidationReportTransformer.java new file mode 100644 index 00000000..c9e25d3e --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/model/transformer/ValidationReportTransformer.java @@ -0,0 +1,49 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ +package io.redlink.more.studymanager.model.transformer; + +import io.redlink.more.studymanager.api.v1.model.ValidationReportDTO; +import io.redlink.more.studymanager.api.v1.model.ValidationReportItemDTO; +import io.redlink.more.studymanager.core.exception.ConfigurationValidationException; +import io.redlink.more.studymanager.core.validation.ConfigurationValidationReport; +import io.redlink.more.studymanager.core.validation.ValidationIssue; +import java.util.List; + +public final class ValidationReportTransformer { + + private ValidationReportTransformer() { + } + + public static ValidationReportDTO validationReportDTO_V1(ConfigurationValidationException validation) { + return validationReportDTO_V1(validation.getReport()) + .valid(false); + } + + public static ValidationReportDTO validationReportDTO_V1(ConfigurationValidationReport report) { + return new ValidationReportDTO() + .valid(report.isValid()) + .errors(validationReportItemDTO_V1(report.getErrors(), "error")) + .warnings(validationReportItemDTO_V1(report.getWarnings(), "warning")); + } + + private static List validationReportItemDTO_V1(List issues, String type) { + return issues.stream() + .map(i -> validationReportItemDTO_V1(i, type)) + .toList(); + } + + private static ValidationReportItemDTO validationReportItemDTO_V1(ValidationIssue issue, String type) { + return new ValidationReportItemDTO() + .message(issue.getMessage()) + .propertyId(issue.getPropertyId()) + .componentTitle(issue.getComponentTitle()) + .type(type); + } + +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/properties/GatewayProperties.java b/studymanager/src/main/java/io/redlink/more/studymanager/properties/GatewayProperties.java index 6dc942c2..b29d8f3f 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/properties/GatewayProperties.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/properties/GatewayProperties.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.properties; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/RepositoryUtils.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/RepositoryUtils.java index 5c6154d3..a6766333 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/RepositoryUtils.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/RepositoryUtils.java @@ -1,16 +1,20 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.repository; import io.redlink.more.studymanager.model.Participant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.sql.ResultSet; import java.sql.SQLException; import java.time.Instant; import java.time.LocalDate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.RowMapper; public final class RepositoryUtils { diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyAclRepository.java b/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyAclRepository.java index 749adad4..e5d168e2 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyAclRepository.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/repository/StudyAclRepository.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.repository; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/sdk/MoreSDK.java b/studymanager/src/main/java/io/redlink/more/studymanager/sdk/MoreSDK.java index 9d532532..95cebadc 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/sdk/MoreSDK.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/sdk/MoreSDK.java @@ -15,6 +15,8 @@ import io.redlink.more.studymanager.core.sdk.MoreObservationSDK; import io.redlink.more.studymanager.core.sdk.MoreTriggerSDK; import io.redlink.more.studymanager.core.sdk.schedule.Schedule; +import io.redlink.more.studymanager.core.ui.DataViewData; +import io.redlink.more.studymanager.core.ui.ViewConfig; import io.redlink.more.studymanager.model.Participant; import io.redlink.more.studymanager.model.data.ElasticActionDataPoint; import io.redlink.more.studymanager.model.data.ElasticDataPoint; @@ -26,18 +28,23 @@ import io.redlink.more.studymanager.sdk.scoped.MoreActionSDKImpl; import io.redlink.more.studymanager.sdk.scoped.MoreObservationSDKImpl; import io.redlink.more.studymanager.sdk.scoped.MoreTriggerSDKImpl; +import io.redlink.more.studymanager.service.ElasticDataService; import io.redlink.more.studymanager.service.ElasticService; import io.redlink.more.studymanager.service.ParticipantService; import io.redlink.more.studymanager.service.PushNotificationService; +import java.io.IOException; +import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import java.io.Serializable; -import java.time.Instant; -import java.util.*; -import java.util.stream.Collectors; - @Component public class MoreSDK { @@ -51,6 +58,8 @@ public class MoreSDK { private final ElasticService elasticService; + private final ElasticDataService elasticDataService; + private final PushNotificationService pushNotificationService; private final ObservationRepository observationRepository; @@ -59,12 +68,13 @@ public MoreSDK( NameValuePairRepository nvpairs, SchedulingService schedulingService, ParticipantService participantService, - ElasticService elasticService, + ElasticService elasticService, ElasticDataService elasticDataService, PushNotificationService pushNotificationService, ObservationRepository observationRepository) { this.nvpairs = nvpairs; this.schedulingService = schedulingService; this.participantService = participantService; this.elasticService = elasticService; + this.elasticDataService = elasticDataService; this.pushNotificationService = pushNotificationService; this.observationRepository = observationRepository; } @@ -169,4 +179,13 @@ public Optional getPropertiesForParticipant(long studyId, public void removePropertiesForParticipant(long studyId, Integer participantId, int observationId) { observationRepository.removeParticipantProperties(studyId, participantId, observationId); } + + public DataViewData queryData(ViewConfig viewConfig, long studyId, Integer studyGroupId, int observationId, Integer participantId, TimeRange timerange) { + try { + return elasticDataService.queryObservationViewData(viewConfig, studyId, studyGroupId, observationId, participantId, timerange); + } catch (IOException e) { + LOGGER.warn("Failed to query observation data for view config {}", viewConfig, e); + return null; + } + } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/sdk/scoped/MoreObservationSDKImpl.java b/studymanager/src/main/java/io/redlink/more/studymanager/sdk/scoped/MoreObservationSDKImpl.java index d984ba47..82a8636c 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/sdk/scoped/MoreObservationSDKImpl.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/sdk/scoped/MoreObservationSDKImpl.java @@ -8,13 +8,18 @@ */ package io.redlink.more.studymanager.sdk.scoped; +import io.redlink.more.studymanager.core.io.TimeRange; import io.redlink.more.studymanager.core.properties.ObservationProperties; import io.redlink.more.studymanager.core.sdk.MoreObservationSDK; +import io.redlink.more.studymanager.core.ui.DataViewData; +import io.redlink.more.studymanager.core.ui.DataViewRow; +import io.redlink.more.studymanager.core.ui.ViewConfig; import io.redlink.more.studymanager.model.data.ElasticDataPoint; import io.redlink.more.studymanager.sdk.MoreSDK; import java.io.Serializable; import java.time.Instant; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -79,4 +84,9 @@ public void removePropertiesForParticipant(Integer participantId) { public void storeDataPoint(Integer participantId, String observationType, Map data) { sdk.storeDatapoint(ElasticDataPoint.Type.observation, studyId, studyGroupId, participantId, observationId, observationType, Instant.now(), data); } + + @Override + public DataViewData queryData(ViewConfig viewConfig, Integer studyGroupId, Integer participantId, TimeRange timerange) { + return sdk.queryData(viewConfig, studyId, studyGroupId, observationId, participantId, timerange); + } } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/CalendarService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/CalendarService.java index 8c2c48b1..e621261a 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/CalendarService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/CalendarService.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.service; import io.redlink.more.studymanager.exception.NotFoundException; @@ -14,7 +22,6 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; -import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.List; @@ -41,7 +48,7 @@ public CalendarService(StudyService studyService, ObservationService observation this.participantService = participantService; } - public StudyTimeline getTimeline(Long studyId, Integer participantId, Integer studyGroupId, OffsetDateTime referenceDate, LocalDate from, LocalDate to) { + public StudyTimeline getTimeline(Long studyId, Integer participantId, Integer studyGroupId, Instant referenceDate, LocalDate from, LocalDate to) { final Study study = studyService.getStudy(studyId, null) .orElseThrow(() -> NotFoundException.Study(studyId)); final Range studyRange = Range.of( @@ -66,7 +73,7 @@ public StudyTimeline getTimeline(Long studyId, Integer participantId, Integer st */ final Instant participantStart; if (referenceDate != null) { - participantStart = referenceDate.toInstant(); + participantStart = referenceDate; } else if (participant != null && participant.getStart() != null) { participantStart = participant.getStart(); } else { diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/DataProcessingService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/DataProcessingService.java index 26e5f415..1645ed84 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/DataProcessingService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/DataProcessingService.java @@ -9,17 +9,24 @@ package io.redlink.more.studymanager.service; import io.redlink.more.studymanager.api.v1.model.DataPointDTO; +import io.redlink.more.studymanager.core.io.Timeframe; +import io.redlink.more.studymanager.core.ui.DataView; +import io.redlink.more.studymanager.core.ui.DataViewInfo; import io.redlink.more.studymanager.model.Observation; import io.redlink.more.studymanager.model.Participant; -import io.redlink.more.studymanager.model.ParticipationData; import io.redlink.more.studymanager.model.StudyGroup; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Service; - +import io.redlink.more.studymanager.model.data.ParticipationData; import java.io.IOException; -import java.time.OffsetDateTime; +import java.time.Instant; import java.time.format.DateTimeFormatter; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; @Service public class DataProcessingService { @@ -112,7 +119,20 @@ public List getParticipationData(Long studyId){ return participationDataList; } - public List getDataPoints(Long studyId, Integer size, Integer observationId, Integer participantId, OffsetDateTime date) { + public DataViewInfo[] listDataViews(Long studyId, Integer observationId) { + return observationService.listDataViews(studyId, observationId); + } + + public DataView getDataView(Long studyId, Integer observationId, String viewName, Integer studyGroupId, Integer participantId, Instant from, Instant to) { + if (participantId != null) { + return observationService.queryData(studyId, observationId, viewName, null, participantId, from != null && to != null ? new Timeframe(from, to) : null); + } else if (studyGroupId != null) { + return observationService.queryData(studyId, observationId, viewName, studyGroupId, null, from != null && to != null ? new Timeframe(from, to) : null); + } + return observationService.queryData(studyId, observationId, viewName, studyGroupId, participantId, from != null && to != null ? new Timeframe(from, to) : null); + } + + public List getDataPoints(Long studyId, Integer size, Integer observationId, Integer participantId, Instant date) { try { return elasticService.listDataPoints(studyId, participantId, observationId, toIsoString(date), size).stream() .map(dp -> new DataPointDTO() @@ -129,8 +149,8 @@ public List getDataPoints(Long studyId, Integer size, Integer obse } } - private String toIsoString(OffsetDateTime date) { - return date != null ? date.format(DateTimeFormatter.ISO_INSTANT) : null; + private String toIsoString(Instant date) { + return date != null ? DateTimeFormatter.ISO_INSTANT.format(date) : null; } @Cacheable("participants") diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/ElasticDataService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/ElasticDataService.java new file mode 100644 index 00000000..1818deef --- /dev/null +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/ElasticDataService.java @@ -0,0 +1,250 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ +package io.redlink.more.studymanager.service; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import co.elastic.clients.elasticsearch._types.aggregations.Aggregate; +import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; +import co.elastic.clients.elasticsearch._types.aggregations.DateHistogramBucket; +import co.elastic.clients.elasticsearch._types.aggregations.MinimumInterval; +import co.elastic.clients.elasticsearch._types.aggregations.MultiBucketBase; +import co.elastic.clients.elasticsearch._types.aggregations.StringTermsBucket; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch.core.SearchRequest; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.util.ObjectBuilder; +import io.redlink.more.studymanager.core.io.TimeRange; +import io.redlink.more.studymanager.core.ui.DataViewData; +import io.redlink.more.studymanager.core.ui.DataViewRow; +import io.redlink.more.studymanager.core.ui.ViewConfig; +import io.redlink.more.studymanager.model.StudyGroup; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import static io.redlink.more.studymanager.service.ElasticService.getFilters; +import static io.redlink.more.studymanager.service.ElasticService.getStudyIdString; + +@Service +public class ElasticDataService { + + private static final Logger LOG = LoggerFactory.getLogger(ElasticDataService.class); + + private static final String AGG_NAME_SERIES = "series"; + private static final String AGG_NAME_ROWS = "rows"; + private static final String AGG_NAME_VALUES = "values"; + private static final String NO_GROUP_KEY = "no_group"; + + private final ElasticsearchClient client; + + private final ParticipantService participantService; + private final StudyGroupService studyGroupService; + + public ElasticDataService(ElasticsearchClient client, ParticipantService participantService, StudyGroupService studyGroupService) { + this.client = client; + this.participantService = participantService; + this.studyGroupService = studyGroupService; + } + + public DataViewData queryObservationViewData(ViewConfig viewConfig, long studyId, Integer studyGroupId, int observationId, Integer participantId, TimeRange timerange) throws IOException { + final List filters = getFilters(studyId, observationId, studyGroupId, participantId, timerange); + + final SearchRequest.Builder builder = buildDataPreviewRequest(viewConfig, filters, studyId); + final SearchRequest request = builder.build(); + + try { + final SearchResponse searchResponse = client.search(request, Void.class); + return processDataPreviewResponse(viewConfig, searchResponse, studyId); + } catch (IOException | ElasticsearchException e) { + return ElasticService.handleIndexNotFoundException(e, () -> null, IOException::new); + } + } + + private SearchRequest.Builder buildDataPreviewRequest(ViewConfig viewConfig, List filters, long studyId) { + final var rows = viewConfig.rowAggregation(); + final var series = viewConfig.seriesAggregation(); + return new SearchRequest.Builder() + .index(getStudyIdString(studyId)) + .size(0) + .query(q -> q.bool(b -> b.filter(filters))) + .aggregations(AGG_NAME_SERIES, s -> + applyAggregation(s, series, viewConfig.operation()) + .aggregations(AGG_NAME_ROWS, r -> + applyAggregation(r, rows, viewConfig.operation()) + .aggregations(AGG_NAME_VALUES, d -> applyOperation(d, viewConfig)) + ) + + ) + .aggregations("rowLabels", rl -> + applyAggregation(rl, rows, viewConfig.operation()) + ) + ; + } + + private List readBuckets(Map aggregations, String aggName) { + final var agg = aggregations.get(aggName); + if (agg.isAutoDateHistogram()) { + return agg.autoDateHistogram() + .buckets().array(); + } else if (agg.isSterms()) { + return agg.sterms() + .buckets().array(); + } + throw new IllegalStateException("Unknown aggregation type: " + agg._kind()); + } + + private String readBucketKey(MultiBucketBase bucket) { + if (bucket instanceof StringTermsBucket str) { + return str.key().stringValue(); + } else if (bucket instanceof DateHistogramBucket date) { + return date.keyAsString(); + } else { + return null; + } + } + + private Function createTitleResolver(ViewConfig.Aggregation aggregation, long studyId) { + if (aggregation == null) { + return Function.identity(); + } + + final Map mapping = switch (aggregation) { + case PARTICIPANT -> + participantService.listParticipants(studyId) + .stream() + .collect(Collectors.toMap( + p -> ElasticService.getParticipantIdString(p.getParticipantId()), + p -> String.format("%s (%d)", p.getAlias(), p.getParticipantId()) + )); + + case STUDY_GROUP -> { + final Map m = new HashMap<>( + studyGroupService.listStudyGroups(studyId) + .stream() + .collect(Collectors.toMap( + g -> ElasticService.getStudyGroupIdString(g.getStudyGroupId()), + StudyGroup::getTitle + )) + ); + m.put(NO_GROUP_KEY, "i18n.global.placeholder.noGroup"); + yield m; + } + default -> Map.of(); + }; + return l -> mapping.getOrDefault(l, l); + } + + private DataViewData processDataPreviewResponse(ViewConfig viewConfig, SearchResponse searchResponse, long studyId) { + + final List seriesBuckets = readBuckets(searchResponse.aggregations(), AGG_NAME_SERIES); + final int seriesCount = seriesBuckets.size(); + final List labels = new ArrayList<>(seriesCount); + final Supplier> createValueArray = () -> { + final ArrayList array = new ArrayList<>(seriesCount); + for (int i = 0; i < seriesCount; i++) { + array.add(null); + } + return array; + }; + + final LinkedHashMap> rowMap = new LinkedHashMap<>(); + + final Function labelsTitleResolver = createTitleResolver(viewConfig.seriesAggregation(), studyId); + for (MultiBucketBase bucket : seriesBuckets) { + final String bucketKey = readBucketKey(bucket); + + labels.add(labelsTitleResolver.apply(bucketKey)); + final int seriesIdx = labels.size() - 1; + + final List rowsBuckets = readBuckets(bucket.aggregations(), AGG_NAME_ROWS); + for (MultiBucketBase rowBucket : rowsBuckets) { + final String rowKey = readBucketKey(rowBucket); + + final var valueAgg = rowBucket.aggregations().get(AGG_NAME_VALUES); + final var value = switch (viewConfig.operation().operator()) { + case SUM, COUNT -> valueAgg.sum().value(); + case MIN -> valueAgg.min().value(); + case MAX -> valueAgg.max().value(); + case AVG -> valueAgg.avg().value(); + }; + rowMap.computeIfAbsent(rowKey, k -> createValueArray.get()) + .set(seriesIdx, value); + } + } + + final Function rowTitleResolver = createTitleResolver(viewConfig.rowAggregation(), studyId); + return new DataViewData( + List.copyOf(labels), + rowMap.entrySet().stream() + .map(e -> new DataViewRow( + rowTitleResolver.apply(e.getKey()), + e.getValue() + )) + .toList() + ); + } + + private Aggregation.Builder.ContainerBuilder applyAggregation(Aggregation.Builder a, ViewConfig.Aggregation aggregation, ViewConfig.Operation operation) { + if (aggregation == null) { + // If there's no aggregation required at this level, we perform + // "no-op"-aggregation to keep response-structure aligned. + return a.terms(n -> n.field("study_id.keyword")); + } + return switch (aggregation) { + case TIME -> a.autoDateHistogram(dateHistogram -> dateHistogram + .field("effective_time_frame") + .buckets(1000) // TODO: Hidden magic number, aligned with chart-width in the UI! + .minimumInterval(MinimumInterval.Minute) + .format("yyyy-MM-dd'T'HH:mmZ") + ); + case PARTICIPANT -> a.terms(pt -> pt.field("participant_id.keyword")); + case STUDY_GROUP -> a.terms(sg -> sg.field("study_group_id.keyword") + .minDocCount(0) + .missing(NO_GROUP_KEY) + ); + case TERM_FIELD -> a.terms(tf -> tf.field("data_%s.keyword".formatted(operation.field())) + .minDocCount(1) + ); + }; + } + + private ObjectBuilder applyOperation(Aggregation.Builder agg, ViewConfig viewConfig) { + final String field = viewConfig.operation().field(); + return switch (viewConfig.operation().operator()) { + case AVG -> agg.avg( + s -> s.field(String.format("data_%s", field)) + ); + case SUM -> agg.sum( + s -> s.field(String.format("data_%s", field)) + ); + case MIN -> agg.min( + m -> m.field(String.format("data_%s", field)) + ); + case MAX -> agg.max( + m -> m.field(String.format("data_%s", field)) + ); + case COUNT -> agg.sum( + s -> s.field("non_existing_field") + .missing(1) + ); + }; + } + + +} diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/ElasticService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/ElasticService.java index 8035caa6..a83e5b62 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/ElasticService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/ElasticService.java @@ -14,24 +14,26 @@ import co.elastic.clients.elasticsearch._types.FieldValue; import co.elastic.clients.elasticsearch._types.SortOrder; import co.elastic.clients.elasticsearch._types.aggregations.StringTermsBucket; +import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; import co.elastic.clients.elasticsearch._types.query_dsl.Query; -import co.elastic.clients.elasticsearch.core.*; +import co.elastic.clients.elasticsearch._types.query_dsl.TermsQueryField; +import co.elastic.clients.elasticsearch.core.DeleteByQueryRequest; +import co.elastic.clients.elasticsearch.core.SearchRequest; +import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.indices.CloseIndexRequest; import co.elastic.clients.elasticsearch.indices.DeleteIndexRequest; import co.elastic.clients.elasticsearch.indices.ExistsRequest; +import co.elastic.clients.json.JsonData; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.Iterables; import io.redlink.more.studymanager.core.io.TimeRange; -import io.redlink.more.studymanager.model.ParticipationData; import io.redlink.more.studymanager.model.Study; import io.redlink.more.studymanager.model.data.ElasticDataPoint; +import io.redlink.more.studymanager.model.data.ParticipationData; import io.redlink.more.studymanager.model.data.SimpleDataPoint; import io.redlink.more.studymanager.properties.ElasticProperties; import io.redlink.more.studymanager.utils.MapperUtils; -import java.util.Objects; -import java.util.function.Function; -import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -41,10 +43,9 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; @Service @@ -95,29 +96,73 @@ public List participantsThatMapQuery(Long studyId, Integer studyGroupId } } - private List getFilters(Long studyId, Integer studyGroupId, TimeRange timerange) { + static List getFilters(Long studyId, Integer studyGroupId, TimeRange timerange) { List queries = new ArrayList<>(); - queries.add(Query.of(f -> f. - term(t -> t. - field("study_id"). - value(getStudyIdString(studyId))))); + queries.add(getStudyFilter(studyId)); if (studyGroupId != null) { - queries.add(Query.of(f -> f.term(t -> t. - field("study_group_id"). - value(getStudyGroupIdString(studyGroupId))))); + queries.add(getStudyGroupFilter(studyGroupId)); } if (timerange != null && timerange.getFromString() != null && timerange.getToString() != null) { - queries.add(Query.of(f -> f. - range(r -> r. - field("effective_time_frame"). - from(timerange.getFromString()). - to(timerange.getToString()) - ))); + queries.add(getTimeRangeFilter(timerange)); } return queries; } + static List getFilters(Long studyId, Integer observationId, Integer studyGroupId, Integer participantId, TimeRange timerange) { + List filters = new ArrayList<>(); + filters.add(getStudyFilter(studyId)); + filters.add(getObservationFilter(observationId)); + + if (participantId != null) { + filters.add(getParticipantFilter(participantId)); + } else if(studyGroupId != null) { + filters.add(getStudyGroupFilter(studyGroupId)); + } + + if (timerange != null && timerange.getFromString() != null && timerange.getToString() != null) { + filters.add(getTimeRangeFilter(timerange)); + } + return filters; + } + + static Query getStudyFilter(Long studyId) { + return Query.of(f -> f. + term(t -> t. + field("study_id.keyword"). + value(getStudyIdString(studyId)))); + } + + static Query getObservationFilter(Integer observationId) { + return Query.of(f -> f. + term(t -> t. + field("observation_id.keyword"). + value(observationId))); + } + + static Query getStudyGroupFilter(Integer studyGroupId) { + return Query.of(f -> f. + term(t -> t. + field("study_group_id.keyword"). + value(getStudyGroupIdString(studyGroupId)))); + } + + static Query getParticipantFilter(Integer participantId) { + return Query.of(f -> f. + term(t -> t. + field("participant_id.keyword"). + value(getParticipantIdString(participantId)))); + } + + static Query getTimeRangeFilter(TimeRange timerange) { + return Query.of(f -> f. + range(r -> r. + field("effective_time_frame"). + from(timerange.getFromString()). + to(timerange.getToString()) + )); + } + public boolean indexExists(long studyId) { ExistsRequest request = new ExistsRequest.Builder() .index(getStudyIdString(studyId)) @@ -185,7 +230,7 @@ static String getStudyIdString(Study study) { return getStudyIdString(study.getStudyId()); } - private String getStudyGroupIdString(Integer studyGroupId) { + static String getStudyGroupIdString(Integer studyGroupId) { return "study_group_" + studyGroupId; } @@ -193,6 +238,10 @@ static String getStudyIdString(Long id) { return "study_" + id; } + static String getParticipantIdString(Integer participantId) { + return "participant_" + participantId; + } + public void setDataPoint(Long studyId, ElasticDataPoint elasticActionDataPoint) { try { client.index(i -> i.index(getStudyIdString(studyId)).document(elasticActionDataPoint)); @@ -279,23 +328,23 @@ public List getParticipationData(Long studyId){ } } - public void exportData(Long studyId, OutputStream outputStream) throws IOException { + public void exportData(OutputStream outputStream, Long studyId, List studyGroupId, List participantId, List observationId, Instant from, Instant to) throws IOException { String index = getStudyIdString(studyId); if(!client.indices().exists(e -> e.index(index)).value()) { return; } - SearchRequest request = getQuery(index, null); + SearchRequest request = getQuery(index, studyGroupId, participantId, observationId, from, to, null); SearchResponse rsp = client.search(request, JsonNode.class); while (rsp.hits().hits().size() > 0) { writeHits(rsp.hits().hits(), outputStream); outputStream.flush(); List searchAfterSort = Iterables.getLast(rsp.hits().hits()).sort(); - request = getQuery(index, searchAfterSort); + request = getQuery(index, studyGroupId, participantId, observationId, from, to, searchAfterSort); rsp = client.search(request, JsonNode.class); - if(rsp.hits().hits().size() > 0) { + if (rsp.hits().hits().size() > 0) { outputStream.write(",".getBytes(StandardCharsets.UTF_8)); } } @@ -309,21 +358,46 @@ private void writeHits(List> hits, OutputStream outputStream) thro outputStream.write(datapoints.getBytes(StandardCharsets.UTF_8)); } - private SearchRequest getQuery(String index, List searchAfterSort) { + private SearchRequest getQuery(String index, List studyGroupIds, List participantIds, List observationIds, Instant from, Instant to, List searchAfterSort) { SearchRequest.Builder builder = new SearchRequest.Builder(); + BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder(); + + if (studyGroupIds != null && !studyGroupIds.isEmpty()) { + List studyGroupIdsStrings = studyGroupIds.stream() + .map(ElasticService::getStudyGroupIdString) + .toList(); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("study_group_id").terms(TermsQueryField.of(tf -> tf.value(studyGroupIdsStrings.stream().map(FieldValue::of).collect(Collectors.toList())))))); + } + + if (participantIds != null && !participantIds.isEmpty()) { + List participantIdStrings = participantIds.stream() + .map(ElasticService::getParticipantIdString) + .toList(); + boolQueryBuilder.filter(f -> f.terms(t -> t.field("participant_id").terms(TermsQueryField.of(tf -> tf.value(participantIdStrings.stream().map(FieldValue::of).collect(Collectors.toList())))))); + } + + if (observationIds != null && !observationIds.isEmpty()) { + boolQueryBuilder.filter(f -> f.terms(t -> t.field("observation_id").terms(v -> v.value(observationIds.stream().map(FieldValue::of).collect(Collectors.toList()))))); + } + + if (from != null && to != null) { + boolQueryBuilder.filter(f -> f.range(r -> r.field("effective_time_frame").gte(JsonData.of(from)).lte(JsonData.of(to)))); + } + builder.index(index) - .query(q -> q.matchAll(m -> m)) - //.pit(p -> p.id(pitId).keepAlive(k -> k.time("1m"))) + .query(q -> q.bool(boolQueryBuilder.build())) .sort(s -> s.field(f -> f.field("effective_time_frame").order(SortOrder.Asc))) + .sort(s -> s.field(f -> f.field("datapoint_id.keyword").order(SortOrder.Asc))) .size(BATCH_SIZE_FOR_EXPORT_REQUESTS); - if(searchAfterSort != null) { + if (searchAfterSort != null) { builder.searchAfter(searchAfterSort); } return builder.build(); } + public List listDataPoints( Long studyId, Integer participantId, Integer observationId, String isoDate, int size) throws IOException { SearchRequest.Builder builder = new SearchRequest.Builder(); @@ -372,16 +446,16 @@ private Map toData(Map source) { return result; } - private static R handleIndexNotFoundException(T e, Supplier supplier) throws T { + static R handleIndexNotFoundException(T e, Supplier supplier) throws T { return ElasticService.handleIndexNotFoundException(e, supplier, Function.identity()); } - private static R handleIndexNotFoundException(E e, Supplier supplier, Function exceptionTFunction) throws T { + static R handleIndexNotFoundException(E e, Supplier supplier, Function exceptionTFunction) throws T { if (isElasticIndexNotFound(e)) return supplier.get(); throw exceptionTFunction.apply(e); } - private static boolean isElasticIndexNotFound(Exception e) { + static boolean isElasticIndexNotFound(Exception e) { if (e instanceof ElasticsearchException ee) { if (Objects.equals(ee.error().type(), "index_not_found_exception")) { LOG.debug("Swallowing Index-Not-Found from Elastic"); @@ -391,11 +465,11 @@ private static boolean isElasticIndexNotFound(Exception e) { return false; } - private static void handleIndexNotFoundException(E e) throws E { + static void handleIndexNotFoundException(E e) throws E { handleIndexNotFoundException(e, Function.identity()); } - private static void handleIndexNotFoundException(E e, Function exceptionWrapper) throws T { + static void handleIndexNotFoundException(E e, Function exceptionWrapper) throws T { if (!isElasticIndexNotFound(e)) throw exceptionWrapper.apply(e); } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/ImportExportService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/ImportExportService.java index a4273d75..d4889342 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/ImportExportService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/ImportExportService.java @@ -10,7 +10,6 @@ import io.redlink.more.studymanager.exception.NotFoundException; import io.redlink.more.studymanager.model.*; -import jakarta.servlet.ServletOutputStream; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,7 +21,9 @@ import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.*; @Service @@ -151,19 +152,19 @@ public Study importStudy(StudyImportExport studyImport, AuthenticatedUser user) return newStudy; } - public void exportStudyData(ServletOutputStream outputStream, Long studyId) { + public void exportStudyData(OutputStream outputStream, Long studyId, List studyGroupId, List participantId, List observationId, Instant from, Instant to) { if (studyService.existsStudy(studyId).orElse(false)) { - exportStudyDataAsync(outputStream, studyId); + exportStudyDataAsync(outputStream, studyId, studyGroupId, participantId, observationId, from, to); } else { throw NotFoundException.Study(studyId); } } @Async - public void exportStudyDataAsync(ServletOutputStream outputStream, Long studyId) { + public void exportStudyDataAsync(OutputStream outputStream, Long studyId, List studyGroupId, List participantId, List observationId, Instant from, Instant to) { try (outputStream) { outputStream.write("[".getBytes(StandardCharsets.UTF_8)); - elasticService.exportData(studyId, outputStream); + elasticService.exportData(outputStream, studyId, studyGroupId, participantId, observationId, from, to); outputStream.write("]".getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { LOGGER.error("Cannot export study data for {}", studyId, e); diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/InterventionService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/InterventionService.java index 07da6003..d1bdf9a1 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/InterventionService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/InterventionService.java @@ -14,7 +14,6 @@ import io.redlink.more.studymanager.core.factory.TriggerFactory; import io.redlink.more.studymanager.core.properties.ActionProperties; import io.redlink.more.studymanager.core.properties.TriggerProperties; -import io.redlink.more.studymanager.core.validation.ConfigurationValidationReport; import io.redlink.more.studymanager.exception.BadRequestException; import io.redlink.more.studymanager.exception.NotFoundException; import io.redlink.more.studymanager.model.Action; @@ -215,7 +214,7 @@ private Trigger validateTrigger(Trigger trigger) { try { CronExpression.validateExpression(trigger.getProperties().get("cronSchedule").toString()); } catch (ParseException e) { - throw new ConfigurationValidationException(ConfigurationValidationReport.init().error(e.getMessage())); + throw ConfigurationValidationException.ofError(e.getMessage()); } } } catch (ConfigurationValidationException e) { diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/OAuth2AuthenticationService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/OAuth2AuthenticationService.java index 64a9abc9..291eebbf 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/OAuth2AuthenticationService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/OAuth2AuthenticationService.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.service; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/ObservationService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/ObservationService.java index b2f5b62f..9f47854e 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/ObservationService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/ObservationService.java @@ -11,19 +11,22 @@ import io.redlink.more.studymanager.core.component.Component; import io.redlink.more.studymanager.core.exception.ConfigurationValidationException; import io.redlink.more.studymanager.core.factory.ObservationFactory; +import io.redlink.more.studymanager.core.io.TimeRange; import io.redlink.more.studymanager.core.properties.ObservationProperties; +import io.redlink.more.studymanager.core.ui.DataView; +import io.redlink.more.studymanager.core.ui.DataViewInfo; +import io.redlink.more.studymanager.core.validation.ValidationIssue; import io.redlink.more.studymanager.exception.BadRequestException; import io.redlink.more.studymanager.exception.NotFoundException; import io.redlink.more.studymanager.model.Observation; import io.redlink.more.studymanager.model.Study; import io.redlink.more.studymanager.repository.ObservationRepository; import io.redlink.more.studymanager.sdk.MoreSDK; -import java.util.EnumSet; -import org.springframework.stereotype.Service; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.time.OffsetDateTime; +import java.util.*; + +import org.springframework.stereotype.Service; @Service public class ObservationService { @@ -95,14 +98,51 @@ public void alignObservationsWithStudyState(Study study){ private void deactivateObservationsFor(Study study){ listObservationsFor(study).forEach(Component::deactivate); } + private void validateProperties(List observations) { + for (Observation observation : observations) { + try { + factory(observation).validate(observation.getProperties()); + } catch (ConfigurationValidationException e) { + for (ValidationIssue issue : e.getReport().getIssues()) { + issue.setComponentTitle(observation.getTitle()); + } + + throw e; + } + } + } + public List listObservationsFor(Study study){ - return listObservations(study.getStudyId()).stream() - .map(observation -> factory(observation) - .create( - sdk.scopedObservationSDK(observation.getStudyId(), observation.getStudyGroupId(), observation.getObservationId()), - observation.getProperties() - )) - .toList(); + List observations = listObservations(study.getStudyId()); + List result = new ArrayList<>(); + + validateProperties(observations); + + for (Observation observation : observations) { + result.add(factory(observation) + .create( + sdk.scopedObservationSDK(observation.getStudyId(), observation.getStudyGroupId(), observation.getObservationId()), + observation.getProperties() + )); + } + + return result; + } + + public DataViewInfo[] listDataViews(Long studyId, Integer observationId) { + var obs = getObservation(studyId, observationId).orElseThrow(); + + return factory(obs).create( + sdk.scopedObservationSDK(obs.getStudyId(), obs.getStudyGroupId(), obs.getObservationId()), obs.getProperties() + ).listViews(); + } + + public DataView queryData(Long studyId, Integer observationId, String viewName, Integer studyGroupId, Integer participantId, TimeRange timerange) { + var obs = getObservation(studyId, observationId).orElseThrow(); + + return factory(obs).create( + sdk.scopedObservationSDK(obs.getStudyId(), obs.getStudyGroupId(), obs.getObservationId()), obs.getProperties() + ).getView(viewName, studyGroupId, participantId, timerange); } private ObservationFactory factory(Observation observation) { diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyPermissionService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyPermissionService.java index 0285c2a0..6a228458 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyPermissionService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyPermissionService.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.service; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java index b520dc83..acd3fb2a 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/StudyService.java @@ -23,12 +23,9 @@ import io.redlink.more.studymanager.repository.StudyGroupRepository; import io.redlink.more.studymanager.repository.StudyRepository; import io.redlink.more.studymanager.repository.UserRepository; - -import java.time.temporal.ChronoUnit; import java.util.EnumSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import org.apache.commons.lang3.StringUtils; @@ -118,7 +115,7 @@ public void deleteStudy(Long studyId) { } @Transactional - public void setStatus(Long studyId, Study.Status newState, User user) { + public Optional setStatus(Long studyId, Study.Status newState, User user) { final Study study = getStudy(studyId, user) .orElseThrow(() -> NotFoundException.Study(studyId)); final Study.Status oldState = study.getStudyState(); @@ -128,8 +125,8 @@ public void setStatus(Long studyId, Study.Status newState, User user) { throw BadRequestException.StateChange(oldState, newState); } - studyRepository.setStateById(studyId, newState) - .ifPresent(s -> { + return studyRepository.setStateById(studyId, newState) + .map(s -> { try { alignWithStudyState(s); participantService.listParticipants(studyId).forEach(participant -> @@ -147,6 +144,7 @@ public void setStatus(Long studyId, Study.Status newState, User user) { studyRepository.getById(studyId).ifPresent(this::alignWithStudyState); throw new BadRequestException("Study cannot be initialized", e); } + return s; }); } diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/service/UserService.java b/studymanager/src/main/java/io/redlink/more/studymanager/service/UserService.java index 78ed8650..4eb65ba3 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/service/UserService.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/service/UserService.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2022 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.service; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/utils/LoggingUtils.java b/studymanager/src/main/java/io/redlink/more/studymanager/utils/LoggingUtils.java index 690b3a6f..ec16e05b 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/utils/LoggingUtils.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/utils/LoggingUtils.java @@ -1,5 +1,10 @@ /* - * Copyright (c) 2023 Redlink GmbH. + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. */ package io.redlink.more.studymanager.utils; diff --git a/studymanager/src/main/java/io/redlink/more/studymanager/utils/SchedulerUtils.java b/studymanager/src/main/java/io/redlink/more/studymanager/utils/SchedulerUtils.java index d772a74b..1314d986 100644 --- a/studymanager/src/main/java/io/redlink/more/studymanager/utils/SchedulerUtils.java +++ b/studymanager/src/main/java/io/redlink/more/studymanager/utils/SchedulerUtils.java @@ -1,3 +1,11 @@ +/* + * Copyright LBI-DHP and/or licensed to LBI-DHP under one or more + * contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute + * for Digital Health and Prevention -- A research institute of the + * Ludwig Boltzmann Gesellschaft, Österreichische Vereinigung zur + * Förderung der wissenschaftlichen Forschung). + * Licensed under the Elastic License 2.0. + */ package io.redlink.more.studymanager.utils; import biweekly.component.VEvent; @@ -7,19 +15,29 @@ import biweekly.util.com.google.ical.compat.javautil.DateIterator; import io.redlink.more.studymanager.model.Observation; import io.redlink.more.studymanager.model.Trigger; -import io.redlink.more.studymanager.model.scheduler.*; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.stream.Stream; -import org.apache.commons.lang3.Range; -import org.quartz.CronExpression; - +import io.redlink.more.studymanager.model.scheduler.Duration; +import io.redlink.more.studymanager.model.scheduler.Event; +import io.redlink.more.studymanager.model.scheduler.RecurrenceRule; +import io.redlink.more.studymanager.model.scheduler.RelativeDate; +import io.redlink.more.studymanager.model.scheduler.RelativeEvent; +import io.redlink.more.studymanager.model.scheduler.RelativeRecurrenceRule; +import io.redlink.more.studymanager.model.scheduler.ScheduleEvent; import java.sql.Date; import java.text.ParseException; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.TimeZone; +import java.util.stream.Stream; +import org.apache.commons.lang3.Range; +import org.quartz.CronExpression; public final class SchedulerUtils { diff --git a/studymanager/src/main/resources/application.yaml b/studymanager/src/main/resources/application.yaml index c945047c..8a5cd547 100644 --- a/studymanager/src/main/resources/application.yaml +++ b/studymanager/src/main/resources/application.yaml @@ -50,6 +50,9 @@ spring: flyway: out-of-order: true + mvc: + async: + request-timeout: -1 server: forward-headers-strategy: framework error: diff --git a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml index 3dfd1569..be8085ce 100644 --- a/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml +++ b/studymanager/src/main/resources/openapi/StudyManagerAPI.yaml @@ -11,21 +11,13 @@ servers: description: Local Test Server paths: /components/{componentType}: + parameters: + - $ref: '#/components/parameters/ComponentType' get: tags: - components operationId: listComponents description: List component of certain type - parameters: - - name: componentType - in: path - schema: - type: string - enum: - - action - - trigger - - observation - required: true responses: '200': description: Components successfully returned @@ -37,26 +29,18 @@ paths: $ref: '#/components/schemas/ComponentFactory' /components/{componentType}/{componentId}/validate: + parameters: + - $ref: '#/components/parameters/ComponentType' + - name: componentId + in: path + schema: + type: string + required: true post: tags: - components description: check if properties are valid for component operationId: validateProperties - parameters: - - name: componentType - in: path - schema: - type: string - enum: - - action - - trigger - - observation - required: true - - name: componentId - in: path - schema: - type: string - required: true requestBody: content: application/json: @@ -73,31 +57,23 @@ paths: description: Not found /components/{componentType}/{componentId}/api/{slug}: + parameters: + - $ref: '#/components/parameters/ComponentType' + - name: componentId + in: path + schema: + type: string + required: true + - name: slug + in: path + schema: + type: string + required: true post: tags: - components operationId: accessModuleSpecificEndpoint description: access module specific endpoint - parameters: - - name: componentType - in: path - schema: - type: string - enum: - - action - - trigger - - observation - required: true - - name: componentId - in: path - schema: - type: string - required: true - - name: slug - in: path - schema: - type: string - required: true requestBody: required: true content: @@ -117,26 +93,18 @@ paths: description: Error /components/{componentType}/{componentId}/web-component.js: + parameters: + - $ref: '#/components/parameters/ComponentType' + - name: componentId + in: path + schema: + type: string + required: true get: tags: - components operationId: getWebComponentScript description: Get web component script - parameters: - - name: componentType - in: path - schema: - type: string - enum: - - action - - trigger - - observation - required: true - - name: componentId - in: path - schema: - type: string - required: true responses: '200': description: Returned script successfully @@ -253,10 +221,20 @@ paths: schema: $ref: '#/components/schemas/StatusChange' responses: - '202': - description: Status changed deleted + '200': + description: Study status changed + content: + application/json: + schema: + $ref: '#/components/schemas/Study' '400': - description: Bad request + description: The requested transition is not allowed + '409': + description: Could not update status due to configuration error + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationReport' /studies/{studyId}/collaborators: parameters: @@ -395,7 +373,7 @@ paths: schema: $ref: '#/components/schemas/StudyTimeline' '404': - description: Not found + description: Not found /studies/{studyId}/studyGroups: @@ -1076,14 +1054,14 @@ paths: format: binary responses: - '201': - description: import was successful - content: - application/json: - schema: - $ref: '#/components/schemas/Study' - '400': - description: bad request + '201': + description: import was successful + content: + application/json: + schema: + $ref: '#/components/schemas/Study' + '400': + description: bad request /studies/{studyId}/export/study: parameters: @@ -1095,7 +1073,7 @@ paths: operationId: exportStudy responses: '200': - description: foo + description: Operation successful content: application/json: schema: @@ -1110,10 +1088,31 @@ paths: tags: - importExport operationId: generateDownloadToken - description: generate a download token that can be used once for download and is valid for 10 minutes + description: Generate a download token that can be used once for download and is valid for 10 minutes + parameters: + - name: studyGroupId + in: query + schema: + type: array + items: + type: integer + - name: participantId + in: query + schema: + type: array + items: + type: integer + - name: observationId + in: query + schema: + type: array + items: + type: integer + - $ref: '#/components/parameters/From' + - $ref: '#/components/parameters/To' responses: '200': - description: foo + description: Operation successful content: application/json: schema: @@ -1124,6 +1123,50 @@ paths: '404': description: study not found + /studies/{studyId}/export/studydata/{token}: + parameters: + - $ref: '#/components/parameters/StudyId' + - name: token + in: path + required: true + schema: + type: string + get: + tags: + - importExport + description: Download Study data + operationId: exportStudyData + parameters: + - name: studyGroupId + in: query + schema: + type: array + items: + type: integer + - name: participantId + in: query + schema: + type: array + items: + type: integer + - name: observationId + in: query + schema: + type: array + items: + type: integer + - $ref: '#/components/parameters/From' + - $ref: '#/components/parameters/To' + responses: + '200': + description: Operation successful + content: + application/json: + schema: + $ref: '#/components/schemas/DataExport' + '404': + description: study not found + /studies/{studyId}/import/participants: parameters: - $ref: '#/components/parameters/StudyId' @@ -1157,7 +1200,7 @@ paths: operationId: exportParticipants responses: '200': - description: foo + description: Operation successful content: text/csv: schema: @@ -1167,13 +1210,13 @@ paths: description: bad request /studies/{studyId}/data: + parameters: + - $ref: '#/components/parameters/StudyId' get: tags: - data description: Get Study Participation Data operationId: getParticipationData - parameters: - - $ref: '#/components/parameters/StudyId' responses: '200': description: Operation successful @@ -1186,14 +1229,71 @@ paths: '404': description: not found + /studies/{studyId}/observations/{observationId}/views: + parameters: + - $ref: '#/components/parameters/StudyId' + - $ref: '#/components/parameters/ObservationId' + get: + operationId: listObservationViews + description: "Get a list of available views" + tags: + - data + responses: + 200: + description: List of available Views + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ObservationDataView' + + /studies/{studyId}/observations/{observationId}/views/{viewName}: + parameters: + - $ref: '#/components/parameters/StudyId' + - $ref: '#/components/parameters/ObservationId' + - name: viewName + in: path + required: true + schema: + type: string + get: + tags: + - data + description: "Get observation view data" + operationId: getObservationViewData + parameters: + - name: studyGroupId + in: query + schema: + type: integer + required: false + - name: participantId + in: query + schema: + type: integer + required: false + - $ref: '#/components/parameters/From' + - $ref: '#/components/parameters/To' + responses: + '200': + description: Operation successful + content: + application/json: + schema: + $ref: '#/components/schemas/ObservationDataViewData' + '404': + description: not found + /studies/{studyId}/datapoints: + parameters: + - $ref: '#/components/parameters/StudyId' get: tags: - data description: Get Data Points operationId: getDataPoints parameters: - - $ref: '#/components/parameters/StudyId' - name: observationId in: query schema: @@ -1209,7 +1309,6 @@ paths: schema: type: string format: date-time - readOnly: true - name: size in: query schema: @@ -1284,6 +1383,19 @@ paths: schema: $ref: '#/components/schemas/FrontendConfiguration' + /config/buildInfo: + get: + tags: + - configuration + description: retrieve the build info + operationId: getBuildInfo + responses: + 200: + description: the build-info + content: + application/json: + schema: + $ref: '#/components/schemas/BuildInfo' components: schemas: @@ -1348,6 +1460,8 @@ components: type: string propertyId: type: string + componentTitle: + type: string type: type: string @@ -1431,7 +1545,6 @@ components: - active - paused - closed - readOnly: true default: draft StudyGroup: @@ -1657,6 +1770,52 @@ components: type: string format: date-time readOnly: true + + ObservationDataView: + type: object + properties: + name: + type: string + label: + type: string + title: + type: string + description: + type: string + required: + - name + + ObservationDataViewData: + type: object + properties: + view: + $ref: '#/components/schemas/ObservationDataView' + chartType: + type: string + enum: + - line + - bar + - pie + labels: + type: array + items: + type: string + data: + type: array + items: + $ref: '#/components/schemas/ObservationDataViewDataRow' + + ObservationDataViewDataRow: + type: object + properties: + label: + type: string + values: + type: array + items: + type: number + format: double + DataPoint: type: object properties: @@ -2003,7 +2162,59 @@ components: - auth readOnly: true + BuildInfo: + type: object + properties: + version: + type: string + default: '0.0.0' + date: + type: string + format: date-time + branch: + type: string + rev: + type: string + required: + - version + - date + DataExport: + type: array + items: + type: object + properties: + datapoint_id: + type: string + participant_id: + type: string + study_id: + type: string + study_group_id: + type: string + nullable: true + observation_id: + type: string + observation_type: + type: string + data_type: + type: string + storage_date: + type: string + effective_time_frame: + type: string + additionalProperties: true + parameters: + ComponentType: + name: componentType + in: path + schema: + type: string + enum: + - action + - trigger + - observation + required: true StudyId: name: studyId in: path @@ -2052,6 +2263,24 @@ components: type: integer format: int32 required: true + DateTime: + name: dateTime + in: query + schema: + type: string + format: date-time + From: + name: from + in: query + schema: + type: string + format: date-time + To: + name: to + in: query + schema: + type: string + format: date-time securitySchemes: OAuth: diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/CalendarControllerTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/CalendarControllerTest.java index 0908610c..1945e85d 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/CalendarControllerTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/CalendarControllerTest.java @@ -10,6 +10,13 @@ import io.redlink.more.studymanager.model.timeline.StudyTimeline; import io.redlink.more.studymanager.service.CalendarService; import io.redlink.more.studymanager.service.OAuth2AuthenticationService; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; import org.apache.commons.lang3.Range; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -21,13 +28,6 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import java.time.Instant; -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.*; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -69,11 +69,11 @@ void testGetStudyTimeline() throws Exception { LocalDate from = LocalDate.of(2024, 2, 1); LocalDate to = LocalDate.of(2024, 5, 1); - when(service.getTimeline(any(), any(), any(), any(OffsetDateTime.class), any(LocalDate.class), any(LocalDate.class))) + when(service.getTimeline(any(), any(), any(), any(Instant.class), any(LocalDate.class), any(LocalDate.class))) .thenAnswer(invocationOnMock -> { return new StudyTimeline( referenceDate, - Range.between(from, to, LocalDate::compareTo), + Range.of(from, to, LocalDate::compareTo), List.of( ObservationTimelineEvent.fromObservation( new Observation() diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/InterventionControllerTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/InterventionControllerTest.java index 0be3b95c..13754225 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/InterventionControllerTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/InterventionControllerTest.java @@ -16,14 +16,17 @@ import io.redlink.more.studymanager.core.properties.TriggerProperties; import io.redlink.more.studymanager.model.Action; import io.redlink.more.studymanager.model.AuthenticatedUser; -import io.redlink.more.studymanager.model.scheduler.Event; import io.redlink.more.studymanager.model.Intervention; import io.redlink.more.studymanager.model.PlatformRole; import io.redlink.more.studymanager.model.Trigger; +import io.redlink.more.studymanager.model.scheduler.Event; import io.redlink.more.studymanager.service.InterventionService; import io.redlink.more.studymanager.service.OAuth2AuthenticationService; import io.redlink.more.studymanager.utils.MapperUtils; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.EnumSet; +import java.util.Map; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -36,17 +39,14 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; -import java.util.Map; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest({InterventionsApiV1Controller.class}) @AutoConfigureMockMvc(addFilters = false) @@ -102,8 +102,8 @@ void testAddIntervention() throws Exception { .purpose("some purpose") .studyGroupId(1) .schedule(new EventDTO() - .dtstart(dateStart.atOffset(ZoneOffset.UTC)) - .dtend(dateEnd.atOffset(ZoneOffset.UTC))); + .dtstart(dateStart) + .dtend(dateEnd)); mvc.perform(post("/api/v1/studies/1/interventions") .content(mapper.writeValueAsString(interventionRequest)) @@ -142,8 +142,8 @@ void testUpdateIntervention() throws Exception { .purpose("some purpose") .title("a title") .schedule(new EventDTO() - .dtstart(dateStart.atOffset(ZoneOffset.UTC)) - .dtend(dateEnd.atOffset(ZoneOffset.UTC))); + .dtstart(dateStart) + .dtend(dateEnd)); mvc.perform(put("/api/v1/studies/1/interventions/1") .content(mapper.writeValueAsString(interventionRequest)) diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationControllerTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationControllerTest.java index a1b91d5d..925a6cf6 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationControllerTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/controller/studymanager/ObservationControllerTest.java @@ -12,13 +12,21 @@ import io.redlink.more.studymanager.api.v1.model.EndpointTokenDTO; import io.redlink.more.studymanager.api.v1.model.ObservationDTO; import io.redlink.more.studymanager.api.v1.model.ObservationScheduleDTO; -import io.redlink.more.studymanager.model.*; +import io.redlink.more.studymanager.model.AuthenticatedUser; +import io.redlink.more.studymanager.model.EndpointToken; +import io.redlink.more.studymanager.model.Observation; +import io.redlink.more.studymanager.model.PlatformRole; import io.redlink.more.studymanager.model.scheduler.Event; import io.redlink.more.studymanager.service.IntegrationService; import io.redlink.more.studymanager.service.OAuth2AuthenticationService; import io.redlink.more.studymanager.service.ObservationService; import io.redlink.more.studymanager.utils.MapperUtils; -import java.time.OffsetDateTime; +import java.time.Instant; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -29,14 +37,14 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import java.time.Instant; -import java.util.*; - -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -168,7 +176,7 @@ void testAddToken() throws Exception{ EndpointTokenDTO token = new EndpointTokenDTO( 1, "testLabel", - OffsetDateTime.now(), + Instant.now(), "test"); when(integrationService.addToken(anyLong(), anyInt(), anyString())) .thenAnswer(invocationOnMock -> Optional.of(new EndpointToken( diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/service/CalendarServiceTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/service/CalendarServiceTest.java index 49119379..4762d820 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/service/CalendarServiceTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/service/CalendarServiceTest.java @@ -14,9 +14,10 @@ import io.redlink.more.studymanager.model.scheduler.RelativeEvent; import io.redlink.more.studymanager.model.scheduler.RelativeRecurrenceRule; import io.redlink.more.studymanager.model.timeline.StudyTimeline; +import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.OffsetDateTime; import java.time.ZoneId; import java.util.List; import java.util.Optional; @@ -52,7 +53,7 @@ class CalendarServiceTest { @Test void testStudyNotFound() { when(studyService.getStudy(any(), any())).thenReturn(Optional.empty()); - assertThrows(NotFoundException.class, () -> calendarService.getTimeline(1L, 1, 1, OffsetDateTime.now(), LocalDate.now(), LocalDate.now())); + assertThrows(NotFoundException.class, () -> calendarService.getTimeline(1L, 1, 1, Instant.now(), LocalDate.now(), LocalDate.now())); } @Test @@ -65,7 +66,7 @@ void testParticipantNotFound() { .setPlannedStartDate(LocalDate.now()) .setPlannedEndDate(LocalDate.now().plusDays(3)) )); - assertThrows(NotFoundException.class, () -> calendarService.getTimeline(1L, 1,1, OffsetDateTime.now(), LocalDate.now(), LocalDate.now())); + assertThrows(NotFoundException.class, () -> calendarService.getTimeline(1L, 1,1, Instant.now(), LocalDate.now(), LocalDate.now())); } @Test @@ -176,10 +177,10 @@ void testGetTimeline() { 1L, 1, 2, - OffsetDateTime.of( + LocalDateTime.of( LocalDate.of(2024, 5, 11), - LocalTime.of(10,10,10), - OffsetDateTime.now().getOffset()), + LocalTime.of(10,10,10) + ).atZone(ZoneId.systemDefault()).toInstant(), LocalDate.of(2024, 5, 9), LocalDate.of(2024,5,17) ); diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/service/DataProcessingServiceTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/service/DataProcessingServiceTest.java index 7c85d0f8..fb247f8a 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/service/DataProcessingServiceTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/service/DataProcessingServiceTest.java @@ -10,7 +10,7 @@ import io.redlink.more.studymanager.model.Observation; import io.redlink.more.studymanager.model.Participant; -import io.redlink.more.studymanager.model.ParticipationData; +import io.redlink.more.studymanager.model.data.ParticipationData; import io.redlink.more.studymanager.model.StudyGroup; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; diff --git a/studymanager/src/test/java/io/redlink/more/studymanager/service/ElasticSearchServiceTest.java b/studymanager/src/test/java/io/redlink/more/studymanager/service/ElasticSearchServiceTest.java index 47efea77..7b119c98 100644 --- a/studymanager/src/test/java/io/redlink/more/studymanager/service/ElasticSearchServiceTest.java +++ b/studymanager/src/test/java/io/redlink/more/studymanager/service/ElasticSearchServiceTest.java @@ -14,18 +14,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.io.Resources; import io.redlink.more.studymanager.configuration.ElasticConfiguration; -import io.redlink.more.studymanager.model.data.ElasticActionDataPoint; -import io.redlink.more.studymanager.model.Study; import io.redlink.more.studymanager.core.io.Timeframe; - -import java.io.ByteArrayOutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.UUID; - +import io.redlink.more.studymanager.model.Study; +import io.redlink.more.studymanager.model.data.ElasticActionDataPoint; import io.redlink.more.studymanager.model.data.ElasticObservationDataPoint; import io.redlink.more.studymanager.utils.MapperUtils; -import org.junit.Assert; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; @@ -38,12 +31,17 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.List; +import java.util.Map; +import java.util.UUID; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @Testcontainers @@ -109,14 +107,14 @@ void testRecordAction() { @Test void testExportData() throws JsonProcessingException, InterruptedException { for (int i = 0; i < 1200; i++) { - setDataPoint(1L, 2, i); + setDataPoint(1L, "2", "1", "1", i); } //wait for auto commit Thread.sleep(2000); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try(outputStream) { outputStream.write("[".getBytes(StandardCharsets.UTF_8)); - elasticService.exportData(1L, outputStream); + elasticService.exportData(outputStream, 1L, List.of(2), List.of(1), List.of(1), null, null); outputStream.write("]".getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { //do nothing than close @@ -126,13 +124,13 @@ void testExportData() throws JsonProcessingException, InterruptedException { assertThat(MapperUtils.MAPPER.readValue(result, List.class)).hasSize(1200); } - private void setDataPoint(Long studyId, int participantId, int i) { + private void setDataPoint(Long studyId, String studyGroupId, String participantId, String observationId, int i) { elasticService.setDataPoint(studyId, new ElasticObservationDataPoint( "DP_" + studyId + "_" + participantId + "_" + i, "participant_"+ participantId, "study_" + studyId, - null, - "2", + "study_group_" + studyGroupId, + observationId, "acc-mobile-observation", "acc-mobile-observation", Instant.now(),