diff --git a/pom.xml b/pom.xml
index fbae7a2..8e40297 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,5 +80,15 @@
lombok
provided
+
+ org.mockito
+ mockito-core
+ test
+
+
+ junit
+ junit
+ test
+
diff --git a/src/main/java/org/ehrbase/aqleditor/controler/AqlEditorAqlController.java b/src/main/java/org/ehrbase/aqleditor/controler/AqlEditorAqlController.java
index 657667a..c867cc6 100644
--- a/src/main/java/org/ehrbase/aqleditor/controler/AqlEditorAqlController.java
+++ b/src/main/java/org/ehrbase/aqleditor/controler/AqlEditorAqlController.java
@@ -19,8 +19,10 @@
package org.ehrbase.aqleditor.controler;
+import com.sun.istack.NotNull;
import lombok.AllArgsConstructor;
import org.ehrbase.aql.dto.AqlDto;
+import org.ehrbase.aqleditor.dto.aql.QueryValidationResponse;
import org.ehrbase.aqleditor.dto.aql.Result;
import org.ehrbase.aqleditor.service.AqlEditorAqlService;
import org.springframework.http.MediaType;
@@ -49,4 +51,9 @@ public ResponseEntity buildAql(@RequestBody AqlDto aqlDto) {
public ResponseEntity parseAql(@RequestBody Result result) {
return ResponseEntity.ok(aqlEditorAqlService.parseAql(result));
}
+
+ @PostMapping("/validate")
+ public ResponseEntity validateAql(@RequestBody @NotNull Result query) {
+ return ResponseEntity.ok(aqlEditorAqlService.validateAql(query));
+ }
}
diff --git a/src/main/java/org/ehrbase/aqleditor/dto/aql/QueryValidationResponse.java b/src/main/java/org/ehrbase/aqleditor/dto/aql/QueryValidationResponse.java
new file mode 100644
index 0000000..6ccc336
--- /dev/null
+++ b/src/main/java/org/ehrbase/aqleditor/dto/aql/QueryValidationResponse.java
@@ -0,0 +1,21 @@
+package org.ehrbase.aqleditor.dto.aql;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class QueryValidationResponse {
+
+ private boolean valid;
+ private String message;
+ private String startLine;
+ private String startColumn;
+ private String error;
+
+}
diff --git a/src/main/java/org/ehrbase/aqleditor/dto/aql/Result.java b/src/main/java/org/ehrbase/aqleditor/dto/aql/Result.java
index c66fbc3..0386eba 100644
--- a/src/main/java/org/ehrbase/aqleditor/dto/aql/Result.java
+++ b/src/main/java/org/ehrbase/aqleditor/dto/aql/Result.java
@@ -22,8 +22,10 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+@Builder
@Data
@AllArgsConstructor
public class Result {
diff --git a/src/main/java/org/ehrbase/aqleditor/service/AqlEditorAqlService.java b/src/main/java/org/ehrbase/aqleditor/service/AqlEditorAqlService.java
index c3f21df..6824428 100644
--- a/src/main/java/org/ehrbase/aqleditor/service/AqlEditorAqlService.java
+++ b/src/main/java/org/ehrbase/aqleditor/service/AqlEditorAqlService.java
@@ -21,8 +21,11 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.ehrbase.aql.binder.AqlBinder;
import org.ehrbase.aql.dto.AqlDto;
@@ -30,7 +33,9 @@
import org.ehrbase.aql.dto.condition.ConditionDto;
import org.ehrbase.aql.dto.condition.ConditionLogicalOperatorDto;
import org.ehrbase.aql.dto.condition.ParameterValue;
+import org.ehrbase.aql.parser.AqlParseException;
import org.ehrbase.aql.parser.AqlToDtoParser;
+import org.ehrbase.aqleditor.dto.aql.QueryValidationResponse;
import org.ehrbase.aqleditor.dto.aql.Result;
import org.ehrbase.client.aql.query.EntityQuery;
import org.ehrbase.client.aql.record.Record;
@@ -66,6 +71,16 @@ public AqlDto parseAql(Result result) {
return aqlDto;
}
+ public QueryValidationResponse validateAql(Result query) {
+ try {
+ new AqlToDtoParser().parse(query.getQ());
+ } catch (AqlParseException e) {
+ return buildResponse(e.getMessage());
+ }
+
+ return QueryValidationResponse.builder().valid(true).message("Query is valid").build();
+ }
+
private List extractParameterValues(ConditionDto conditionDto) {
List values = new ArrayList<>();
@@ -84,4 +99,23 @@ private List extractParameterValues(ConditionDto conditionDto) {
return values;
}
+
+ public QueryValidationResponse buildResponse(String errorMessage) {
+ if (StringUtils.isEmpty(errorMessage)) {
+ return QueryValidationResponse.builder().valid(false).build();
+ }
+
+ Pattern pattern = Pattern.compile("^.*line (\\d+): char (\\d+) (.*).*$");
+ Matcher matcher = pattern.matcher(errorMessage);
+
+ if (matcher.matches()) {
+ String line = matcher.group(1);
+ String column = matcher.group(2);
+ String error = matcher.group(3);
+ return QueryValidationResponse.builder().valid(false).error(error).message(errorMessage)
+ .startColumn(column).startLine(line).build();
+ }
+
+ return QueryValidationResponse.builder().valid(false).message(errorMessage).build();
+ }
}
diff --git a/src/test/java/org.ehrbase.aqleditor/AqlServiceTest.java b/src/test/java/org.ehrbase.aqleditor/AqlServiceTest.java
new file mode 100644
index 0000000..fbcaf13
--- /dev/null
+++ b/src/test/java/org.ehrbase.aqleditor/AqlServiceTest.java
@@ -0,0 +1,109 @@
+package org.ehrbase.aqleditor;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+
+import org.apache.commons.lang3.StringUtils;
+import org.ehrbase.aqleditor.dto.aql.QueryValidationResponse;
+import org.ehrbase.aqleditor.dto.aql.Result;
+import org.ehrbase.aqleditor.service.AqlEditorAqlService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AqlServiceTest {
+
+ @InjectMocks
+ private AqlEditorAqlService aqlService;
+
+ private final String INVALID_QUERY = "Select TOP 13 FORWARD where (o0/data[at0001]/events[at0at0001]/events[at0002]/data[at0003]/items[at0004]/value/magnitude < 1.1)";
+ private final String VALID_QUERY = "Select TOP 13 FORWARD o0/data[at0001]/events[at0002]/data[at0003]/items[at0004]/value/magnitude as Systolic__magnitude, e/ehr_id/value as ehr_id from EHR e contains OBSERVATION o0[openEHR-EHR-OBSERVATION.sample_blood_pressure.v1] where (o0/data[at0001]/events[at0002]/data[at0003]/items[at0004]/value/magnitude >= $magnitude and o0/data[at0001]/events[at0002]/data[at0003]/items[at0004]/value/magnitude < 1.1)";
+
+ @Test
+ public void shouldCorrectlyValidateAql1() {
+ Result aql = Result.builder().q("invalid aql").build();
+ QueryValidationResponse response = aqlService.validateAql(aql);
+
+ assertThat(response, notNullValue());
+ assertThat(response.isValid(), is(false));
+
+ assertThat(response.getStartColumn(), is("0"));
+ assertThat(response.getStartLine(), is("1"));
+ assertThat(response.getMessage(),
+ containsStringIgnoringCase("AQL Parse exception: line 1: char 0"));
+ assertThat(response.getError(),
+ containsStringIgnoringCase("mismatched input 'invalid' expecting SELECT"));
+ }
+
+ @Test
+ public void shouldCorrectlyValidateAql2() {
+
+ Result aql = Result.builder().q(INVALID_QUERY).build();
+ QueryValidationResponse response = aqlService.validateAql(aql);
+
+ assertThat(response, notNullValue());
+ assertThat(response.isValid(), is(false));
+
+ assertThat(response.getStartColumn(), is("23"));
+ assertThat(response.getStartLine(), is("1"));
+ assertThat(response.getMessage(),
+ containsStringIgnoringCase("AQL Parse exception: line 1: char 23"));
+ assertThat(response.getError(),
+ containsStringIgnoringCase("mismatched input 'where' expecting {DISTINCT, FUNCTION_IDENTIFIER, EXTENSION_IDENTIFIER, IDENTIFIER, INTEGER, STRING}"));
+ }
+
+ @Test
+ public void shouldCorrectlyValidateAql3() {
+ Result aql = Result.builder().q(VALID_QUERY).build();
+ QueryValidationResponse response = aqlService.validateAql(aql);
+
+ assertThat(response, notNullValue());
+ assertThat(response.isValid(), is(true));
+ assertThat(response.getMessage(), containsStringIgnoringCase("Query is valid"));
+ }
+
+ @Test
+ public void shouldCorrectlyBuildErrorResponseWhenLineAndCharMissing() {
+
+ QueryValidationResponse response = aqlService.buildResponse("AQL Parse exception:");
+
+ assertThat(response, notNullValue());
+ assertThat(response.isValid(), is(false));
+
+ assertThat(response.getStartColumn(), nullValue());
+ assertThat(response.getStartLine(), nullValue());
+ assertThat(response.getMessage(),
+ containsStringIgnoringCase("AQL Parse exception:"));
+ }
+
+ @Test
+ public void shouldCorrectlyBuildErrorResponseWhenEmpty() {
+
+ QueryValidationResponse response = aqlService.buildResponse(StringUtils.EMPTY);
+
+ assertThat(response, notNullValue());
+ assertThat(response.isValid(), is(false));
+
+ assertThat(response.getStartColumn(), nullValue());
+ assertThat(response.getStartLine(), nullValue());
+ assertThat(response.getMessage(), nullValue());
+ }
+
+ @Test
+ public void shouldCorrectlyBuildErrorResponseWhenNull() {
+
+ QueryValidationResponse response = aqlService.buildResponse(null);
+
+ assertThat(response, notNullValue());
+ assertThat(response.isValid(), is(false));
+
+ assertThat(response.getStartColumn(), nullValue());
+ assertThat(response.getStartLine(), nullValue());
+ assertThat(response.getMessage(), nullValue());
+ }
+}