From dffca953d65b2eb389f91e36ebdc229b5c686dac Mon Sep 17 00:00:00 2001 From: Daniela Tomoiaga Date: Tue, 9 Feb 2021 12:50:09 +0200 Subject: [PATCH] feat(NUM-1095): Validate aql query --- pom.xml | 10 ++ .../controler/AqlEditorAqlController.java | 7 ++ .../dto/aql/QueryValidationResponse.java | 21 ++++ .../org/ehrbase/aqleditor/dto/aql/Result.java | 2 + .../service/AqlEditorAqlService.java | 34 ++++++ .../org.ehrbase.aqleditor/AqlServiceTest.java | 109 ++++++++++++++++++ 6 files changed, 183 insertions(+) create mode 100644 src/main/java/org/ehrbase/aqleditor/dto/aql/QueryValidationResponse.java create mode 100644 src/test/java/org.ehrbase.aqleditor/AqlServiceTest.java 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()); + } +}