Skip to content

Commit

Permalink
#258 - Add an endpoint that takes a structured query and validates it
Browse files Browse the repository at this point in the history
- Return invalid criteria (termcode/context) instead of just termcode
- Replace old /validate endpoint with the new one
- add swagger documentation for the endpoint
- When no invalid terms are found, return an empty array instead of omitting the parameter
- Don't include the invalid terms list in the structured query itself but create a new object ValidatedStructuredQuery, containing the original query and the list of invalid terms
- Add a new endpoint that takes a structured query and sends back the same query with additional invalidTerms if present
  • Loading branch information
michael-82 committed Mar 19, 2024
1 parent 32ddf69 commit 59b460a
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.numcodex.feasibility_gui_backend.common.api.Criterion;
import de.numcodex.feasibility_gui_backend.common.api.TermCode;
import de.numcodex.feasibility_gui_backend.query.api.validation.StructuredQueryValidation;
import lombok.Builder;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.numcodex.feasibility_gui_backend.query.api;

import com.fasterxml.jackson.annotation.JsonProperty;
import de.numcodex.feasibility_gui_backend.common.api.Criterion;
import lombok.Builder;

import java.util.List;

@Builder
public record ValidatedStructuredQuery(
@JsonProperty StructuredQuery query,
@JsonProperty List<Criterion> invalidCriteria
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.factory.annotation.Qualifier;

import java.util.stream.Collectors;

/**
* Validator for {@link StructuredQuery} that does an actual check based on a JSON schema.
*/
Expand Down Expand Up @@ -47,8 +49,11 @@ public boolean isValid(StructuredQuery structuredQuery,
var jsonSubject = new JSONObject(jsonUtil.writeValueAsString(structuredQuery));
jsonSchema.validate(jsonSubject);
return true;
} catch (ValidationException | JsonProcessingException e) {
log.debug("Structured query is invalid", e);
} catch (ValidationException e) {
log.error("Structured query is invalid: {}", e.getCausingExceptions().stream().map(Throwable::getMessage).collect(Collectors.joining(" ## ")));
return false;
} catch (JsonProcessingException jpe) {
log.debug("Could not process JSON", jpe);
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@
import de.numcodex.feasibility_gui_backend.query.QueryHandlerService;
import de.numcodex.feasibility_gui_backend.query.QueryHandlerService.ResultDetail;
import de.numcodex.feasibility_gui_backend.query.QueryNotFoundException;
import de.numcodex.feasibility_gui_backend.query.api.Query;
import de.numcodex.feasibility_gui_backend.query.api.QueryListEntry;
import de.numcodex.feasibility_gui_backend.query.api.QueryResult;
import de.numcodex.feasibility_gui_backend.query.api.QueryResultRateLimit;
import de.numcodex.feasibility_gui_backend.query.api.SavedQuery;
import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery;
import de.numcodex.feasibility_gui_backend.query.api.*;
import de.numcodex.feasibility_gui_backend.query.api.status.FeasibilityIssue;
import de.numcodex.feasibility_gui_backend.query.api.status.FeasibilityIssues;
import de.numcodex.feasibility_gui_backend.query.api.status.SavedQuerySlots;
Expand Down Expand Up @@ -404,9 +399,14 @@ public ResponseEntity<Object> getQueryContent(
}

@PostMapping("/validate")
public ResponseEntity<Object> validateStructuredQuery(
public ResponseEntity<ValidatedStructuredQuery> validateStructuredQuery(
@Valid @RequestBody StructuredQuery query) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
var invalidCriteria = termCodeValidation.getInvalidCriteria(query);
var validatedQuery = ValidatedStructuredQuery.builder()
.query(query)
.invalidCriteria(invalidCriteria)
.build();
return new ResponseEntity<>(validatedQuery, HttpStatus.OK);
}

private boolean hasAccess(Long queryId, Authentication authentication) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,46 @@ public List<TermCode> getInvalidTermCodes(StructuredQuery structuredQuery) {

return invalidTermCodes;
}

private Criterion removeFilters(Criterion in) {
return Criterion.builder()
.termCodes(in.termCodes())
.context(in.context())
.build();
}

public List<Criterion> getInvalidCriteria(StructuredQuery structuredQuery) {
var invalidCriteria = new ArrayList<Criterion>();

List<List<Criterion>> combinedCriteria;

if (structuredQuery.exclusionCriteria() != null && !structuredQuery.exclusionCriteria().isEmpty()) {
combinedCriteria = Stream.of(
structuredQuery.inclusionCriteria(),
structuredQuery.exclusionCriteria()).flatMap(
Collection::stream).toList();
} else {
combinedCriteria = structuredQuery.inclusionCriteria();
}

for (List<Criterion> criterionList : combinedCriteria) {
for (Criterion criterion : criterionList) {
if (criterion.context() == null) {
log.debug("Missing context");
invalidCriteria.add(removeFilters(criterion));
}
for (TermCode termCode : criterion.termCodes()) {
if (terminologyService.isExistingTermCode(termCode.system(), termCode.code(), termCode.version())) {
log.trace("termcode ok: {} - {} - {}", termCode.system(), termCode.code(), termCode.version());
} else {
log.debug("termcode invalid: {} - {} - {}", termCode.system(), termCode.code(),
termCode.version());
invalidCriteria.add(removeFilters(criterion));
}
}
}
}

return invalidCriteria;
}
}
41 changes: 41 additions & 0 deletions src/main/resources/static/v3/api-docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,35 @@ paths:
security:
- feasibility_auth:
- user
/query/validate:
post:
tags:
- query
- validation
summary: Validates a submitted (structured) query to check for schema violations or invalid termCodes
operationId: validateQuery
requestBody:
description: Structured query to validate
content:
application/json:
schema:
$ref: '#/components/schemas/StructuredQuery'
required: true
responses:
200:
description: Query adheres to json schema. If invalid termCodes are present, they will be in the response.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ValidatedStructuredQuery'
400:
description: Query does not adhere to json schema
content: { }
401:
description: Unauthorized - please login first
content: { }
/query/by-user/{userId}:
get:
tags:
Expand Down Expand Up @@ -987,6 +1016,18 @@ components:
type: array
items:
$ref: "#/components/schemas/CriterionList"
ValidatedStructuredQuery:
type: object
required:
- query
- invalidTerms
properties:
query:
$ref: "#/components/schemas/StructuredQuery"
invalidTerms:
type: array
items:
$ref: "#/components/schemas/TermCode"
SavedQuery:
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,26 +184,36 @@ public void testRunQueryEndpoint_FailsOnSoftQuotaExceeded() throws Exception {

@Test
@WithMockUser(roles = "FEASIBILITY_TEST_USER", username = "test")
public void testValidateQueryEndpoint_SucceedsOnValidQuery() throws Exception {
public void testValidate2QueryEndpoint_SucceedsOnValidQuery() throws Exception {
StructuredQuery testQuery = createValidStructuredQuery();

doReturn(List.of()).when(termCodeValidation).getInvalidTermCodes(any(StructuredQuery.class));

mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + "/validate")).with(csrf())
.contentType(APPLICATION_JSON)
.content(jsonUtil.writeValueAsString(testQuery)))
.andExpect(status().isNoContent());
.andExpect(status().isOk())
.andExpect(jsonPath("$.invalidTerms").isEmpty());
}

@Test
@WithMockUser(roles = "FEASIBILITY_TEST_USER")
public void testValidateQueryEndpoint_FailsOnInvalidStructuredQueryWith400() throws Exception {
var testQuery = StructuredQuery.builder().build();
public void testValidate2QueryEndpoint_SucceedsDespiteInvalidTermcodesWith200() throws Exception {
StructuredQuery testQuery = createValidStructuredQuery();
var invalidTermCode = TermCode.builder()
.code("LL2191-6")
.system("http://loinc.org")
.display("Geschlecht")
.build();

doReturn(List.of(invalidTermCode)).when(termCodeValidation).getInvalidTermCodes(any(StructuredQuery.class));

mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + "/validate")).with(csrf())
.contentType(APPLICATION_JSON)
.content(jsonUtil.writeValueAsString(testQuery)))
.andExpect(status().isBadRequest());
.andExpect(status().isOk())
.andExpect(jsonPath("$.invalidTerms").exists())
.andExpect(jsonPath("$.invalidTerms").isNotEmpty());
}

@Test
Expand Down

0 comments on commit 59b460a

Please sign in to comment.