diff --git a/.github/scripts/post-elastic-test-queries.sh b/.github/scripts/post-elastic-test-queries.sh
index d37702f5..46658c03 100755
--- a/.github/scripts/post-elastic-test-queries.sh
+++ b/.github/scripts/post-elastic-test-queries.sh
@@ -47,7 +47,7 @@ access_token="$(curl -s --request POST \
--data password=testpassword \
--data scope=openid | jq '.access_token' | tr -d '"')"
-response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v4/terminology/entry/search?searchterm=Blutdruck")
+response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v5/terminology/entry/search?searchterm=Blutdruck")
http_code="${response: -3}"
json_body=$(cat response_body)
@@ -63,7 +63,7 @@ else
exit 1
fi
-response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v4/terminology/entry/$onto_example_id")
+response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v5/terminology/entry/$onto_example_id")
http_code="${response: -3}"
json_body=$(cat response_body)
@@ -79,7 +79,7 @@ else
exit 1
fi
-response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v4/codeable-concept/entry/search?searchterm=Vectorcardiogram")
+response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v5/codeable-concept/entry/search?searchterm=Vectorcardiogram")
http_code="${response: -3}"
json_body=$(cat response_body)
@@ -95,7 +95,7 @@ else
exit 1
fi
-response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v4/codeable-concept/entry?ids=$cc_example_id")
+response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v5/codeable-concept/entry?ids=$cc_example_id")
http_code="${response: -3}"
json_body=$(cat response_body)
diff --git a/.github/scripts/post-test-query.sh b/.github/scripts/post-test-query.sh
index d82fde5f..c0d309b8 100755
--- a/.github/scripts/post-test-query.sh
+++ b/.github/scripts/post-test-query.sh
@@ -10,7 +10,7 @@ access_token="$(curl -s --request POST \
--data scope=openid | jq '.access_token' | tr -d '"')"
response=$(curl -s -i \
- --url http://localhost:8091/api/v4/query \
+ --url http://localhost:8091/api/v5/query/feasibility \
--header "Authorization: Bearer $access_token" \
--header 'Content-Type: application/json' \
--data '{
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0b375f99..10e6c06e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -148,7 +148,7 @@ jobs:
needs: tests
runs-on: ubuntu-latest
env:
- ONTOLOGY_GIT_TAG: v3.0.2-alpha.5
+ ONTOLOGY_GIT_TAG: v3.1.0
ELASTIC_HOST: http://localhost:9200
ELASTIC_FILEPATH: https://github.com/medizininformatik-initiative/fhir-ontology-generator/releases/download/TAGPLACEHOLDER/
ELASTIC_FILENAME: elastic.zip
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3737bb72..c9090479 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+## [UNRELEASED] - 2025-mm-dd
+
+### Changed
+- REST API changed to v5
+### Removed
+- Query Templates have been removed
+
## [6.1.0] - 2025-02-14
- Based on ontology **[v3.1.0](https://github.com/medizininformatik-initiative/fhir-ontology-generator/releases/tag/v3.1.0)**
diff --git a/pom.xml b/pom.xml
index 6f8b9a38..b9533a56 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
de.medizininformatik-initiative
DataportalBackend
- 6.2.0-SNAPSHOT
+ 7.0.0-SNAPSHOT
Dataportal Backend
Backend of the Dataportal
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/config/RateLimitingConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/config/RateLimitingConfig.java
index b15628e8..c74e3a7b 100644
--- a/src/main/java/de/numcodex/feasibility_gui_backend/config/RateLimitingConfig.java
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/config/RateLimitingConfig.java
@@ -1,11 +1,5 @@
package de.numcodex.feasibility_gui_backend.config;
-import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_API;
-import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_DETAILED_OBFUSCATED_RESULT;
-import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_QUERY;
-import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_ID_MATCHER;
-import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_SUMMARY_RESULT;
-
import de.numcodex.feasibility_gui_backend.query.ratelimiting.RateLimitingInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
@@ -13,6 +7,8 @@
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.*;
+
@Component
public class RateLimitingConfig implements WebMvcConfigurer {
@@ -23,7 +19,7 @@ public class RateLimitingConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor)
- .addPathPatterns(PATH_API + PATH_QUERY + PATH_ID_MATCHER + PATH_SUMMARY_RESULT)
- .addPathPatterns(PATH_API + PATH_QUERY + PATH_ID_MATCHER + PATH_DETAILED_OBFUSCATED_RESULT);
+ .addPathPatterns(PATH_API + PATH_QUERY + PATH_FEASIBILITY + PATH_ID_MATCHER + PATH_SUMMARY_RESULT)
+ .addPathPatterns(PATH_API + PATH_QUERY + PATH_FEASIBILITY + PATH_ID_MATCHER + PATH_DETAILED_OBFUSCATED_RESULT);
}
}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java
index 1f6982de..76a6afd3 100644
--- a/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java
@@ -37,17 +37,17 @@ public class WebSecurityConfig {
public static final String KEY_SPRING_ADDONS_CONFIDENTIAL = "spring-addons-confidential";
public static final String KEY_SPRING_ADDONS_PUBLIC = "spring-addons-public";
public static final String PATH_ACTUATOR_HEALTH = "/actuator/health";
- public static final String PATH_API = "/api/v4";
+ public static final String PATH_API = "/api/v5";
public static final String PATH_QUERY = "/query";
+ public static final String PATH_DATA = "/data";
+ public static final String PATH_FEASIBILITY = "/feasibility";
public static final String PATH_ID_MATCHER = "/{id:\\d+}";
public static final String PATH_USER_ID_MATCHER = "/by-user/{id:[\\w-]+}";
- public static final String PATH_SAVED = "/saved";
public static final String PATH_CONTENT = "/content";
public static final String PATH_SUMMARY_RESULT = "/summary-result";
public static final String PATH_DETAILED_OBFUSCATED_RESULT = "/detailed-obfuscated-result";
public static final String PATH_DETAILED_RESULT = "/detailed-result";
public static final String PATH_TERMINOLOGY = "/terminology";
- public static final String PATH_TEMPLATE = "/template";
public static final String PATH_DSE = "/dse";
public static final String PATH_CODEABLE_CONCEPT = "/codeable-concept";
public static final String PATH_SWAGGER_UI = "/swagger-ui/**";
@@ -102,12 +102,11 @@ public SecurityFilterChain apiFilterChain(
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_TERMINOLOGY + "/**")).hasAuthority(keycloakAllowedRole)
- .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY)).hasAuthority(keycloakAllowedRole)
- .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_USER_ID_MATCHER)).hasAuthority(keycloakAdminRole)
- .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_ID_MATCHER + PATH_SAVED)).hasAuthority(keycloakAllowedRole)
- .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_ID_MATCHER + PATH_DETAILED_RESULT)).hasAuthority(keycloakAdminRole)
- .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_TEMPLATE)).hasAuthority(keycloakAllowedRole)
- .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_TEMPLATE + "/*")).hasAuthority(keycloakAllowedRole)
+ .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_DATA)).hasAuthority(keycloakAllowedRole)
+ .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_DATA + PATH_USER_ID_MATCHER)).hasAuthority(keycloakAdminRole)
+ .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_DATA + "/*")).hasAuthority(keycloakAllowedRole)
+ .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_FEASIBILITY)).hasAuthority(keycloakAllowedRole)
+ .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_FEASIBILITY + PATH_ID_MATCHER + PATH_DETAILED_RESULT)).hasAuthority(keycloakAdminRole)
.requestMatchers(new MvcRequestMatcher(introspector, PATH_API + "/**")).hasAnyAuthority(keycloakAdminRole, keycloakAllowedRole)
.requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_DSE + "/**")).hasAnyAuthority(keycloakAdminRole, keycloakAllowedRole)
.requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_CODEABLE_CONCEPT + "/**")).hasAnyAuthority(keycloakAdminRole, keycloakAllowedRole)
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestController.java b/src/main/java/de/numcodex/feasibility_gui_backend/dse/v5/DseRestController.java
similarity index 96%
rename from src/main/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestController.java
rename to src/main/java/de/numcodex/feasibility_gui_backend/dse/v5/DseRestController.java
index c8da0388..a5b12921 100644
--- a/src/main/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestController.java
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/dse/v5/DseRestController.java
@@ -1,4 +1,4 @@
-package de.numcodex.feasibility_gui_backend.dse.v4;
+package de.numcodex.feasibility_gui_backend.dse.v5;
import de.numcodex.feasibility_gui_backend.dse.DseService;
import de.numcodex.feasibility_gui_backend.dse.api.DseProfile;
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java
index 2bfccdec..63ad40df 100644
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java
@@ -3,8 +3,6 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.numcodex.feasibility_gui_backend.query.api.Query;
-import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate;
-import de.numcodex.feasibility_gui_backend.query.api.SavedQuery;
import de.numcodex.feasibility_gui_backend.query.api.*;
import de.numcodex.feasibility_gui_backend.query.dispatch.QueryDispatchException;
import de.numcodex.feasibility_gui_backend.query.dispatch.QueryDispatcher;
@@ -12,21 +10,15 @@
import de.numcodex.feasibility_gui_backend.query.result.RandomSiteNameGenerator;
import de.numcodex.feasibility_gui_backend.query.result.ResultLine;
import de.numcodex.feasibility_gui_backend.query.result.ResultService;
-import de.numcodex.feasibility_gui_backend.query.templates.QueryTemplateException;
-import de.numcodex.feasibility_gui_backend.query.templates.QueryTemplateHandler;
import de.numcodex.feasibility_gui_backend.terminology.validation.StructuredQueryValidation;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
-import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;
-import java.sql.Timestamp;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
@Service
@RequiredArgsConstructor
@@ -41,9 +33,6 @@ public enum ResultDetail {
@NonNull
private final QueryDispatcher queryDispatcher;
- @NonNull
- private final QueryTemplateHandler queryTemplateHandler;
-
@NonNull
private final QueryRepository queryRepository;
@@ -53,12 +42,6 @@ public enum ResultDetail {
@NonNull
private final ResultService resultService;
- @NonNull
- private final QueryTemplateRepository queryTemplateRepository;
-
- @NonNull
- private final SavedQueryRepository savedQueryRepository;
-
@NonNull
private final StructuredQueryValidation structuredQueryValidation;
@@ -101,9 +84,8 @@ public QueryResult getQueryResult(Long queryId, ResultDetail resultDetail) {
public Query getQuery(Long queryId) throws JsonProcessingException {
var query = queryRepository.findById(queryId);
- var savedQuery = savedQueryRepository.findByQueryId(queryId);
if (query.isPresent()) {
- return convertQueryToApi(query.get(), savedQuery);
+ return convertQueryToApi(query.get());
} else {
return null;
}
@@ -118,186 +100,20 @@ public StructuredQuery getQueryContent(Long queryId) throws JsonProcessingExcept
}
}
- public Long storeQueryTemplate(QueryTemplate queryTemplate, String userId)
- throws QueryTemplateException {
- return queryTemplateHandler.storeTemplate(queryTemplate, userId);
- }
-
- public Long saveQuery(Long queryId, String userId, SavedQuery savedQueryApi) {
- if (savedQueryRepository.existsSavedQueryByLabelAndUserId(savedQueryApi.label(), userId)) {
- throw new DataIntegrityViolationException(String.format("User %s already has a saved query named %s", userId, savedQueryApi.label()));
- }
- de.numcodex.feasibility_gui_backend.query.persistence.SavedQuery savedQuery = convertSavedQueryApiToPersistence(savedQueryApi, queryId);
- return savedQueryRepository.save(savedQuery).getId();
- }
-
- public void updateSavedQuery(Long queryId, SavedQuery savedQuery) throws QueryNotFoundException {
- Optional savedQueryOptional = savedQueryRepository.findByQueryId(queryId);
- if (savedQueryOptional.isEmpty()) {
- throw new QueryNotFoundException();
- }
- var oldSavedQuery = savedQueryOptional.get();
- oldSavedQuery.setLabel(savedQuery.label());
- oldSavedQuery.setComment(savedQuery.comment());
- savedQueryRepository.save(oldSavedQuery);
- }
-
- public de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate getQueryTemplate(
- Long queryId, String authorId) throws QueryTemplateException {
- de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate queryTemplate = queryTemplateRepository.findById(
- queryId).orElseThrow(QueryTemplateException::new);
- if (!queryTemplate.getQuery().getCreatedBy().equalsIgnoreCase(authorId)) {
- throw new QueryTemplateException();
- }
- return queryTemplate;
- }
-
- public List getQueryTemplatesForAuthor(
- String authorId) {
- return queryTemplateRepository.findByAuthor(authorId);
- }
-
- public void updateQueryTemplate(Long queryTemplateId, QueryTemplate queryTemplate, String authorId) throws QueryTemplateException {
- var templates = getQueryTemplatesForAuthor(authorId);
- Optional templateToUpdate = templates.stream().
- filter(t -> t.getId().equals(queryTemplateId)).
- findFirst();
-
- if (templateToUpdate.isPresent()) {
- var template = templateToUpdate.get();
- template.setLabel(queryTemplate.label());
- template.setComment(queryTemplate.comment());
- template.setLastModified(Timestamp.from(Instant.now()));
- queryTemplateRepository.save(template);
- } else {
- throw new QueryTemplateException("not found");
- }
- }
-
- public void deleteQueryTemplate(Long queryTemplateId, String authorId) throws QueryTemplateException {
- var templates = getQueryTemplatesForAuthor(authorId);
- Optional templateToDelete = templates.stream().
- filter(t -> t.getId().equals(queryTemplateId)).
- findFirst();
-
- if (templateToDelete.isPresent()) {
- queryTemplateRepository.delete(templateToDelete.get());
- } else {
- throw new QueryTemplateException("not found");
- }
- }
-
- public QueryTemplate convertTemplatePersistenceToApi(
- de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate in)
- throws JsonProcessingException {
- return queryTemplateHandler.convertPersistenceToApi(in);
- }
-
- private Query convertQueryToApi(de.numcodex.feasibility_gui_backend.query.persistence.Query in,
- Optional savedQuery)
- throws JsonProcessingException {
-
- if (savedQuery.isPresent()) {
- return Query.builder()
- .id(in.getId())
- .content(jsonUtil.readValue(in.getQueryContent().getQueryContent(), StructuredQuery.class))
- .label(savedQuery.get().getLabel())
- .comment(savedQuery.get().getComment())
- .totalNumberOfPatients(savedQuery.get().getResultSize())
- .build();
- } else {
- return Query.builder()
- .id(in.getId())
- .content(jsonUtil.readValue(in.getQueryContent().getQueryContent(), StructuredQuery.class))
- .build();
- }
- }
-
- private de.numcodex.feasibility_gui_backend.query.persistence.SavedQuery convertSavedQueryApiToPersistence(
- SavedQuery in, Long queryId) {
- var out = new de.numcodex.feasibility_gui_backend.query.persistence.SavedQuery();
- out.setQuery(queryRepository.getReferenceById(queryId));
- out.setComment(in.comment());
- out.setLabel(in.label());
- out.setResultSize(in.totalNumberOfPatients());
- return out;
- }
- public List getQueryListForAuthor(
- String userId, boolean savedOnly) {
- Optional> queries;
+ private Query convertQueryToApi(de.numcodex.feasibility_gui_backend.query.persistence.Query in)
+ throws JsonProcessingException {
- if (savedOnly) {
- queries = queryRepository.findSavedQueriesByAuthor(userId);
- } else {
- queries = queryRepository.findByAuthor(userId);
- }
-
- return queries.orElseGet(ArrayList::new);
+ return Query.builder()
+ .id(in.getId())
+ .content(jsonUtil.readValue(in.getQueryContent().getQueryContent(), StructuredQuery.class))
+ .build();
}
public String getAuthorId(Long queryId) throws QueryNotFoundException {
return queryRepository.getAuthor(queryId).orElseThrow(QueryNotFoundException::new);
}
- public QueryListEntry convertQueryToQueryListEntry(de.numcodex.feasibility_gui_backend.query.persistence.Query query,
- boolean skipValidation) {
- boolean isValid = true;
- if (!skipValidation) {
- try {
- var sq = jsonUtil.readValue(query.getQueryContent().getQueryContent(), StructuredQuery.class);
- isValid = structuredQueryValidation.isValid(sq);
- } catch (JsonProcessingException e) {
- isValid = false;
- }
- }
-
- if (query.getSavedQuery() != null) {
- if (skipValidation) {
- return
- QueryListEntry.builder()
- .id(query.getId())
- .label(query.getSavedQuery().getLabel())
- .comment(query.getSavedQuery().getComment())
- .totalNumberOfPatients(query.getSavedQuery().getResultSize())
- .createdAt(query.getCreatedAt())
- .build();
- } else {
- return
- QueryListEntry.builder()
- .id(query.getId())
- .label(query.getSavedQuery().getLabel())
- .comment(query.getSavedQuery().getComment())
- .totalNumberOfPatients(query.getSavedQuery().getResultSize())
- .createdAt(query.getCreatedAt())
- .isValid(isValid)
- .build();
- }
- } else {
- if (skipValidation) {
- return
- QueryListEntry.builder()
- .id(query.getId())
- .createdAt(query.getCreatedAt())
- .build();
- } else {
- return
- QueryListEntry.builder()
- .id(query.getId())
- .createdAt(query.getCreatedAt())
- .isValid(isValid)
- .build();
- }
- }
- }
-
- public List convertQueriesToQueryListEntries(List queryList,
- boolean skipValidation) {
- var ret = new ArrayList();
- queryList.forEach(q -> ret.add(convertQueryToQueryListEntry(q, skipValidation)));
- return ret;
- }
-
public Long getAmountOfQueriesByUserAndInterval(String userId, int minutes) {
return queryRepository.countQueriesByAuthorInTheLastNMinutes(userId, minutes);
}
@@ -309,18 +125,4 @@ public Long getRetryAfterTime(String userId, int offset, long interval) {
return 0L;
}
}
-
- public Long getAmountOfSavedQueriesByUser(String userId) {
- var queries = queryRepository.findSavedQueriesByAuthor(userId);
- return queries.map(queryList -> (long) queryList.size()).orElse(0L);
- }
-
- public void deleteSavedQuery(Long queryId) throws QueryNotFoundException {
- var savedQueryOptional = savedQueryRepository.findByQueryId(queryId);
- if (savedQueryOptional.isPresent()) {
- savedQueryRepository.deleteById(savedQueryOptional.get().getId());
- } else {
- throw new QueryNotFoundException();
- }
- }
}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Crtdl.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Crtdl.java
new file mode 100644
index 00000000..a0cb7324
--- /dev/null
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Crtdl.java
@@ -0,0 +1,13 @@
+package de.numcodex.feasibility_gui_backend.query.api;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+
+@Builder
+public record Crtdl(
+ @JsonProperty String version,
+ @JsonProperty String display,
+ @JsonProperty StructuredQuery cohortDefinition,
+ @JsonProperty DataExtraction dataExtraction
+) {
+}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/QueryTemplate.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/DataExtraction.java
similarity index 79%
rename from src/main/java/de/numcodex/feasibility_gui_backend/query/api/QueryTemplate.java
rename to src/main/java/de/numcodex/feasibility_gui_backend/query/api/DataExtraction.java
index d3877d7d..6017d03c 100644
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/QueryTemplate.java
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/DataExtraction.java
@@ -3,13 +3,11 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
-import de.numcodex.feasibility_gui_backend.query.api.validation.QueryTemplateValidation;
import lombok.Builder;
@JsonInclude(Include.NON_NULL)
-@QueryTemplateValidation
@Builder
-public record QueryTemplate(
+public record DataExtraction(
@JsonProperty long id,
@JsonProperty StructuredQuery content,
@JsonProperty String label,
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/SavedQuery.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Dataquery.java
similarity index 61%
rename from src/main/java/de/numcodex/feasibility_gui_backend/query/api/SavedQuery.java
rename to src/main/java/de/numcodex/feasibility_gui_backend/query/api/Dataquery.java
index 24114474..cd5b6aea 100644
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/SavedQuery.java
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Dataquery.java
@@ -7,10 +7,15 @@
@JsonInclude(Include.NON_NULL)
@Builder
-public record SavedQuery(
+public record Dataquery(
+ @JsonProperty long id,
+ @JsonProperty Crtdl content,
@JsonProperty String label,
@JsonProperty String comment,
- @JsonProperty Long totalNumberOfPatients
+ @JsonProperty String lastModified,
+ @JsonProperty String createdBy,
+ @JsonProperty Boolean isValid,
+ @JsonProperty Long resultSize
) {
}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplatePassValidator.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplatePassValidator.java
deleted file mode 100644
index fa13235a..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplatePassValidator.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.api.validation;
-
-import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate;
-import jakarta.validation.ConstraintValidator;
-import jakarta.validation.ConstraintValidatorContext;
-
-/**
- * Validator for {@link QueryTemplate} that always passes no matter what instance gets checked.
- */
-public class QueryTemplatePassValidator implements ConstraintValidator {
- @Override
- public boolean isValid(QueryTemplate queryTemplate, ConstraintValidatorContext constraintValidatorContext) {
- return true;
- }
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidation.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidation.java
deleted file mode 100644
index 383d6726..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidation.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.api.validation;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import jakarta.validation.Constraint;
-import jakarta.validation.Payload;
-
-@Target({ElementType.TYPE})
-@Retention(RetentionPolicy.RUNTIME)
-@Constraint(validatedBy = QueryTemplateValidator.class)
-public @interface QueryTemplateValidation {
- String message() default "Query template is invalid";
-
- Class>[] groups() default {};
-
- Class extends Payload>[] payload() default {};
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidator.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidator.java
deleted file mode 100644
index 362621ac..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidator.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.api.validation;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate;
-import jakarta.validation.ConstraintValidator;
-import jakarta.validation.ConstraintValidatorContext;
-import lombok.NonNull;
-import lombok.extern.slf4j.Slf4j;
-import org.everit.json.schema.Schema;
-import org.everit.json.schema.ValidationException;
-import org.json.JSONObject;
-import org.springframework.beans.factory.annotation.Qualifier;
-
-/**
- * Validator for {@link QueryTemplate} that does an actual check based on a JSON schema.
- */
-@Slf4j
-public class QueryTemplateValidator implements ConstraintValidator {
-
- @NonNull
- private Schema jsonSchema;
-
- @NonNull
- private ObjectMapper jsonUtil;
-
- /**
- * Required args constructor.
- *
- * Lombok annotation had to be removed since it could not take the necessary Schema Qualifier
- */
- public QueryTemplateValidator(@Qualifier(value = "validation-template") Schema jsonSchema, ObjectMapper jsonUtil) {
- this.jsonSchema = jsonSchema;
- this.jsonUtil = jsonUtil;
- }
-
- /**
- * Validate the submitted {@link QueryTemplate} against the json query schema.
- *
- * @param queryTemplate the {@link QueryTemplate} to validate
- */
- @Override
- public boolean isValid(QueryTemplate queryTemplate,
- ConstraintValidatorContext constraintValidatorContext) {
- try {
- var jsonSubject = new JSONObject(jsonUtil.writeValueAsString(queryTemplate));
- jsonSchema.validate(jsonSubject);
- return true;
- } catch (ValidationException | JsonProcessingException e) {
- log.debug("Stored query is invalid", e);
- return false;
- }
- }
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidatorSpringConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidatorSpringConfig.java
deleted file mode 100644
index 9c63057c..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidatorSpringConfig.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.api.validation;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate;
-import java.io.InputStream;
-import jakarta.validation.ConstraintValidator;
-import lombok.extern.slf4j.Slf4j;
-import org.everit.json.schema.Schema;
-import org.everit.json.schema.loader.SchemaClient;
-import org.everit.json.schema.loader.SchemaLoader;
-import org.json.JSONObject;
-import org.json.JSONTokener;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@Slf4j
-public class QueryTemplateValidatorSpringConfig {
-
- private static final String JSON_SCHEMA = "query-template-schema.json";
-
- @Value("${app.enableQueryValidation}")
- private boolean enabled;
-
- @Bean
- public ConstraintValidator createStoredQueryValidator(
- @Qualifier("validation-template") Schema schema) {
- return enabled
- ? new QueryTemplateValidator(schema, new ObjectMapper())
- : new QueryTemplatePassValidator();
- }
-
- @Qualifier("validation-template")
- @Bean
- public Schema createStoredQueryValidatorJsonSchema() {
- InputStream inputStream = QueryTemplateValidator.class.getResourceAsStream(JSON_SCHEMA);
- var jsonSchema = new JSONObject(new JSONTokener(inputStream));
- SchemaLoader loader = SchemaLoader.builder()
- .schemaClient(SchemaClient.classPathAwareClient())
- .schemaJson(jsonSchema)
- .resolutionScope("classpath://query/")
- .draftV7Support()
- .build();
- return loader.load().build();
- }
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryException.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryException.java
new file mode 100644
index 00000000..44cb1a72
--- /dev/null
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryException.java
@@ -0,0 +1,12 @@
+package de.numcodex.feasibility_gui_backend.query.dataquery;
+
+public class DataqueryException extends Exception {
+
+ public DataqueryException() {
+ super();
+ }
+
+ public DataqueryException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandler.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandler.java
new file mode 100644
index 00000000..2e5ed5a3
--- /dev/null
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandler.java
@@ -0,0 +1,146 @@
+package de.numcodex.feasibility_gui_backend.query.dataquery;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.numcodex.feasibility_gui_backend.query.api.Crtdl;
+import de.numcodex.feasibility_gui_backend.query.api.Dataquery;
+import de.numcodex.feasibility_gui_backend.query.api.status.SavedQuerySlots;
+import de.numcodex.feasibility_gui_backend.query.persistence.DataqueryRepository;
+import jakarta.transaction.Transactional;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Transactional
+@RequiredArgsConstructor
+public class DataqueryHandler {
+
+ @NonNull
+ private ObjectMapper jsonUtil;
+
+ @NonNull
+ private DataqueryRepository dataqueryRepository;
+
+ @NonNull
+ private Integer maxDataqueriesPerUser;
+
+ public Long storeDataquery(@NonNull Dataquery dataquery, @NonNull String userId) throws DataqueryException, DataqueryStorageFullException {
+
+ // By definition, a user can save an unlimited amount of queries without result
+ if (dataquery.resultSize() != null && dataqueryRepository.countByCreatedByWhereResultIsNotNull(userId) >= maxDataqueriesPerUser) {
+ throw new DataqueryStorageFullException();
+ }
+
+ var tmp = Dataquery.builder()
+ .resultSize(dataquery.resultSize())
+ .content(dataquery.content())
+ .label(dataquery.label())
+ .comment(dataquery.comment())
+ .createdBy(userId)
+ .build();
+
+ try {
+ de.numcodex.feasibility_gui_backend.query.persistence.Dataquery dataqueryEntity = convertApiToPersistence(tmp);
+ dataqueryEntity = dataqueryRepository.save(dataqueryEntity);
+ return dataqueryEntity.getId();
+ } catch (JsonProcessingException e) {
+ throw new DataqueryException(e.getMessage());
+ }
+ }
+
+ public Dataquery getDataqueryById(Long dataqueryId, String userId) throws DataqueryException, JsonProcessingException {
+ de.numcodex.feasibility_gui_backend.query.persistence.Dataquery dataquery = dataqueryRepository.findById(dataqueryId).orElseThrow(DataqueryException::new);
+ if (dataquery.getCreatedBy() == null || !dataquery.getCreatedBy().equals(userId)) {
+ throw new DataqueryException();
+ }
+ return convertPersistenceToApi(dataquery);
+ }
+
+ public void updateDataquery(Long queryId, Dataquery dataquery, String userId) throws DataqueryException, DataqueryStorageFullException, JsonProcessingException {
+ var usedSlots = dataqueryRepository.countByCreatedByWhereResultIsNotNull(userId);
+ var existingDataquery = dataqueryRepository.findById(queryId).orElseThrow(DataqueryException::new);
+
+ if (usedSlots >= maxDataqueriesPerUser) {
+ // Only throw an exception when the updated query contains a result and the original didn't
+ if (dataquery.resultSize() != null && existingDataquery.getResultSize() == null) {
+ throw new DataqueryStorageFullException();
+ }
+ }
+
+ if (existingDataquery.getCreatedBy().equals(userId)) {
+ var dataqueryToUpdate = convertApiToPersistence(dataquery);
+ dataqueryToUpdate.setId(existingDataquery.getId());
+ dataqueryToUpdate.setCreatedBy(userId);
+ dataqueryToUpdate.setLastModified(Timestamp.valueOf(LocalDateTime.now()));
+ dataqueryRepository.save(dataqueryToUpdate);
+ } else {
+ throw new DataqueryException();
+ }
+ }
+
+ public List getDataqueriesByAuthor(String userId) throws DataqueryException {
+ List dataqueries = dataqueryRepository.findAllByCreatedBy(userId);
+ List ret = new ArrayList<>();
+
+ for (de.numcodex.feasibility_gui_backend.query.persistence.Dataquery dataquery : dataqueries) {
+ try {
+ ret.add(convertPersistenceToApi(dataquery));
+ } catch (JsonProcessingException e) {
+ throw new DataqueryException();
+ }
+ }
+
+ return ret;
+ }
+
+ public void deleteDataquery(Long dataqueryId, String userId) throws DataqueryException {
+ de.numcodex.feasibility_gui_backend.query.persistence.Dataquery dataquery = dataqueryRepository.findById(dataqueryId).orElseThrow(DataqueryException::new);
+ if (!dataquery.getCreatedBy().equals(userId)) {
+ throw new DataqueryException();
+ } else {
+ dataqueryRepository.delete(dataquery);
+ }
+ }
+
+ public SavedQuerySlots getDataquerySlotsJson(String userId) {
+ var queryAmount = dataqueryRepository.countByCreatedByWhereResultIsNotNull(userId);
+
+ return SavedQuerySlots.builder()
+ .used(queryAmount)
+ .total(maxDataqueriesPerUser)
+ .build();
+ }
+
+ public de.numcodex.feasibility_gui_backend.query.persistence.Dataquery convertApiToPersistence(de.numcodex.feasibility_gui_backend.query.api.Dataquery in) throws JsonProcessingException {
+ de.numcodex.feasibility_gui_backend.query.persistence.Dataquery out = new de.numcodex.feasibility_gui_backend.query.persistence.Dataquery();
+ out.setId(in.id() > 0 ? in.id() : null);
+ out.setLabel(in.label());
+ out.setComment(in.comment());
+ if (in.lastModified() != null) {
+ out.setLastModified(Timestamp.valueOf(in.lastModified()));
+ }
+ out.setCreatedBy(in.createdBy());
+ out.setResultSize(in.resultSize());
+ out.setCrtdl(jsonUtil.writeValueAsString(in.content()));
+ return out;
+ }
+
+ public de.numcodex.feasibility_gui_backend.query.api.Dataquery convertPersistenceToApi(de.numcodex.feasibility_gui_backend.query.persistence.Dataquery in) throws JsonProcessingException {
+ return de.numcodex.feasibility_gui_backend.query.api.Dataquery.builder()
+ .id(in.getId())
+ .label(in.getLabel())
+ .comment(in.getComment())
+ .createdBy(in.getCreatedBy())
+ .resultSize(in.getResultSize())
+ .lastModified(in.getLastModified() == null ? null : in.getLastModified().toString())
+ .content(jsonUtil.readValue(in.getCrtdl(), Crtdl.class))
+ .build();
+ }
+}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataquerySpringConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataquerySpringConfig.java
new file mode 100644
index 00000000..a3834f72
--- /dev/null
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataquerySpringConfig.java
@@ -0,0 +1,21 @@
+package de.numcodex.feasibility_gui_backend.query.dataquery;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.numcodex.feasibility_gui_backend.query.persistence.DataqueryRepository;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class DataquerySpringConfig {
+
+ @Bean
+ public DataqueryHandler createDataqueryHandler(
+ @Qualifier("translation") ObjectMapper jsonUtil,
+ DataqueryRepository dataqueryRepository,
+ @Value("${app.maxSavedQueriesPerUser}") Integer maxSavedQueriesPerUser
+ ) {
+ return new DataqueryHandler(jsonUtil, dataqueryRepository, maxSavedQueriesPerUser);
+ }
+}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryStorageFullException.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryStorageFullException.java
new file mode 100644
index 00000000..4ff437de
--- /dev/null
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryStorageFullException.java
@@ -0,0 +1,8 @@
+package de.numcodex.feasibility_gui_backend.query.dataquery;
+
+public class DataqueryStorageFullException extends Exception {
+
+ public DataqueryStorageFullException() {
+ super();
+ }
+}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Dataquery.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Dataquery.java
new file mode 100644
index 00000000..484e8888
--- /dev/null
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Dataquery.java
@@ -0,0 +1,52 @@
+package de.numcodex.feasibility_gui_backend.query.persistence;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.sql.Timestamp;
+import java.util.Objects;
+
+@Getter
+@Setter
+@ToString
+@RequiredArgsConstructor
+@Entity
+public class Dataquery {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "created_by", updatable = false)
+ private String createdBy;
+
+ @Column(name = "label", nullable = false)
+ private String label;
+
+ @Column(name = "comment")
+ private String comment;
+
+ @Column(name = "crtdl")
+ private String crtdl;
+
+ @Column(name = "last_modified", insertable = false)
+ private Timestamp lastModified;
+
+ @Column(name = "result_size")
+ private Long resultSize;
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ Dataquery dataquery = (Dataquery) o;
+ return Objects.equals(id, dataquery.id) && Objects.equals(createdBy, dataquery.createdBy) && Objects.equals(label, dataquery.label) && Objects.equals(comment, dataquery.comment) && Objects.equals(crtdl, dataquery.crtdl) && Objects.equals(lastModified, dataquery.lastModified) && Objects.equals(resultSize, dataquery.resultSize);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, createdBy, label, comment, crtdl, lastModified, resultSize);
+ }
+}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/DataqueryRepository.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/DataqueryRepository.java
new file mode 100644
index 00000000..c5d8fec1
--- /dev/null
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/DataqueryRepository.java
@@ -0,0 +1,13 @@
+package de.numcodex.feasibility_gui_backend.query.persistence;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
+
+public interface DataqueryRepository extends JpaRepository {
+ List findAllByCreatedBy(String userId);
+
+ @Query(value = "SELECT COUNT(*) FROM Dataquery WHERE createdBy = ?1 AND resultSize IS NOT NULL")
+ Long countByCreatedByWhereResultIsNotNull(String userId);
+}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Query.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Query.java
index bf615eb1..0176c670 100644
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Query.java
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Query.java
@@ -29,9 +29,6 @@ public class Query {
@ToString.Exclude
private QueryContent queryContent;
- @OneToOne(mappedBy = "query", cascade = CascadeType.ALL)
- private SavedQuery savedQuery;
-
@Override
public final boolean equals(Object o) {
if (this == o) return true;
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryRepository.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryRepository.java
index 4e5e4db6..58138250 100644
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryRepository.java
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryRepository.java
@@ -9,9 +9,6 @@ public interface QueryRepository extends JpaRepository {
@org.springframework.data.jpa.repository.Query("SELECT t FROM Query t WHERE t.createdBy = ?1")
Optional> findByAuthor(String authorId);
- @org.springframework.data.jpa.repository.Query("SELECT t FROM Query t left join SavedQuery s on t.id = s.query.id WHERE t.createdBy = ?1 AND s.id IS NOT NULL")
- Optional> findSavedQueriesByAuthor(String authorId);
-
@org.springframework.data.jpa.repository.Query("SELECT t.createdBy FROM Query t WHERE t.id = ?1")
Optional getAuthor(Long queryId);
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplate.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplate.java
deleted file mode 100644
index e2f51c50..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplate.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.persistence;
-
-import jakarta.persistence.*;
-import lombok.*;
-import org.hibernate.proxy.HibernateProxy;
-
-import java.sql.Timestamp;
-import java.util.Objects;
-
-@Getter
-@Setter
-@ToString
-@RequiredArgsConstructor
-@Entity
-public class QueryTemplate {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(referencedColumnName = "id", name = "query_id")
- @ToString.Exclude
- private Query query;
-
- @Column(name = "label", nullable = false)
- private String label;
-
- @Column(name = "comment")
- private String comment;
-
- @Column(name = "last_modified", insertable = false)
- private Timestamp lastModified;
-
- @Override
- public final boolean equals(Object o) {
- if (this == o) return true;
- if (o == null) return false;
- Class> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
- Class> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
- if (thisEffectiveClass != oEffectiveClass) return false;
- QueryTemplate that = (QueryTemplate) o;
- return getId() != null && Objects.equals(getId(), that.getId());
- }
-
- @Override
- public final int hashCode() {
- return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
- }
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplateRepository.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplateRepository.java
deleted file mode 100644
index 3de68ba7..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplateRepository.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.persistence;
-
-import java.util.List;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-
-public interface QueryTemplateRepository extends JpaRepository {
-
- @Query("SELECT qt FROM QueryTemplate qt left join Query q ON qt.query.id = q.id WHERE q.createdBy = ?1")
- List findByAuthor(String authorId);
-
- @Query("select case when count(qt) > 0 then true else false end from QueryTemplate qt left join Query q on qt.query.id = q.id where qt.label =?1 and q.createdBy = ?2")
- boolean existsQueryTemplateByLabelAndUserId(String label, String authorId);
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQuery.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQuery.java
deleted file mode 100644
index abd9b682..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQuery.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.persistence;
-
-import jakarta.persistence.*;
-import lombok.*;
-import org.hibernate.proxy.HibernateProxy;
-
-import java.util.Objects;
-
-@Getter
-@Setter
-@ToString
-@RequiredArgsConstructor
-@Entity
-public class SavedQuery {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @JoinColumn(referencedColumnName = "id", name = "query_id", nullable = false)
- @OneToOne(fetch = FetchType.LAZY)
- @ToString.Exclude
- private Query query;
-
- @Column(name = "label", nullable = false)
- private String label;
-
- @Column(name = "comment")
- private String comment;
-
- @Column(name = "result_size")
- private Long resultSize;
-
- @Override
- public final boolean equals(Object o) {
- if (this == o) return true;
- if (o == null) return false;
- Class> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
- Class> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
- if (thisEffectiveClass != oEffectiveClass) return false;
- SavedQuery that = (SavedQuery) o;
- return getId() != null && Objects.equals(getId(), that.getId());
- }
-
- @Override
- public final int hashCode() {
- return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
- }
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQueryRepository.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQueryRepository.java
deleted file mode 100644
index 3a51334e..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQueryRepository.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.persistence;
-
-import java.util.Optional;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-
-public interface SavedQueryRepository extends JpaRepository {
-
- @Query("SELECT sq FROM SavedQuery sq left join Query q ON sq.query.id = q.id WHERE q.id = ?1")
- Optional findByQueryId(Long queryId);
-
- @Query("select case when count(sq) > 0 then true else false end from SavedQuery sq left join Query q on sq.query.id = q.id where sq.label =?1 and q.createdBy = ?2")
- boolean existsSavedQueryByLabelAndUserId(String label, String authorId);
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptor.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptor.java
index 39da211f..3a4f8673 100644
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptor.java
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptor.java
@@ -2,7 +2,7 @@
import de.numcodex.feasibility_gui_backend.config.WebSecurityConfig;
import de.numcodex.feasibility_gui_backend.query.api.status.FeasibilityIssue;
-import de.numcodex.feasibility_gui_backend.query.v4.QueryHandlerRestController;
+import de.numcodex.feasibility_gui_backend.query.v5.FeasibilityQueryHandlerRestController;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
@@ -136,14 +136,14 @@ public void afterCompletion(HttpServletRequest request,
if (request.getRequestURI()
.endsWith(WebSecurityConfig.PATH_DETAILED_OBFUSCATED_RESULT) && response.containsHeader(
- QueryHandlerRestController.HEADER_X_DETAILED_OBFUSCATED_RESULT_WAS_EMPTY)) {
+ FeasibilityQueryHandlerRestController.HEADER_X_DETAILED_OBFUSCATED_RESULT_WAS_EMPTY)) {
var authentication = SecurityContextHolder.getContext()
.getAuthentication();
var detailedObfuscatedResultTokenBucket = rateLimitingService.resolveViewDetailedObfuscatedBucket(
authentication.getName());
detailedObfuscatedResultTokenBucket.addTokens(1);
response.setHeader(
- QueryHandlerRestController.HEADER_X_DETAILED_OBFUSCATED_RESULT_WAS_EMPTY,
+ FeasibilityQueryHandlerRestController.HEADER_X_DETAILED_OBFUSCATED_RESULT_WAS_EMPTY,
null);
}
}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingService.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingService.java
index 215c2804..452d4a38 100644
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingService.java
+++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingService.java
@@ -1,9 +1,6 @@
package de.numcodex.feasibility_gui_backend.query.ratelimiting;
-import io.github.bucket4j.Bandwidth;
-import io.github.bucket4j.BandwidthBuilder;
import io.github.bucket4j.Bucket;
-import io.github.bucket4j.Refill;
import lombok.Getter;
import java.time.Duration;
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateException.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateException.java
deleted file mode 100644
index 2a527a57..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateException.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.templates;
-
-public class QueryTemplateException extends Exception {
-
- /**
- * Constructs a new {@link QueryTemplateException} without further details.
- */
- public QueryTemplateException() {
- super();
- }
-
- /**
- * Constructs a new {@link QueryTemplateException} with the specified detail message.
- *
- * @param message The detail message.
- */
- public QueryTemplateException(String message) {
- super(message);
- }
-
- /**
- * Constructs a new {@link QueryTemplateException} with the specified detail message and cause.
- *
- * @param message The detail message.
- * @param cause The cause.
- */
- public QueryTemplateException(String message, Throwable cause) {
- super(message, cause);
- }
-
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateHandler.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateHandler.java
deleted file mode 100644
index 6a0ce08b..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateHandler.java
+++ /dev/null
@@ -1,117 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.templates;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate;
-import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery;
-import de.numcodex.feasibility_gui_backend.query.dispatch.QueryHashCalculator;
-import de.numcodex.feasibility_gui_backend.query.persistence.Query;
-import de.numcodex.feasibility_gui_backend.query.persistence.QueryContent;
-import de.numcodex.feasibility_gui_backend.query.persistence.QueryContentRepository;
-import de.numcodex.feasibility_gui_backend.query.persistence.QueryRepository;
-import de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplateRepository;
-import java.sql.Timestamp;
-import java.time.Instant;
-import jakarta.transaction.Transactional;
-import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.dao.DataIntegrityViolationException;
-
-@Slf4j
-@Transactional
-@RequiredArgsConstructor
-public class QueryTemplateHandler {
-
- @NonNull
- private QueryHashCalculator queryHashCalculator;
-
- @NonNull
- private ObjectMapper jsonUtil;
-
- @NonNull
- private QueryRepository queryRepository;
-
- @NonNull
- private QueryContentRepository queryContentRepository;
-
- @NonNull
- private QueryTemplateRepository queryTemplateRepository;
-
- public Long storeTemplate(QueryTemplate queryTemplateApi, String userId)
- throws QueryTemplateException {
-
- if (queryTemplateRepository.existsQueryTemplateByLabelAndUserId(queryTemplateApi.label(), userId)) {
- throw new DataIntegrityViolationException(String.format("User %s already has a query template named %s", userId, queryTemplateApi.label()));
- }
-
- Long queryId = storeNewQuery(queryTemplateApi.content(), userId);
- de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate queryTemplate
- = convertApiToPersistence(queryTemplateApi, queryId);
- queryTemplate = queryTemplateRepository.save(queryTemplate);
- return queryTemplate.getId();
- }
-
- public Long storeNewQuery(StructuredQuery query, String userId) throws QueryTemplateException {
- var querySerialized = serializedStructuredQuery(query);
-
- var queryHash = queryHashCalculator.calculateSerializedQueryBodyHash(querySerialized);
- var queryBody = queryContentRepository.findByHash(queryHash)
- .orElseGet(() -> {
- var freshQueryBody = new QueryContent(querySerialized);
- freshQueryBody.setHash(queryHash);
- return queryContentRepository.save(freshQueryBody);
- });
-
- var queryId = persistQuery(queryBody, userId);
- log.info("enqueued query '{}'", queryId);
- return queryId;
- }
-
- private Long persistQuery(QueryContent queryBody, String userId) {
- var feasibilityQuery = new Query();
- feasibilityQuery.setCreatedAt(Timestamp.from(Instant.now()));
- feasibilityQuery.setCreatedBy(userId);
- feasibilityQuery.setQueryContent(queryBody);
- return queryRepository.save(feasibilityQuery).getId();
- }
-
- private String serializedStructuredQuery(StructuredQuery query) throws QueryTemplateException {
- if (query == null) {
- throw new QueryTemplateException("Submitted query is null");
- }
- try {
- return jsonUtil.writeValueAsString(query);
- } catch (JsonProcessingException e) {
- throw new QueryTemplateException("could not serialize query in order to save it for a template", e);
- }
- }
-
- public de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate convertApiToPersistence(
- QueryTemplate in, Long queryId) {
- de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate out = new de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate();
-
- out.setQuery(queryRepository.getReferenceById(queryId));
- out.setComment(in.comment());
- out.setLabel(in.label());
- if (in.lastModified() != null) {
- out.setLastModified(Timestamp.valueOf(in.lastModified()));
- }
- return out;
- }
-
- public QueryTemplate convertPersistenceToApi(
- de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate in)
- throws JsonProcessingException {
-
- ObjectMapper jsonUtil = new ObjectMapper();
- return QueryTemplate.builder()
- .id(in.getId())
- .content(jsonUtil.readValue(in.getQuery().getQueryContent().getQueryContent(), StructuredQuery.class))
- .label(in.getLabel())
- .comment(in.getComment())
- .lastModified(in.getLastModified().toString())
- .createdBy(in.getQuery().getCreatedBy())
- .build();
- }
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateSpringConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateSpringConfig.java
deleted file mode 100644
index 2068c702..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateSpringConfig.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.templates;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import de.numcodex.feasibility_gui_backend.query.dispatch.QueryHashCalculator;
-import de.numcodex.feasibility_gui_backend.query.persistence.QueryContentRepository;
-import de.numcodex.feasibility_gui_backend.query.persistence.QueryRepository;
-import de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplateRepository;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class QueryTemplateSpringConfig {
-
- @Bean
- public QueryTemplateHandler createQueryTemplateHandler(
- QueryHashCalculator queryHashCalculator,
- @Qualifier("translation") ObjectMapper jsonUtil,
- QueryRepository queryRepository,
- QueryContentRepository queryContentRepository,
- QueryTemplateRepository queryTemplateRepository) {
- return new QueryTemplateHandler(queryHashCalculator, jsonUtil,
- queryRepository, queryContentRepository, queryTemplateRepository);
- }
-}
diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryTemplateHandlerRestController.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryTemplateHandlerRestController.java
deleted file mode 100644
index b95f925b..00000000
--- a/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryTemplateHandlerRestController.java
+++ /dev/null
@@ -1,160 +0,0 @@
-package de.numcodex.feasibility_gui_backend.query.v4;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import de.numcodex.feasibility_gui_backend.query.QueryHandlerService;
-import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate;
-import de.numcodex.feasibility_gui_backend.query.templates.QueryTemplateException;
-import de.numcodex.feasibility_gui_backend.terminology.validation.StructuredQueryValidation;
-
-import java.security.Principal;
-import java.util.ArrayList;
-
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.validation.Valid;
-import jakarta.ws.rs.core.Context;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.dao.DataIntegrityViolationException;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
-import org.springframework.web.util.UriComponentsBuilder;
-
-import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.*;
-
-/*
-Rest Interface for the UI to send and receive query templates from the backend.
-*/
-@RequestMapping(PATH_API + PATH_QUERY + PATH_TEMPLATE)
-@RestController("QueryTemplateHandlerRestController-v4")
-@Slf4j
-@CrossOrigin(origins = "${cors.allowedOrigins}", exposedHeaders = "Location")
-public class QueryTemplateHandlerRestController {
-
- private final QueryHandlerService queryHandlerService;
- private final StructuredQueryValidation structuredQueryValidation;
- private final String apiBaseUrl;
-
- public QueryTemplateHandlerRestController(QueryHandlerService queryHandlerService,
- StructuredQueryValidation structuredQueryValidation, @Value("${app.apiBaseUrl}") String apiBaseUrl) {
- this.queryHandlerService = queryHandlerService;
- this.structuredQueryValidation = structuredQueryValidation;
- this.apiBaseUrl = apiBaseUrl;
- }
-
- @PostMapping(path = "")
- public ResponseEntity