Skip to content
This repository has been archived by the owner on Dec 6, 2023. It is now read-only.

Commit

Permalink
Merge pull request #2 from ehrbase/feature/NUM-1095_validate_aql
Browse files Browse the repository at this point in the history
feat(NUM-1095): Validate aql query
  • Loading branch information
alexkarle authored Feb 9, 2021
2 parents ec0323f + dffca95 commit 972165a
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 0 deletions.
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,15 @@
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,4 +51,9 @@ public ResponseEntity<Result> buildAql(@RequestBody AqlDto aqlDto) {
public ResponseEntity<AqlDto> parseAql(@RequestBody Result result) {
return ResponseEntity.ok(aqlEditorAqlService.parseAql(result));
}

@PostMapping("/validate")
public ResponseEntity<QueryValidationResponse> validateAql(@RequestBody @NotNull Result query) {
return ResponseEntity.ok(aqlEditorAqlService.validateAql(query));
}
}
Original file line number Diff line number Diff line change
@@ -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;

}
2 changes: 2 additions & 0 deletions src/main/java/org/ehrbase/aqleditor/dto/aql/Result.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,21 @@

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;
import org.ehrbase.aql.dto.condition.ConditionComparisonOperatorDto;
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;
Expand Down Expand Up @@ -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<ParameterValue> extractParameterValues(ConditionDto conditionDto) {
List<ParameterValue> values = new ArrayList<>();

Expand All @@ -84,4 +99,23 @@ private List<ParameterValue> 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();
}
}
109 changes: 109 additions & 0 deletions src/test/java/org.ehrbase.aqleditor/AqlServiceTest.java
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit 972165a

Please sign in to comment.