patientSet = new HashSet<>();
- for (UUID ehr : ehrs) {
- String subjectId = ehrService.getSubjectExtRef(ehr.toString());
- // check if patient token is available and if it matches OR internal reference is null
- if (tokenPatient.equals(subjectId) || subjectId == null) {
- // matches OR EHR's external ref is null, so add our subject from token
- patientSet.add(tokenPatient);
- } else {
- // doesn't match -> requesting data for patient X with token for patient Y
- return false;
- }
-
- }
- // put result set into the requestMap and exit
- requestMap.put(PATIENT, patientSet);
- return true;
- } else {
- throw new InternalServerException("ABAC: AQL audit patient data unavailable.");
- }
- } else {
- throw new InternalServerException("ABAC: AQL audit patient data malformed.");
- }
- }
-
- // in all other cases just handle the one String "subject" variable
- // check if matches (to block accessing patient X with token from patient Y) OR null reference
- if (tokenPatient.equals(subject) || subject == null) {
- // matches OR EHR's external ref is null, so add our subject from token
- requestMap.put(PATIENT, tokenPatient);
- } else {
- // doesn't match -> requesting data for patient X with token for patient Y
- return false;
- }
-
- return true;
- }
-
- /**
- * Handles template ID extraction of specific payload.
- *
- * Payload will be a response body string, in case of @PostAuthorize.
- *
- * Payload will be request body string, or already deserialized object (e.g. EhrStatus), in case of @PreAuthorize.
- * @param type Object type of scope
- * @param payload Payload object, either request's input or response's output
- * @param contentType Content type from the scope
- * @param requestMap ABAC request attribute map to add the result
- * @param authType Pre- or PostAuthorize, determines payload style (string or object)
- */
- private void templateHandling(String type, Object payload, String contentType, Map requestMap, String authType) {
- switch (type) {
- case BaseController.EHR:
- throw new IllegalArgumentException("ABAC: Unsupported configuration: Can't set template ID for EHR type.");
- case BaseController.EHR_STATUS:
- throw new IllegalArgumentException("ABAC: Unsupported configuration: Can't set template ID for EHR_STATUS type.");
- case BaseController.COMPOSITION:
- String content = "";
- if (authType.equals(POST)) {
- // @PostAuthorize gives a ResponseEntity type for "returnObject", so payload is of that type
- if (((ResponseEntity) payload).hasBody()) {
- Object body = ((ResponseEntity) payload).getBody();
- // can have "No content" here (even with some data in the body) if the compo was (logically) deleted
- if (((ResponseEntity>) payload).getStatusCode().equals(HttpStatus.NO_CONTENT)) {
- if (body instanceof Map) {
- Object error = ((Map, ?>) body).get("error");
- if (error != null) {
- if (((String) error).contains("delet")) {
- //composition was deleted, so nothing to check here, skip
- break;
- }
- }
- }
- throw new InternalServerException("ABAC: Unexpected empty response from composition reuquest");
- }
- if (body instanceof OriginalVersionResponseData) {
- // case of versioned_composition --> fast path, because template is easy to get
- if (((OriginalVersionResponseData>) body).getData() instanceof Composition) {
- String template = Objects.requireNonNull(
- ((Composition) ((OriginalVersionResponseData>) body).getData())
- .getArchetypeDetails().getTemplateId()).getValue();
- requestMap.put(TEMPLATE, template);
- break; // special case, so done here, exit
- }
- } else if (body instanceof String) {
- content = (String) body;
- } else {
- throw new InternalServerException("ABAC: unexpected composition payload object");
- }
- } else {
- throw new InternalServerException("ABAC: unexpected empty response body");
- }
- } else if (authType.equals(PRE)) {
- try {
- // try if this is the Delete composition case. Payload would contain the UUID of the compo.
- ObjectVersionId versionId = new ObjectVersionId((String) payload);
- UUID compositionUid = UUID.fromString(versionId.getRoot().getValue());
- Optional compoDto = compositionService.retrieve(compositionUid, null);
- if (compoDto.isPresent()) {
- Composition c = compoDto.get().getComposition();
- requestMap.put(TEMPLATE, c.getArchetypeDetails().getTemplateId().getValue());
- break; // special case, so done here, exit
- } else {
- throw new InternalServerException(
- "ABAC: unexpected empty response from composition delete");
- }
- } catch (IllegalArgumentException e) {
- // if not an UUID, the payload is a composition itself so continue
- content = (String) payload;
- }
- } else {
- throw new InternalServerException("ABAC: invalid auth type given.");
- }
- String templateId;
- if (MediaType.parseMediaType(contentType).isCompatibleWith(MediaType.APPLICATION_JSON)) {
- templateId = compositionService.getTemplateIdFromInputComposition(content, CompositionFormat.JSON);
- } else if (MediaType.parseMediaType(contentType).isCompatibleWith(MediaType.APPLICATION_XML)) {
- templateId = compositionService.getTemplateIdFromInputComposition(content, CompositionFormat.XML);
- } else {
- throw new IllegalArgumentException("ABAC: Only JSON and XML composition are supported.");
- }
- requestMap.put(TEMPLATE, templateId);
- break;
- case BaseController.CONTRIBUTION:
- CompositionFormat format;
- if (MediaType.parseMediaType(contentType).isCompatibleWith(MediaType.APPLICATION_JSON)) {
- format = CompositionFormat.JSON;
- } else if (MediaType.parseMediaType(contentType).isCompatibleWith(MediaType.APPLICATION_XML)) {
- format = CompositionFormat.XML;
- } else {
- throw new IllegalArgumentException("ABAC: Only JSON and XML composition are supported.");
- }
- if (payload instanceof String) {
- Set templates = contributionService.getListOfTemplates((String) payload, format);
- requestMap.put(TEMPLATE, templates);
- break;
- } else {
- throw new InternalServerException("ABAC: invalid POST contribution payload.");
- }
- case BaseController.QUERY:
- // special case of type QUERY, where multiple subjects are possible
- if (payload instanceof Map) {
- if (((Map, ?>) payload).containsKey(AuditVariables.TEMPLATE_PATH)) {
- Set templates = (Set) ((Map, ?>) payload).get(AuditVariables.TEMPLATE_PATH);
- Set templateSet = new HashSet<>(templates);
- // put result set into the requestMap and exit
- requestMap.put(TEMPLATE, templateSet);
- break;
- } else {
- throw new InternalServerException("ABAC: AQL audit template data unavailable.");
- }
- } else {
- throw new InternalServerException("ABAC: AQL audit template data malformed.");
- }
- default:
- throw new InternalServerException("ABAC: Invalid type given from Pre- or PostAuthorize");
- }
- }
-
- private boolean abacCheckRequest(String url, Map bodyMap)
- throws IOException, InterruptedException {
- // prepare request attributes and convert from to
- Map request = new HashMap<>();
- if (bodyMap.containsKey(ORGANIZATION)) {
- request.put(ORGANIZATION, (String) bodyMap.get(ORGANIZATION));
- }
- // check if patient attribues are available and see if it contains a Set or simple String
- if (bodyMap.containsKey(PATIENT)) {
- if (bodyMap.get(PATIENT) instanceof Set) {
- // check if templates are also configured
- if (bodyMap.containsKey(TEMPLATE)) {
- if (bodyMap.get(TEMPLATE) instanceof Set) {
- // multiple templates possible: need cartesian product of n patients and m templates
- // so: for each patient, go through templates and do a request each
- Set setP = (Set) bodyMap.get(PATIENT);
- for (String p : setP) {
- request.put(PATIENT, p);
- boolean success = sendRequestForEach(TEMPLATE, url, bodyMap, request);
- if (!success) {
- return false;
- }
- }
- // in case all combinations were validated successfully
- return true;
- }
- } else {
- // only patients (or + orga) set. So run request for each patient, without template.
- return sendRequestForEach(PATIENT, url, bodyMap, request);
- }
- } else if (bodyMap.get(PATIENT) instanceof String) {
- request.put(PATIENT, (String) bodyMap.get(PATIENT));
- } else {
- // if it is just a String, set it and continue normal
- throw new InternalServerException("ABAC: Invalid patient attribute content.");
- }
- }
- // check if template attributes are available and see if it contains a Set or simple String
- if (bodyMap.containsKey(TEMPLATE)) {
- if (bodyMap.get(TEMPLATE) instanceof Set) {
- // set each template and send separate ABAC requests
- return sendRequestForEach(TEMPLATE, url, bodyMap, request);
- } else if (bodyMap.get(TEMPLATE) instanceof String) {
- // if it is just a String, set it and continue normal
- request.put(TEMPLATE, (String) bodyMap.get(TEMPLATE));
- } else {
- throw new InternalServerException("ABAC: Invalid template attribute content.");
- }
- }
- return abacCheck.execute(url, request);
- }
-
- /**
- * Goes through all template IDs and sends an ABAC request for each.
- * @param type Type, either ORGANIZATION, TEMPLATE, PATIENT
- * @param url ABAC server request URL
- * @param bodyMap Unprocessed attributes for the request
- * @param request Processed attributes for the request
- * @return True on success, False if one combinations is rejected by the ABAC server
- * @throws IOException On error during attribute or HTTP handling
- * @throws InterruptedException On error during HTTP handling
- */
- private boolean sendRequestForEach(String type, String url, Map bodyMap,
- Map request) throws IOException, InterruptedException {
- Set set = (Set) bodyMap.get(type);
- for (String s : set) {
- request.put(type, s);
- boolean allowed = abacCheck.execute(url, request);
- if (!allowed) {
- // if only one combination of attributes is rejected by ABAC return false for all
- return false;
- }
- }
- // in case all combinations were validated successfully
- return true;
- }
-
- /**
- * Extracts the JWT auth token.
- * @param auth Auth object.
- * @return JWT Auth Token
- */
- private JwtAuthenticationToken getJwtAuthenticationToken(Authentication auth) {
- JwtAuthenticationToken jwt;
- if (auth instanceof JwtAuthenticationToken) {
- jwt = (JwtAuthenticationToken) auth;
- } else {
- throw new IllegalArgumentException("ABAC: Invalid authentication, no JWT available.");
- }
- return jwt;
- }
-
- @Override
- public Object getFilterObject() {
- return this.filterObject;
- }
-
- @Override
- public void setFilterObject(Object filterObject) {
- this.filterObject = filterObject;
- }
-
- @Override
- public Object getReturnObject() {
- return this.returnObject;
- }
-
- @Override
- public void setReturnObject(Object returnObject) {
- this.returnObject = returnObject;
- }
-
- @Override
- public Object getThis() {
- return this;
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/abac/MethodSecurityConfig.java b/application/src/main/java/org/ehrbase/application/abac/MethodSecurityConfig.java
deleted file mode 100644
index a18cb87274..0000000000
--- a/application/src/main/java/org/ehrbase/application/abac/MethodSecurityConfig.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (c) 2021 Jake Smolka (Hannover Medical School) and Vitasystems GmbH.
- *
- * This file is part of project EHRbase
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.abac;
-
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
-import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
-import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
-
-@ConditionalOnProperty(name = "abac.enabled")
-@Configuration
-@EnableGlobalMethodSecurity(prePostEnabled = true)
-public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
-
- private final AbacConfig abacConfig;
-
- public MethodSecurityConfig(AbacConfig abacConfig) {
- this.abacConfig = abacConfig;
- }
-
- /**
- * Registration of custom SpEL expressions, here to include ABAC checks.
- */
- @Override
- protected MethodSecurityExpressionHandler createExpressionHandler() {
- // "null" for beans here, but autowiring will make the beans available on runtime
- return new CustomMethodSecurityExpressionHandler(abacConfig, null, null, null, null);
- }
-
-}
diff --git a/application/src/main/java/org/ehrbase/application/cli/EhrBaseCli.java b/application/src/main/java/org/ehrbase/application/cli/EhrBaseCli.java
new file mode 100644
index 0000000000..8bf902a44e
--- /dev/null
+++ b/application/src/main/java/org/ehrbase/application/cli/EhrBaseCli.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2019-2024 vitasystems GmbH.
+ *
+ * This file is part of project EHRbase
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.ehrbase.application.cli;
+
+import java.util.Map;
+import org.ehrbase.cli.CliConfiguration;
+import org.ehrbase.cli.CliRunner;
+import org.ehrbase.configuration.EhrBaseCliConfiguration;
+import org.springframework.boot.Banner;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.context.annotation.Import;
+
+@SpringBootApplication(exclude = {WebMvcAutoConfiguration.class, RedisAutoConfiguration.class})
+@Import({EhrBaseCliConfiguration.class, CliConfiguration.class})
+public class EhrBaseCli implements CommandLineRunner {
+
+ public static SpringApplication build(String[] args) {
+ return new SpringApplicationBuilder(EhrBaseCli.class)
+ .web(WebApplicationType.NONE)
+ .headless(true)
+ .properties(Map.of(
+ "spring.main.allow-bean-definition-overriding", "true",
+ "spring.banner.location", "classpath:banner-cli.txt"))
+ .bannerMode(Banner.Mode.CONSOLE)
+ .logStartupInfo(false)
+ .build(args);
+ }
+
+ private final CliRunner cliRunner;
+
+ public EhrBaseCli(CliRunner cliRunner) {
+ this.cliRunner = cliRunner;
+ }
+
+ @Override
+ public void run(String... args) {
+ cliRunner.run(args);
+ }
+}
diff --git a/application/src/main/java/org/ehrbase/application/config/HttpClientConfig.java b/application/src/main/java/org/ehrbase/application/config/HttpClientConfig.java
deleted file mode 100644
index 53a2d7131c..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/HttpClientConfig.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (c) 2021 Vitasystems GmbH and Jake Smolka (Hannover Medical School).
- *
- * This file is part of project EHRbase
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package org.ehrbase.application.config;
-
-import java.net.InetSocketAddress;
-import java.net.ProxySelector;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpClient.Redirect;
-import java.net.http.HttpClient.Version;
-import java.time.Duration;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@EnableConfigurationProperties
-@ConfigurationProperties(prefix = "httpclient")
-public class HttpClientConfig {
-
- private HttpClient client;
-
- private URI proxy;
- private int proxyPort;
-
- /**
- * General HTTP client with central configuration.
- */
- public HttpClient getClient() {
- if (this.client == null) {
- var builder = HttpClient.newBuilder()
- .version(Version.HTTP_2)
- .followRedirects(Redirect.NEVER)
- .connectTimeout(Duration.ofSeconds(20));
-
- if (proxy != null && proxyPort != 0) {
- builder.proxy(ProxySelector.of(new InetSocketAddress(proxy.toString(), proxyPort)));
- }
-
- // TODO: allow configuration of authentication
- //builder.authenticator(Authenticator.getDefault());
-
- this.client = builder.build();
- }
- return client;
- }
-
- public URI getProxy() {
- return proxy;
- }
-
- public void setProxy(URI proxy) {
- this.proxy = proxy;
- }
-
- public int getProxyPort() {
- return proxyPort;
- }
-
- public void setProxyPort(int proxyPort) {
- this.proxyPort = proxyPort;
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/JacksonConfiguration.java b/application/src/main/java/org/ehrbase/application/config/JacksonConfiguration.java
deleted file mode 100644
index eaca874549..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/JacksonConfiguration.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (c) 2019 Stefan Spiska (Vitasystems GmbH) and Hannover Medical School.
- *
- * This file is part of project EHRbase
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config;
-
-import com.fasterxml.jackson.dataformat.xml.XmlMapper;
-import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import com.nedap.archie.rm.RMObject;
-import com.nedap.archie.rm.directory.Folder;
-import com.nedap.archie.rm.ehr.EhrStatus;
-import org.ehrbase.api.mapper.StructuredStringJSonSerializer;
-import org.ehrbase.response.ehrscape.StructuredString;
-import org.ehrbase.serialisation.mapper.RmObjectJsonDeSerializer;
-import org.ehrbase.serialisation.mapper.RmObjectJsonSerializer;
-import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
-import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
-import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
-
-@Configuration
-public class JacksonConfiguration {
-
- @Bean
- public Jackson2ObjectMapperBuilderCustomizer addCustomSerialization() {
- return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder
- .serializerByType(StructuredString.class, new StructuredStringJSonSerializer())
- .serializerByType(RMObject.class, new RmObjectJsonSerializer())
- .deserializerByType(EhrStatus.class, new RmObjectJsonDeSerializer())
- .deserializerByType(Folder.class, new RmObjectJsonDeSerializer())
- .modules(new JavaTimeModule());
- }
-
- @Bean
- @Primary
- public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter(
- Jackson2ObjectMapperBuilder builder) {
- XmlMapper objectMapper = builder.createXmlMapper(true).build();
- objectMapper.configure(ToXmlGenerator.Feature.WRITE_XML_DECLARATION, true);
- return new MappingJackson2XmlHttpMessageConverter(
- objectMapper);
- }
-}
\ No newline at end of file
diff --git a/application/src/main/java/org/ehrbase/application/config/ServerConfigImp.java b/application/src/main/java/org/ehrbase/application/config/ServerConfigImp.java
deleted file mode 100644
index 9b6e6e9200..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/ServerConfigImp.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (c) 2020 Vitasystems GmbH and Hannover Medical School.
- *
- * This file is part of project EHRbase
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.ehrbase.application.config;
-
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-
-import javax.validation.constraints.Max;
-import javax.validation.constraints.Min;
-
-@Configuration
-@ConfigurationProperties(prefix = "server")
-public class ServerConfigImp implements org.ehrbase.api.definitions.ServerConfig {
-
- @Min(1025)
- @Max(65536)
- private int port;
- private String nodename = "local.ehrbase.org";
- private AqlConfig aqlConfig;
- private boolean disableStrictValidation = false;
-
- public int getPort() {
- return port;
- }
-
- public void setPort(int port) {
- this.port = port;
- }
-
- public String getNodename() {
- return nodename;
- }
-
- public void setNodename(String nodename) {
- this.nodename = nodename;
- }
-
- @Override
- public String getAqlIterationSkipList() {
- return aqlConfig.getIgnoreIterativeNodeList();
- }
-
- @Override
- public Integer getAqlDepth() {
- return aqlConfig.getIterationScanDepth();
- }
-
- @Override
- public Boolean getUseJsQuery() {
- return aqlConfig.getUseJsQuery();
- }
-
- @Override
- public void setUseJsQuery(boolean b) {
- aqlConfig.setUseJsQuery(b);
- }
-
- public AqlConfig getAqlConfig() {
- return aqlConfig;
- }
-
- public void setAqlConfig(AqlConfig aqlConfig) {
- this.aqlConfig = aqlConfig;
- }
-
- public static class AqlConfig {
-
- private Boolean useJsQuery;
- private String ignoreIterativeNodeList;
- private Integer iterationScanDepth = 1;
-
- public Boolean getUseJsQuery() {
- return useJsQuery;
- }
-
- public String getIgnoreIterativeNodeList() {
- return ignoreIterativeNodeList;
- }
-
- public Integer getIterationScanDepth() {
- return iterationScanDepth;
- }
-
- public void setUseJsQuery(Boolean useJsQuery) {
- this.useJsQuery = useJsQuery;
- }
-
- public void setIgnoreIterativeNodeList(String ignoreIterativeNodeList) {
- this.ignoreIterativeNodeList = ignoreIterativeNodeList;
- }
-
- public void setIterationScanDepth(Integer iterationScanDepth) {
- this.iterationScanDepth = iterationScanDepth;
- }
- }
-
- @Override
- public boolean isDisableStrictValidation() {
- return disableStrictValidation;
- }
-
- public void setDisableStrictValidation(boolean disableStrictValidation) {
- this.disableStrictValidation = disableStrictValidation;
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/SwaggerConfiguration.java b/application/src/main/java/org/ehrbase/application/config/SwaggerConfiguration.java
deleted file mode 100644
index 628eec5bf2..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/SwaggerConfiguration.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package org.ehrbase.application.config;
-
-import io.swagger.v3.oas.models.ExternalDocumentation;
-import io.swagger.v3.oas.models.OpenAPI;
-import io.swagger.v3.oas.models.info.Info;
-import io.swagger.v3.oas.models.info.License;
-import org.springdoc.core.GroupedOpenApi;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class SwaggerConfiguration {
-
- @Bean
- public GroupedOpenApi openEhrApi() {
- return GroupedOpenApi.builder()
- .group("1. openEHR API")
- .pathsToMatch("/rest/openehr/**")
- .build();
- }
-
- @Bean
- public GroupedOpenApi ehrScapeApi() {
- return GroupedOpenApi.builder()
- .group("2. EhrScape API")
- .pathsToMatch("/rest/ecis/**")
- .build();
- }
-
- @Bean
- public GroupedOpenApi statusApi() {
- return GroupedOpenApi.builder()
- .group("3. EHRbase Status Endpoint")
- .pathsToMatch("/rest/status")
- .build();
- }
-
- @Bean
- public GroupedOpenApi adminApi() {
- return GroupedOpenApi.builder()
- .group("4. EHRbase Admin API")
- .pathsToMatch("/rest/admin/**")
- .build();
- }
-
- @Bean
- public GroupedOpenApi actuatorApi() {
- return GroupedOpenApi.builder()
- .group("5. Management API")
- .pathsToMatch("/management/**")
- .build();
- }
-
- @Bean
- public OpenAPI ehrBaseOpenAPI() {
- return new OpenAPI()
- .info(
- new Info()
- .title("EHRbase API")
- .description("EHRbase implements the [official openEHR REST API](https://specifications.openehr.org/releases/ITS-REST/latest/) and "
- + "a subset of the [EhrScape API](https://www.ehrscape.com/). "
- + "Additionally, EHRbase provides a custom `status` heartbeat endpoint, "
- + "an [Admin API](https://ehrbase.readthedocs.io/en/latest/03_development/07_admin/index.html) (if activated) "
- + "and a [Status and Metrics API](https://ehrbase.readthedocs.io/en/latest/03_development/08_status_and_metrics/index.html?highlight=status) (if activated) "
- + "for monitoring and maintenance. "
- + "Please select the definition in the top right."
- + " "
- + "Note: The openEHR REST API and the EhrScape API are documented in their official documentation, not here. Please refer to their separate documentation.")
- .version("v1")
- .license(
- new License()
- .name("Apache 2.0")
- .url("https://github.com/ehrbase/ehrbase/blob/develop/LICENSE.md")))
- .externalDocs(
- new ExternalDocumentation()
- .description("EHRbase Documentation")
- .url("https://ehrbase.readthedocs.io/"));
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/cache/CacheConfiguration.java b/application/src/main/java/org/ehrbase/application/config/cache/CacheConfiguration.java
deleted file mode 100644
index 542e54ddec..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/cache/CacheConfiguration.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2021 vitasystems GmbH and Hannover Medical School.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config.cache;
-
-import org.ehrbase.cache.CacheOptions;
-import org.ehrbase.service.KnowledgeCacheService;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.cache.annotation.EnableCaching;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-/**
- * {@link Configuration} for EhCache using JCache.
- *
- * @author Renaud Subiger
- * @since 1.0.0
- */
-@Configuration(proxyBeanMethods = false)
-@EnableConfigurationProperties(CacheProperties.class)
-@EnableCaching
-public class CacheConfiguration {
-
- @Bean
- public CacheOptions cacheOptions(CacheProperties properties) {
- var options = new CacheOptions();
- options.setPreBuildQueries(properties.isPreBuildQueries());
- options.setPreBuildQueriesDepth(properties.getPreBuildQueriesDepth());
- return options;
- }
-
- @Bean
- @ConditionalOnProperty(prefix = "cache", name = "init-on-startup", havingValue = "true")
- public CacheInitializer cacheInitializer(KnowledgeCacheService knowledgeCacheService) {
- return new CacheInitializer(knowledgeCacheService);
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/cache/CacheInitializer.java b/application/src/main/java/org/ehrbase/application/config/cache/CacheInitializer.java
deleted file mode 100644
index d8d5c1afdc..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/cache/CacheInitializer.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2021-2022 vitasystems GmbH and Hannover Medical School.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config.cache;
-
-import javax.annotation.PostConstruct;
-import org.ehrbase.service.KnowledgeCacheService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Initializes caches during application startup.
- *
- * @author Renaud Subiger
- * @since 1.0.0
- */
-public class CacheInitializer {
-
- private final Logger logger = LoggerFactory.getLogger(getClass());
-
- private final KnowledgeCacheService knowledgeCacheService;
-
- public CacheInitializer(KnowledgeCacheService knowledgeCacheService) {
- this.knowledgeCacheService = knowledgeCacheService;
- }
-
- @PostConstruct
- public void initialize() {
- logger.info("Initializing EHRbase caches");
- knowledgeCacheService.initializeCaches();
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/cache/CacheProperties.java b/application/src/main/java/org/ehrbase/application/config/cache/CacheProperties.java
deleted file mode 100644
index 8fbc23c782..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/cache/CacheProperties.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2021 vitasystems GmbH and Hannover Medical School.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config.cache;
-
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
-/**
- * {@link ConfigurationProperties} for EHRbase cache configuration.
- *
- * @author Renaud Subiger
- * @since 1.0.0
- */
-@ConfigurationProperties(prefix = "cache")
-public class CacheProperties {
-
- /**
- * Whether to initialize the caches during application startup.
- */
- private boolean initOnStartup = true;
-
- /**
- * Whether to pre-build queries when a new template is added.
- */
- private boolean preBuildQueries = true;
-
- /**
- * The default node depth for pre-built queries.
- */
- private Integer preBuildQueriesDepth = 4;
-
- public boolean isInitOnStartup() {
- return initOnStartup;
- }
-
- public void setInitOnStartup(boolean initOnStartup) {
- this.initOnStartup = initOnStartup;
- }
-
- public boolean isPreBuildQueries() {
- return preBuildQueries;
- }
-
- public void setPreBuildQueries(boolean preBuildQueries) {
- this.preBuildQueries = preBuildQueries;
- }
-
- public Integer getPreBuildQueriesDepth() {
- return preBuildQueriesDepth;
- }
-
- public void setPreBuildQueriesDepth(Integer preBuildQueriesDepth) {
- this.preBuildQueriesDepth = preBuildQueriesDepth;
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/client/HttpClientConfiguration.java b/application/src/main/java/org/ehrbase/application/config/client/HttpClientConfiguration.java
deleted file mode 100644
index d9de537376..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/client/HttpClientConfiguration.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (c) 2021 Vitasystems GmbH.
- *
- * This file is part of project EHRbase
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config.client;
-
-import org.apache.http.HttpHost;
-import org.apache.http.auth.AuthScope;
-import org.apache.http.auth.UsernamePasswordCredentials;
-import org.apache.http.client.HttpClient;
-import org.apache.http.conn.ssl.NoopHostnameVerifier;
-import org.apache.http.conn.ssl.TrustAllStrategy;
-import org.apache.http.impl.client.BasicCredentialsProvider;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.apache.http.impl.client.HttpClients;
-import org.apache.http.ssl.SSLContextBuilder;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.util.ResourceUtils;
-
-import javax.net.ssl.SSLContext;
-import java.io.IOException;
-import java.security.KeyManagementException;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableKeyException;
-import java.security.cert.CertificateException;
-
-/**
- * {@link Configuration} for Apache HTTP Client.
- */
-@Configuration
-@EnableConfigurationProperties(HttpClientProperties.class)
-@SuppressWarnings("java:S6212")
-public class HttpClientConfiguration {
-
- @Bean
- public HttpClient httpClient(HttpClientProperties properties) throws UnrecoverableKeyException, CertificateException,
- NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {
-
- HttpClientBuilder builder = HttpClients.custom();
-
- if (properties.getSsl().isEnabled()) {
- builder.setSSLContext(buildSSLContext(properties.getSsl()));
- builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
- }
-
- if (properties.getProxy().getHost() != null && properties.getProxy().getPort() != null) {
- builder.setProxy(new HttpHost(properties.getProxy().getHost(), properties.getProxy().getPort()));
-
- if (properties.getProxy().getUsername() != null && properties.getProxy().getPassword() != null) {
- UsernamePasswordCredentials credentials =
- new UsernamePasswordCredentials(properties.getProxy().getUsername(), properties.getProxy().getPassword());
- BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
- credentialsProvider.setCredentials(AuthScope.ANY, credentials);
- builder.setDefaultCredentialsProvider(credentialsProvider);
- }
- }
-
- return builder.build();
- }
-
- private SSLContext buildSSLContext(HttpClientProperties.Ssl properties) throws UnrecoverableKeyException, CertificateException,
- NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {
-
- SSLContextBuilder builder = SSLContextBuilder.create();
-
- if (properties.getKeyStoreType() != null) {
- builder.setKeyStoreType(properties.getKeyStoreType());
- }
- builder.loadKeyMaterial(ResourceUtils.getFile(properties.getKeyStore()),
- properties.getKeyStorePassword().toCharArray(),
- properties.getKeyPassword().toCharArray());
-
- if (properties.getTrustStoreType() != null) {
- builder.setKeyStoreType(properties.getTrustStoreType());
- }
- builder.loadTrustMaterial(ResourceUtils.getFile(properties.getTrustStore()),
- properties.getTrustStorePassword().toCharArray(), TrustAllStrategy.INSTANCE);
-
- return builder.build();
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/security/BasicAuthSecurityConfiguration.java b/application/src/main/java/org/ehrbase/application/config/security/BasicAuthSecurityConfiguration.java
deleted file mode 100644
index c1ec1b8c7e..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/security/BasicAuthSecurityConfiguration.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright 2021-2022 vitasystems GmbH and Hannover Medical School.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config.security;
-
-import static org.ehrbase.application.config.security.SecurityProperties.ADMIN;
-import static org.ehrbase.application.config.security.SecurityProperties.USER;
-
-import javax.annotation.PostConstruct;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
-import org.springframework.security.config.http.SessionCreationPolicy;
-
-/**
- * {@link Configuration} for Basic authentication.
- *
- * @author Jake Smolka
- * @author Renaud Subiger
- * @since 1.0.0
- */
-@Configuration
-@ConditionalOnProperty(prefix = "security", name = "authType", havingValue = "basic")
-@EnableWebSecurity
-public class BasicAuthSecurityConfiguration extends WebSecurityConfigurerAdapter {
-
- private final Logger logger = LoggerFactory.getLogger(getClass());
-
- private final SecurityProperties properties;
-
- public BasicAuthSecurityConfiguration(SecurityProperties securityProperties) {
- this.properties = securityProperties;
- }
-
- @PostConstruct
- public void initialize() {
- logger.info("Using basic authentication");
- }
-
- @Override
- public void configure(AuthenticationManagerBuilder auth) throws Exception {
- // @formatter:off
- auth
- .inMemoryAuthentication()
- .withUser(properties.getAuthUser())
- .password("{noop}" + properties.getAuthPassword())
- .roles(USER)
- .and()
- .withUser(properties.getAuthAdminUser())
- .password("{noop}" + properties.getAuthAdminPassword())
- .roles(ADMIN);
- // @formatter:on
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- // @formatter:off
- http
- .cors()
- .and()
- .csrf()
- .ignoringAntMatchers("/rest/**")
- .and()
- .authorizeRequests()
- .antMatchers("/rest/admin/**", "/management/**").hasRole(ADMIN)
- .anyRequest().hasAnyRole(ADMIN, USER)
- .and()
- .sessionManagement()
- .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and()
- .httpBasic();
- // @formatter:on
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/security/NoOpSecurityConfiguration.java b/application/src/main/java/org/ehrbase/application/config/security/NoOpSecurityConfiguration.java
deleted file mode 100644
index b0489f151e..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/security/NoOpSecurityConfiguration.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2021-2022 vitasystems GmbH and Hannover Medical School.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config.security;
-
-import javax.annotation.PostConstruct;
-import org.ehrbase.service.IAuthenticationFacade;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
-import org.springframework.security.authentication.AnonymousAuthenticationToken;
-import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
-
-/**
- * {@link Configuration} used when security is disabled.
- *
- * @author Renaud Subiger
- * @since 1.0.0
- */
-@Configuration(proxyBeanMethods = false)
-@ConditionalOnProperty(prefix = "security", name = "auth-type", havingValue = "none")
-public class NoOpSecurityConfiguration {
-
- private final Logger logger = LoggerFactory.getLogger(getClass());
-
- @PostConstruct
- public void initialize() {
- logger.warn("Security is disabled. Configure 'security.auth-type' to disable this warning.");
- }
-
- @Bean
- @Primary
- public IAuthenticationFacade anonymousAuthentication() {
- var filter = new AnonymousAuthenticationFilter("key");
- return () -> new AnonymousAuthenticationToken("key", filter.getPrincipal(),
- filter.getAuthorities());
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/security/OAuth2SecurityConfiguration.java b/application/src/main/java/org/ehrbase/application/config/security/OAuth2SecurityConfiguration.java
deleted file mode 100644
index ddfab5812a..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/security/OAuth2SecurityConfiguration.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 2021-2022 vitasystems GmbH and Hannover Medical School.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config.security;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-import javax.annotation.PostConstruct;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.core.convert.converter.Converter;
-import org.springframework.security.authentication.AbstractAuthenticationToken;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.security.oauth2.jwt.Jwt;
-import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
-
-/**
- * {@link Configuration} for OAuth2 authentication.
- *
- * @author Jake Smolka
- * @since 1.0.0
- */
-@Configuration
-@ConditionalOnProperty(prefix = "security", name = "auth-type", havingValue = "oauth")
-@EnableWebSecurity
-public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter {
-
- public static final String PROFILE_SCOPE = "PROFILE";
-
- private final Logger logger = LoggerFactory.getLogger(getClass());
-
- private final SecurityProperties securityProperties;
-
- private final OAuth2ResourceServerProperties oAuth2rProperties;
-
- public OAuth2SecurityConfiguration(SecurityProperties securityProperties,
- OAuth2ResourceServerProperties oAuth2rProperties) {
- this.securityProperties = securityProperties;
- this.oAuth2rProperties = oAuth2rProperties;
- }
-
- @PostConstruct
- public void initialize() {
- logger.info("Using OAuth2 authentication");
- logger.debug("Using issuer URI: {}", oAuth2rProperties.getJwt().getIssuerUri());
- logger.debug("Using user role: {}", securityProperties.getOauth2UserRole());
- logger.debug("Using admin role: {}", securityProperties.getOauth2AdminRole());
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- String userRole = securityProperties.getOauth2UserRole();
- String adminRole = securityProperties.getOauth2AdminRole();
-
- // @formatter:off
- http
- .cors()
- .and()
- .authorizeRequests()
- .antMatchers("/rest/admin/**", "/management/**").hasRole(adminRole)
- .anyRequest().hasAnyRole(adminRole, userRole, PROFILE_SCOPE)
- .and()
- .oauth2ResourceServer()
- .jwt()
- .jwtAuthenticationConverter(getJwtAuthenticationConverter());
- // @formatter:on
- }
-
- // Converter creates list of "ROLE_*" (upper case) authorities for each "realm access" role
- // and "roles" role from JWT
- @SuppressWarnings("unchecked")
- private Converter getJwtAuthenticationConverter() {
- var converter = new JwtAuthenticationConverter();
- converter.setJwtGrantedAuthoritiesConverter(jwt -> {
- Map realmAccess;
- realmAccess = (Map) jwt.getClaims().get("realm_access");
-
- Collection authority = new HashSet<>();
- if (realmAccess != null && realmAccess.containsKey("roles")) {
- authority.addAll(((List) realmAccess.get("roles")).stream()
- .map(roleName -> "ROLE_" + roleName.toUpperCase()).map(SimpleGrantedAuthority::new)
- .collect(Collectors.toList()));
- }
-
- if (jwt.getClaims().containsKey("scope")) {
- authority.addAll(Arrays.stream(jwt.getClaims().get("scope").toString().split(" "))
- .map(roleName -> "ROLE_" + roleName.toUpperCase()).map(SimpleGrantedAuthority::new)
- .collect(Collectors.toList()));
- }
- return authority;
- });
- return converter;
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/security/SecurityConfiguration.java b/application/src/main/java/org/ehrbase/application/config/security/SecurityConfiguration.java
deleted file mode 100644
index 79c34b0f76..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/security/SecurityConfiguration.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (c) 2020 Vitasystems GmbH and Hannover Medical School.
- *
- * This file is part of project EHRbase
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config.security;
-
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
-
-@Configuration(proxyBeanMethods = false)
-@EnableConfigurationProperties(SecurityProperties.class)
-@Import({NoOpSecurityConfiguration.class, BasicAuthSecurityConfiguration.class})
-public class SecurityConfiguration {
-
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/security/SecurityProperties.java b/application/src/main/java/org/ehrbase/application/config/security/SecurityProperties.java
deleted file mode 100644
index 8d485b3128..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/security/SecurityProperties.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (c) 2020 Vitasystems GmbH and Hannover Medical School.
- *
- * This file is part of project EHRbase
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.ehrbase.application.config.security;
-
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
-@ConfigurationProperties(prefix = "security")
-public class SecurityProperties {
-
- // Roles, when not using OAuth2
- public static final String ADMIN = "ADMIN";
-
- public static final String USER = "USER";
-
- /**
- * Authentication type.
- */
- private AuthTypes authType;
-
- /**
- * Username.
- */
- private String authUser;
-
- /**
- * Password for the user.
- */
- private String authPassword;
-
- /**
- * Admin username.
- */
- private String authAdminUser;
-
- /**
- * Password for the admin user.
- */
- private String authAdminPassword;
-
- /**
- * User role name used with OAuth2 authentication type.
- */
- private String oauth2UserRole;
-
- /**
- * Admin role name used with OAuth2 authentication type.
- */
- private String oauth2AdminRole;
-
- public AuthTypes getAuthType() {
- return authType;
- }
-
- public void setAuthType(AuthTypes authType) {
- this.authType = authType;
- }
-
- public String getAuthUser() {
- return authUser;
- }
-
- public void setAuthUser(String authUser) {
- this.authUser = authUser;
- }
-
- public String getAuthPassword() {
- return authPassword;
- }
-
- public void setAuthPassword(String authPassword) {
- this.authPassword = authPassword;
- }
-
- public String getAuthAdminUser() {
- return authAdminUser;
- }
-
- public void setAuthAdminUser(String authAdminUser) {
- this.authAdminUser = authAdminUser;
- }
-
- public String getAuthAdminPassword() {
- return authAdminPassword;
- }
-
- public void setAuthAdminPassword(String authAdminPassword) {
- this.authAdminPassword = authAdminPassword;
- }
-
- public String getOauth2UserRole() {
- return oauth2UserRole;
- }
-
- public void setOauth2UserRole(String oauth2UserRole) {
- this.oauth2UserRole = oauth2UserRole.toUpperCase();
- }
-
- public String getOauth2AdminRole() {
- return oauth2AdminRole;
- }
-
- public void setOauth2AdminRole(String oauth2AdminRole) {
- this.oauth2AdminRole = oauth2AdminRole.toUpperCase();
- }
-
- public enum AuthTypes {
- NONE, BASIC, OAUTH
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/validation/ValidationConfiguration.java b/application/src/main/java/org/ehrbase/application/config/validation/ValidationConfiguration.java
deleted file mode 100644
index f227f9c5a9..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/validation/ValidationConfiguration.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2021-2022 vitasystems GmbH and Hannover Medical School.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config.validation;
-
-import java.util.Map;
-import org.apache.http.client.HttpClient;
-import org.ehrbase.validation.terminology.ExternalTerminologyValidation;
-import org.ehrbase.validation.terminology.ExternalTerminologyValidationChain;
-import org.ehrbase.validation.terminology.FhirTerminologyValidation;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-/**
- * {@link Configuration} for external terminology validation.
- *
- * @author Renaud Subiger
- * @since 1.0.0
- */
-@Configuration
-@ConditionalOnProperty(name = "validation.external-terminology.enabled", havingValue = "true")
-@EnableConfigurationProperties(ValidationProperties.class)
-@SuppressWarnings("java:S6212")
-public class ValidationConfiguration {
-
- private final Logger logger = LoggerFactory.getLogger(getClass());
-
- private final ValidationProperties properties;
-
- private final HttpClient httpClient;
-
- public ValidationConfiguration(ValidationProperties properties, HttpClient httpClient) {
- this.properties = properties;
- this.httpClient = httpClient;
- }
-
- @Bean
- public ExternalTerminologyValidation externalTerminologyValidator() {
- Map providers = properties.getProvider();
-
- if (providers.isEmpty()) {
- throw new IllegalStateException(
- "At least one external terminology provider must be defined " +
- "if 'validation.external-validation.enabled' is set to 'true'");
- } else if (providers.size() == 1) {
- Map.Entry provider = providers.entrySet().iterator()
- .next();
- return buildExternalTerminologyValidation(provider);
- } else {
- ExternalTerminologyValidationChain chain = new ExternalTerminologyValidationChain();
- for (Map.Entry provider : providers.entrySet()) {
- chain.addExternalTerminologyValidationSupport(buildExternalTerminologyValidation(provider));
- }
- return chain;
- }
- }
-
- private ExternalTerminologyValidation buildExternalTerminologyValidation(
- Map.Entry provider) {
- logger.info("Initializing '{}' external terminology provider (type: {})", provider.getKey(),
- provider.getValue().getType());
- if (provider.getValue().getType() == ValidationProperties.ProviderType.FHIR) {
- return fhirTerminologyValidation(provider.getValue().getUrl());
- }
- throw new IllegalArgumentException("Invalid provider type: " + provider.getValue().getType());
- }
-
- private FhirTerminologyValidation fhirTerminologyValidation(String url) {
- return new FhirTerminologyValidation(url, properties.isFailOnError(), httpClient);
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/validation/ValidationProperties.java b/application/src/main/java/org/ehrbase/application/config/validation/ValidationProperties.java
deleted file mode 100644
index b703a5bab6..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/validation/ValidationProperties.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package org.ehrbase.application.config.validation;
-
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * {@link ConfigurationProperties} for external terminology validation.
- */
-@ConfigurationProperties(prefix = "validation.external-terminology")
-public class ValidationProperties {
-
- private boolean enabled = false;
-
- private boolean failOnError = false;
-
- private final Map provider = new HashMap<>();
-
- public boolean isEnabled() {
- return enabled;
- }
-
- public void setEnabled(boolean enabled) {
- this.enabled = enabled;
- }
-
- public boolean isFailOnError() {
- return failOnError;
- }
-
- public void setFailOnError(boolean failOnError) {
- this.failOnError = failOnError;
- }
-
- public Map getProvider() {
- return provider;
- }
-
- public enum ProviderType {
-
- FHIR
- }
-
- public static class Provider {
-
- private ProviderType type;
-
- private String url;
-
- public ProviderType getType() {
- return type;
- }
-
- public void setType(ProviderType type) {
- this.type = type;
- }
-
- public String getUrl() {
- return url;
- }
-
- public void setUrl(String url) {
- this.url = url;
- }
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/config/web/WebConfiguration.java b/application/src/main/java/org/ehrbase/application/config/web/WebConfiguration.java
deleted file mode 100644
index 26dad5c30f..0000000000
--- a/application/src/main/java/org/ehrbase/application/config/web/WebConfiguration.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (Hannover Medical School).
- *
- * This file is part of project EHRbase
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.config.web;
-
-import org.ehrbase.api.service.CompositionService;
-import org.ehrbase.api.service.EhrService;
-import org.ehrbase.application.util.IsoDateTimeConverter;
-import org.ehrbase.rest.openehr.audit.CompositionAuditInterceptor;
-import org.ehrbase.rest.openehr.audit.EhrAuditInterceptor;
-import org.ehrbase.rest.openehr.audit.QueryAuditInterceptor;
-import org.openehealth.ipf.commons.audit.AuditContext;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.format.FormatterRegistry;
-import org.springframework.lang.NonNull;
-import org.springframework.web.servlet.config.annotation.CorsRegistry;
-import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
-/**
- * {@link Configuration} from Spring Web MVC.
- */
-@Configuration(proxyBeanMethods = false)
-@EnableConfigurationProperties(CorsProperties.class)
-public class WebConfiguration implements WebMvcConfigurer {
-
- private final CorsProperties properties;
-
- private final AuditContext auditContext;
-
- private final EhrService ehrService;
-
- private final CompositionService compositionService;
-
- public WebConfiguration(CorsProperties properties, AuditContext auditContext,
- EhrService ehrService, CompositionService compositionService) {
- this.properties = properties;
- this.auditContext = auditContext;
- this.ehrService = ehrService;
- this.compositionService = compositionService;
- }
-
- @Override
- public void addFormatters(FormatterRegistry registry) {
- registry.addConverter(new IsoDateTimeConverter()); // Converter for version_at_time and other ISO date params
- }
-
- @Override
- public void addCorsMappings(CorsRegistry registry) {
- registry.addMapping("/**")
- .combine(properties.toCorsConfiguration());
- }
-
- @Override
- public void addInterceptors(@NonNull InterceptorRegistry registry) {
- if (auditContext.isAuditEnabled()) {
- // Composition endpoint
- registry
- .addInterceptor(new CompositionAuditInterceptor(auditContext, ehrService, compositionService))
- .addPathPatterns("/rest/openehr/v1/**/composition/**");
- // Ehr endpoint
- registry
- .addInterceptor(new EhrAuditInterceptor(auditContext, ehrService))
- .addPathPatterns("/rest/openehr/v1/ehr", "/rest/openehr/v1/ehr/*");
- // Query endpoint
- registry
- .addInterceptor(new QueryAuditInterceptor(auditContext, ehrService))
- .addPathPatterns("/rest/openehr/v1/query/**");
- }
- }
-}
diff --git a/application/src/main/java/org/ehrbase/application/server/EhrBaseServer.java b/application/src/main/java/org/ehrbase/application/server/EhrBaseServer.java
new file mode 100644
index 0000000000..e5559057a1
--- /dev/null
+++ b/application/src/main/java/org/ehrbase/application/server/EhrBaseServer.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2019-2024 vitasystems GmbH.
+ *
+ * This file is part of project EHRbase
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.ehrbase.application.server;
+
+import org.ehrbase.configuration.EhrBaseServerConfiguration;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration;
+import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.context.annotation.Import;
+
+@SpringBootApplication(
+ exclude = {
+ ManagementWebSecurityAutoConfiguration.class,
+ R2dbcAutoConfiguration.class,
+ SecurityAutoConfiguration.class
+ })
+@Import({EhrBaseServerConfiguration.class})
+@SuppressWarnings("java:S1118")
+public class EhrBaseServer {
+
+ public static SpringApplication build(String[] args) {
+ return new SpringApplicationBuilder(EhrBaseServer.class)
+ .web(WebApplicationType.SERVLET)
+ .build(args);
+ }
+}
diff --git a/application/src/main/java/org/ehrbase/application/web/LoggingContextFilter.java b/application/src/main/java/org/ehrbase/application/web/LoggingContextFilter.java
deleted file mode 100644
index 12805d58c9..0000000000
--- a/application/src/main/java/org/ehrbase/application/web/LoggingContextFilter.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2022 vitasystems GmbH and Hannover Medical School.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.web;
-
-import java.io.IOException;
-import java.util.UUID;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.slf4j.MDC;
-import org.springframework.core.Ordered;
-import org.springframework.core.annotation.Order;
-import org.springframework.lang.NonNull;
-import org.springframework.stereotype.Component;
-import org.springframework.web.filter.OncePerRequestFilter;
-
-/**
- * Filter implementation that associates a unique traceId
for logging purposes to each
- * incoming request.
- *
- * @author Renaud Subiger
- * @since 1.0.0
- */
-@Component
-@Order(Ordered.HIGHEST_PRECEDENCE)
-public class LoggingContextFilter extends OncePerRequestFilter {
-
- @Override
- protected void doFilterInternal(@NonNull HttpServletRequest request,
- @NonNull HttpServletResponse response,
- @NonNull FilterChain filterChain)
- throws ServletException, IOException {
-
- try {
- MDC.put("traceId", generateId());
- logger.trace("Set traceId for current request");
-
- filterChain.doFilter(request, response);
- } finally {
- MDC.remove("traceId");
- }
- }
-
- private String generateId() {
- return UUID.randomUUID().toString();
- }
-}
diff --git a/application/src/main/resources/application-cloud.yml b/application/src/main/resources/application-cloud.yml
deleted file mode 100644
index fcdba00efe..0000000000
--- a/application/src/main/resources/application-cloud.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-spring:
- datasource:
- url: jdbc:postgresql://localhost:5432/ehrbase
- username: ehrbase
- password: ehrbase
- hikari:
- maximum-pool-size: 50
- max-lifetime: 1800000
- minimum-idle: 10
-
-security:
- authType: NONE
-
-
-server:
- port: 8080
- # Optional custom server nodename
- # nodename: 'local.test.org'
-
- aqlConfig:
- # if true, WHERE clause is using jsquery, false uses SQL only
- useJsQuery: false
- # ignore unbounded item in path starting with one of
- ignoreIterativeNodeList: 'events,activities,content'
- # how many embedded jsonb_array_elements(..) are acceptable? Recommended == 1
- iterationScanDepth: 1
-
- servlet:
- context-path: /ehrbase
diff --git a/application/src/main/resources/application-docker.yml b/application/src/main/resources/application-docker.yml
deleted file mode 100644
index 10e0552948..0000000000
--- a/application/src/main/resources/application-docker.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright (c) 2019 Vitasystems GmbH and Hannover Medical School.
-#
-# This file is part of Project EHRbase
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-spring:
- datasource:
- url: ${DB_URL}
- username: ${DB_USER}
- password: ${DB_PASS}
- hikari:
- maximum-pool-size: 50
- max-lifetime: 1800000
- minimum-idle: 10
-
-security:
- authType: NONE
-
-server:
- port: 8080
- # Optional custom server nodename
- # nodename: 'local.test.org'
- servlet:
- context-path: /ehrbase
-
- aqlConfig:
- # if true, WHERE clause is using jsquery, false uses SQL only
- useJsQuery: false
- # ignore unbounded item in path starting with one of
- ignoreIterativeNodeList: 'events,activities,content'
- # how many embedded jsonb_array_elements(..) are acceptable? Recommended == 1
- iterationScanDepth: 1
diff --git a/application/src/main/resources/application-local.yml b/application/src/main/resources/application-local.yml
deleted file mode 100644
index 97da688f5a..0000000000
--- a/application/src/main/resources/application-local.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-# Copyright (c) 2019 Vitasystems GmbH and Hannover Medical School.
-#
-# This file is part of Project EHRbase
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-spring:
- datasource:
- url: jdbc:postgresql://localhost:5432/ehrbase
- username: ehrbase
- password: ehrbase
- tomcat:
- maxIdle: 10
- max-active: 50
- max-wait: 10000
-
-server:
- port: 8080
- # Optional custom server nodename
- # nodename: 'local.test.org'
- servlet:
- context-path: /ehrbase
-
- aqlConfig:
- # if true, WHERE clause is using jsquery, false uses SQL only
- useJsQuery: false
- # ignore unbounded item in path starting with one of
- ignoreIterativeNodeList: 'dummy'
- # how many embedded jsonb_array_elements(..) are acceptable? Recommended == 2
- iterationScanDepth: 20
-
-security:
- authType: NONE
-
-#use admin for cleaning up the db during tests
-admin-api:
- active: true
- allowDeleteAll: true
-
-terminology_server:
- tsUrl: 'https://r4.ontoserver.csiro.au/fhir/'
- codePath: '$["expansion"]["contains"][*]["code"]'
- systemPath: '$["expansion"]["contains"][*]["system"]'
- displayPath: '$["expansion"]["contains"][*]["display"]'
\ No newline at end of file
diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml
deleted file mode 100644
index ef979602dd..0000000000
--- a/application/src/main/resources/application.yml
+++ /dev/null
@@ -1,208 +0,0 @@
-# Copyright (c) 2019 Vitasystems GmbH and Jake Smolka (Hannover Medical School).
-#
-# This file is part of Project EHRbase
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# ------------------------------------------------------------------------------
-# General How-to:
-#
-# You can set all config values here or via an corresponding environment variable which is named as the property you
-# want to set. Replace camel case (aB) as all upper case (AB), dashes (-) and low dashes (_) just get ignored adn words
-# will be in one word. Each nesting step of properties will be separated by low dash in environment variable name.
-# E.g. if you want to allow the delete all endpoints in the admin api set an environment variable like this:
-# ADMINAPI_ALLOWDELETEALL=true
-#
-# See https://docs.spring.io/spring-boot/docs/2.5.0/reference/html/features.html#features.external-config.typesafe-configuration-properties.relaxed-binding.environment-variables
-# for official documentation on this feature.
-#
-# Also see the documentation on externalized configuration in general:
-# https://docs.spring.io/spring-boot/docs/2.5.0/reference/html/features.html#features.external-config
-
-spring:
- application:
- name: ehrbase
-
- cache:
- jcache:
- config: classpath:ehcache.xml
-
- security:
- oauth2:
- resourceserver:
- jwt:
- issuer-uri: # http://localhost:8081/auth/realms/ehrbase # Example issuer URI - or set via env var
- profiles:
- active: local
- datasource:
- driver-class-name: org.postgresql.Driver
-
- flyway:
- schemas: ehr
-
- jackson:
- default-property-inclusion: NON_NULL
-
-security:
- authType: BASIC
- authUser: ehrbase-user
- authPassword: SuperSecretPassword
- authAdminUser: ehrbase-admin
- authAdminPassword: EvenMoreSecretPassword
- oauth2UserRole: USER
- oauth2AdminRole: ADMIN
-
-# Attribute Based Access Control
-abac:
- enabled: false
- # Server URL incl. trailing "/"!
- server: http://localhost:3001/rest/v1/policy/execute/name/
- # Definition of the JWT claim which contains the organization ID.
- organizationClaim: 'organization_id'
- # Definition of the JWT claim which contains the patient ID. Falls back to the EHR's subject.
- patientClaim: 'patient_id'
- # Policies need to be named and configured for each resource. Available parameters are
- # - organization
- # - patient
- # - template
- policy:
- ehr:
- name: 'has_consent_patient'
- parameters: 'organization, patient'
- ehrstatus:
- name: 'has_consent_patient'
- parameters: 'organization, patient'
- composition:
- name: 'has_consent_template'
- parameters: 'organization, patient, template'
- #parameters: 'template' # for manual testing, doesn't depend on real claims in JWT
- contribution:
- name: 'has_consent_template'
- parameters: 'organization, patient, template'
- query:
- name: 'has_consent_template'
- parameters: 'organization, patient, template'
-
-httpclient:
-#proxy: 'localhost'
-#proxyPort: 1234
-
-cache:
- init-on-startup: true
- pre-build-queries: true
- pre-build-queries-depth: 4
-
-system:
- allow-template-overwrite: false
-
-openehr-api:
- context-path: /rest/openehr
-admin-api:
- active: false
- allowDeleteAll: false
- context-path: /rest/admin
-
-# Logging Properties
-logging:
- level:
- org.ehcache: info
- org.jooq: info
- org.springframework: info
- pattern:
- console: '%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr([%X]){faint} %clr(${PID}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx'
-
-server:
- # Optional custom server nodename
- # nodename: 'local.test.org'
-
- aqlConfig:
- # if true, WHERE clause is using jsquery, false uses SQL only
- useJsQuery: false
- # ignore unbounded item in path starting with one of
- ignoreIterativeNodeList: 'activities,content'
- # how many embedded jsonb_array_elements(..) are acceptable? Recommended == 2
- iterationScanDepth: 2
-
- # Option to disable strict invariant validation.
- # disable-strict-validation: true
-
-
-terminology-server:
- tsUrl: 'https://r4.ontoserver.csiro.au/fhir/'
- codePath: '$["expansion"]["contains"][*]["code"]'
- systemPath: '$["expansion"]["contains"][*]["system"]'
- displayPath: '$["expansion"]["contains"][*]["display"]'
-
-# Configuration of actuator for reporting and health endpoints
-management:
- endpoints:
- # Disable all endpoint by default to opt-in enabled endpoints
- enabled-by-default: false
- web:
- base-path: '/management'
- exposure:
- include: 'env, health, info, metrics, prometheus'
- # Per endpoint settings
- endpoint:
- # Env endpoint - Shows information on environment of EHRbase
- env:
- # Enable / disable env endpoint
- enabled: false
- # Health endpoint - Shows information on system status
- health:
- # Enable / disable health endpoint
- enabled: false
- # Show components in health endpoint. Can be "never", "when-authorized" or "always"
- show-components: 'when-authorized'
- # Show details in health endpoint. Can be "never", "when-authorized" or "always"
- show-details: 'when-authorized'
- # Show additional information on used systems. See https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-health-indicators for available keys
- datasource:
- # Enable / disable report if datasource connection could be established
- enabled: true
- # Info endpoint - Shows information on the application as build infor, etc.
- info:
- # Enable / disable info endpoint
- enabled: false
- # Metrics endpoint - Shows several metrics on running EHRbase
- metrics:
- # Enable / disable metrics endpoint
- enabled: false
- # Prometheus metric endpoint - Special metrics format to display in microservice observer solutions
- prometheus:
- # Enable / disable prometheus endpoint
- enabled: false
- # Metrics settings
- metrics:
- export:
- prometheus:
- enabled: true
-
-# Audit Properties
-ipf:
- atna:
- audit-enabled: false
-
-# External Terminology Validation Properties
-validation:
- external-terminology:
- enabled: false
-
-# SSL Properties (used by Spring WebClient and Apache HTTP Client)
-client:
- ssl:
- enabled: false
-
-# JavaMelody
-javamelody:
- enabled: false
\ No newline at end of file
diff --git a/application/src/main/resources/banner-cli.txt b/application/src/main/resources/banner-cli.txt
new file mode 100644
index 0000000000..4b5d029f60
--- /dev/null
+++ b/application/src/main/resources/banner-cli.txt
@@ -0,0 +1 @@
+${AnsiColor.BLUE}EHRbase CLI (Spring Boot ${spring-boot.version} EHRbase @project.version@ https://ehrbase.org/)${AnsiBackground.DEFAULT}${AnsiColor.DEFAULT}
\ No newline at end of file
diff --git a/application/src/main/resources/banner.txt b/application/src/main/resources/banner.txt
index 842966cbae..aaea7d2c91 100644
--- a/application/src/main/resources/banner.txt
+++ b/application/src/main/resources/banner.txt
@@ -6,5 +6,5 @@ ${AnsiColor.BLUE}
| |____| | | | | \ \| |_) | (_| \__ \ __/
|______|_| |_|_| \_\_.__/ \__,_|___/\___|
Spring Boot ${spring-boot.version}
-EHRbase ${application.formatted-version}
+EHRbase @project.version@
https://ehrbase.org/ ${AnsiBackground.DEFAULT}${AnsiColor.DEFAULT}
\ No newline at end of file
diff --git a/application/src/main/resources/ehcache.xml b/application/src/main/resources/ehcache.xml
deleted file mode 100644
index 5bbc8b860c..0000000000
--- a/application/src/main/resources/ehcache.xml
+++ /dev/null
@@ -1,84 +0,0 @@
-
-
-
-
-
-
-
-
-
- java.util.UUID
- org.ehrbase.webtemplate.model.WebTemplate
-
-
-
- org.ehrbase.aql.containment.TemplateIdQueryTuple
- org.ehrbase.aql.containment.JsonPathQueryResult
-
-
-
- org.ehrbase.aql.containment.TemplateIdAqlTuple
- org.ehrbase.aql.sql.queryimpl.ItemInfo
-
-
-
- java.lang.String
- java.util.List
-
-
-
-
-
-
-
- 300
- 400
-
-
-
-
-
-
-
-
- 200
- 400
-
-
-
\ No newline at end of file
diff --git a/application/src/main/resources/static/img/ehrbase.png b/application/src/main/resources/static/img/ehrbase.png
new file mode 100644
index 0000000000..684e11c78d
Binary files /dev/null and b/application/src/main/resources/static/img/ehrbase.png differ
diff --git a/application/src/main/resources/static/img/favicons/apple-icon-120x120.png b/application/src/main/resources/static/img/favicons/apple-icon-120x120.png
new file mode 100644
index 0000000000..d84a25a295
Binary files /dev/null and b/application/src/main/resources/static/img/favicons/apple-icon-120x120.png differ
diff --git a/application/src/main/resources/static/img/favicons/apple-icon-152x152.png b/application/src/main/resources/static/img/favicons/apple-icon-152x152.png
new file mode 100644
index 0000000000..7232b8bf53
Binary files /dev/null and b/application/src/main/resources/static/img/favicons/apple-icon-152x152.png differ
diff --git a/application/src/main/resources/static/img/favicons/apple-icon-60x60.png b/application/src/main/resources/static/img/favicons/apple-icon-60x60.png
new file mode 100644
index 0000000000..416ad41cc6
Binary files /dev/null and b/application/src/main/resources/static/img/favicons/apple-icon-60x60.png differ
diff --git a/application/src/main/resources/static/img/favicons/apple-icon-76x76.png b/application/src/main/resources/static/img/favicons/apple-icon-76x76.png
new file mode 100644
index 0000000000..e8ba74a888
Binary files /dev/null and b/application/src/main/resources/static/img/favicons/apple-icon-76x76.png differ
diff --git a/application/src/main/resources/static/img/favicons/favicon-16x16.png b/application/src/main/resources/static/img/favicons/favicon-16x16.png
new file mode 100644
index 0000000000..6c5d991539
Binary files /dev/null and b/application/src/main/resources/static/img/favicons/favicon-16x16.png differ
diff --git a/application/src/main/resources/static/img/favicons/favicon-32x32.png b/application/src/main/resources/static/img/favicons/favicon-32x32.png
new file mode 100644
index 0000000000..0a49b0e9a7
Binary files /dev/null and b/application/src/main/resources/static/img/favicons/favicon-32x32.png differ
diff --git a/application/src/main/resources/static/index.html b/application/src/main/resources/static/index.html
new file mode 100644
index 0000000000..2c11459ef9
--- /dev/null
+++ b/application/src/main/resources/static/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ EHRbase Open Source
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/test/java/org/ehrbase/application/abac/AbacIntegrationTest.java b/application/src/test/java/org/ehrbase/application/abac/AbacIntegrationTest.java
deleted file mode 100644
index 511d5be6b3..0000000000
--- a/application/src/test/java/org/ehrbase/application/abac/AbacIntegrationTest.java
+++ /dev/null
@@ -1,437 +0,0 @@
-/*
- * Copyright (c) 2021 Vitasystems GmbH and Jake Smolka (Hannover Medical School).
- *
- * This file is part of project EHRbase
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.ehrbase.application.abac;
-
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.anyMap;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-import com.jayway.jsonpath.JsonPath;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-import org.apache.commons.io.IOUtils;
-import org.ehrbase.test_data.composition.CompositionTestDataCanonicalJson;
-import org.ehrbase.test_data.operationaltemplate.OperationalTemplateTestData;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
-import org.springframework.test.context.ActiveProfiles;
-import org.springframework.test.context.junit4.SpringRunner;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.MvcResult;
-import org.springframework.util.ResourceUtils;
-
-@RunWith(SpringRunner.class)
-@SpringBootTest
-@ActiveProfiles({"local", "test"})
-@EnabledIfEnvironmentVariable(named = "EHRBASE_ABAC_IT_TEST", matches = "true")
-@AutoConfigureMockMvc
-class AbacIntegrationTest {
-
- private static final String ORGA_ID = "f47bfc11-ec8d-412e-aebf-c6953cc23e7d";
-
- @MockBean
- private AbacConfig.AbacCheck abacCheck;
-
- @Autowired
- private MockMvc mockMvc;
-
- @Autowired
- private AbacConfig abacConfig;
-
- @Test
- /*
- * This test requires a new and clean DB state to run successfully.
- */
- void testAbacIntegrationTest() throws Exception {
- /*
- ----------------- TEST CONTEXT SETUP -----------------
- */
- // Configure the mock bean of the ABAC server, so we can test with this external service.
- given(this.abacCheck.execute(anyString(), anyMap())).willReturn(true);
-
- Map attributes = new HashMap<>();
- attributes.put("sub", "my-id");
- attributes.put("email", "test@test.org");
-
- // Counters to keep track of number of requests to mock ABAC server bean
- int hasConsentPatientCount = 0;
- int hasConsentTemplateCount = 0;
-
- String externalSubjectRef = UUID.randomUUID().toString();
-
- String ehrStatus =
- String.format(IOUtils.toString(ResourceUtils.getURL("classpath:ehr_status.json"),
- StandardCharsets.UTF_8),
- externalSubjectRef);
-
- MvcResult result = mockMvc.perform(post("/rest/openehr/v1/ehr")
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_JSON_VALUE)
- .contentType(MediaType.APPLICATION_JSON)
- .content(ehrStatus)
- )
- .andExpectAll(status().isCreated(),
- jsonPath("$.ehr_id.value").exists())
- .andReturn();
-
- String ehrId = JsonPath.read(result.getResponse().getContentAsString(), "$.ehr_id.value");
- Assertions.assertNotNull(ehrId);
- assertNotEquals("", ehrId);
-
- InputStream stream = OperationalTemplateTestData.CORONA_ANAMNESE.getStream();
- Assertions.assertNotNull(stream);
- String streamString = IOUtils.toString(stream, StandardCharsets.UTF_8);
-
- mockMvc.perform(post("/rest/openehr/v1/definition/template/adl1.4/")
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .content(streamString)
- .contentType(MediaType.APPLICATION_XML)
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_XML)
- )
- .andExpect(r -> assertTrue(
- // created 201 or conflict 409 are okay
- r.getResponse().getStatus() == HttpStatus.CREATED.value() ||
- r.getResponse().getStatus() == HttpStatus.CONFLICT.value()));
-
- stream = CompositionTestDataCanonicalJson.CORONA.getStream();
- Assertions.assertNotNull(stream);
- streamString = IOUtils.toString(stream, StandardCharsets.UTF_8);
-
- /*
- ----------------- TEST CASES -----------------
- */
-
- /*
- GET EHR
- */
- mockMvc.perform(get(String.format("/rest/openehr/v1/ehr/%s", ehrId))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isOk());
-
- verify(abacCheck, times(++hasConsentPatientCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_patient", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- }});
- /*
- GET EHR_STATUS
- */
- result = mockMvc.perform(get(String.format("/rest/openehr/v1/ehr/%s/ehr_status", ehrId))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isOk())
- .andReturn();
-
- verify(abacCheck, times(++hasConsentPatientCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_patient", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- }});
-
- String ehrStatusVersionUid = JsonPath.read(result.getResponse().getContentAsString(),
- "$.uid.value");
- Assertions.assertNotNull(ehrStatusVersionUid);
- assertNotEquals("", ehrStatusVersionUid);
-
- /*
- PUT EHR_STATUS
- */
- mockMvc.perform(put(String.format("/rest/openehr/v1/ehr/%s/ehr_status", ehrId))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .header("If-Match", ehrStatusVersionUid)
- .header("PREFER", "return=representation")
- .content(ehrStatus)
- .contentType(MediaType.APPLICATION_JSON)
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isOk());
-
- verify(abacCheck, times(++hasConsentPatientCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_patient", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- }});
- /*
- GET VERSIONED_EHR_STATUS
- */
- mockMvc.perform(
- get(String.format("/rest/openehr/v1/ehr/%s/versioned_ehr_status/version", ehrId))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isOk());
-
- verify(abacCheck, times(++hasConsentPatientCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_patient", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- }});
- /*
- POST COMPOSITION
- */
- result = mockMvc.perform(post(String.format("/rest/openehr/v1/ehr/%s/composition", ehrId))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .content(streamString)
- .contentType(MediaType.APPLICATION_JSON)
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isCreated())
- .andReturn();
-
- String compositionVersionUid = JsonPath.read(result.getResponse().getContentAsString(),
- "$.uid.value");
- Assertions.assertNotNull(compositionVersionUid);
- assertNotEquals("", compositionVersionUid);
- assertTrue(compositionVersionUid.contains("::"));
-
- verify(abacCheck, times(++hasConsentTemplateCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- put("template", "Corona_Anamnese");
- }});
- /*
- GET VERSIONED_COMPOSITION
- */
- mockMvc.perform(get(String.format("/rest/openehr/v1/ehr/%s/versioned_composition/%s/version/%s",
- ehrId, compositionVersionUid.split("::")[0], compositionVersionUid))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isOk());
-
- verify(abacCheck, times(++hasConsentTemplateCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- put("template", "Corona_Anamnese");
- }});
-
- /*
- GET COMPOSITION (here of deleted composition)
- */
- mockMvc.perform(
- get(String.format("/rest/openehr/v1/ehr/%s/composition/%s", ehrId, compositionVersionUid))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isOk());
-
- // Failing: Does not call ABAC Server. Deleted composition does not need to call ABAC server?
- verify(abacCheck, times(++hasConsentTemplateCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- put("template", "Corona_Anamnese");
- }});
-
- /*
- DELETE COMPOSITION
- */
- mockMvc.perform(delete(
- String.format("/rest/openehr/v1/ehr/%s/composition/%s", ehrId, compositionVersionUid))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isNoContent());
-
- verify(abacCheck, times(++hasConsentTemplateCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- put("template", "Corona_Anamnese");
- }});
-
- String contribution =
- String.format(IOUtils.toString(ResourceUtils.getURL("classpath:contribution.json"),
- StandardCharsets.UTF_8),
- streamString);
- /*
- POST CONTRIBUTION
- */
- mockMvc.perform(post(String.format("/rest/openehr/v1/ehr/%s/contribution", ehrId))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .content(contribution)
- .contentType(MediaType.APPLICATION_JSON)
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isCreated())
- .andReturn();
-
- verify(abacCheck, times(++hasConsentTemplateCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- put("template", "Corona_Anamnese");
- }});
-
- /*
- POST QUERY
- */
- mockMvc.perform(post("/rest/openehr/v1/query/aql")
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .content("{\n"
- + " \"q\": \"select e/ehr_id/value, c/uid/value, c/archetype_details/template_id/value, c/feeder_audit from EHR e CONTAINS composition c\"\n"
- + "}")
- .contentType(MediaType.APPLICATION_JSON)
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isOk());
-
- verify(abacCheck, times(++hasConsentTemplateCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- put("template", "Corona_Anamnese");
- }});
-
- /*
- GET QUERY
- */
- String pathQuery = "select e/ehr_id/value, c/uid/value, c/archetype_details/template_id/value, c/feeder_audit from EHR e CONTAINS composition c";
-
- mockMvc.perform(get(String.format("/rest/openehr/v1/query/aql?q=%s", pathQuery))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- )
- .andExpect(status().isOk());
-
- verify(abacCheck, times(++hasConsentTemplateCount)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- put("template", "Corona_Anamnese");
- }});
-
- /*
- GET QUERY WITH MULTIPLE EHRs AND TEMPLATES (incl. posting those)
- */
- // post another template
- stream = OperationalTemplateTestData.MINIMAL_EVALUATION.getStream();
- Assertions.assertNotNull(stream);
- streamString = IOUtils.toString(stream, StandardCharsets.UTF_8);
-
- mockMvc.perform(post("/rest/openehr/v1/definition/template/adl1.4/")
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .content(streamString)
- .contentType(MediaType.APPLICATION_XML)
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_XML)
- )
- .andExpect(r -> assertTrue(
- // created 201 or conflict 409 are okay
- r.getResponse().getStatus() == HttpStatus.CREATED.value() ||
- r.getResponse().getStatus() == HttpStatus.CONFLICT.value()));
-
- streamString = IOUtils.toString(ResourceUtils.getURL("classpath:composition.json"),
- StandardCharsets.UTF_8);
-
- mockMvc.perform(post(String.format("/rest/openehr/v1/ehr/%s/composition", ehrId))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- .content(streamString)
- .contentType(MediaType.APPLICATION_JSON)
- .header("PREFER", "return=representation")
- .accept(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(status().isCreated())
- .andReturn();
-
- verify(abacCheck).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template",
- new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- put("template", "minimal_evaluation.en.v1");
- }});
-
- mockMvc.perform(get(String.format("/rest/openehr/v1/query/aql?q=%s", pathQuery))
- .with(jwt().authorities(new OAuth2UserAuthority("ROLE_USER", attributes)).
- jwt(token -> token.claim(abacConfig.getPatientClaim(), externalSubjectRef)
- .claim(abacConfig.getOrganizationClaim(), ORGA_ID)))
- )
- .andExpect(status().isOk());
-
-// verify(abacCheck, times(++hasConsentTemplateCount)).execute(
-// "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template", new HashMap<>() {{
-// put("patient", externalSubjectRef);
-// put("organization", ORGA_ID);
-// put("template", "Corona_Anamnese");
-// }});
-
- verify(abacCheck, times(3)).execute(
- "http://localhost:3001/rest/v1/policy/execute/name/has_consent_template",
- new HashMap<>() {{
- put("patient", externalSubjectRef);
- put("organization", ORGA_ID);
- put("template", "minimal_evaluation.en.v1");
- }});
-
- }
-}
\ No newline at end of file
diff --git a/application/src/test/java/org/ehrbase/application/abac/FlywayTestConfiguration.java b/application/src/test/java/org/ehrbase/application/abac/FlywayTestConfiguration.java
deleted file mode 100644
index 861be79ba7..0000000000
--- a/application/src/test/java/org/ehrbase/application/abac/FlywayTestConfiguration.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.ehrbase.application.abac;
-
-import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class FlywayTestConfiguration {
-
- @Bean
- public FlywayMigrationStrategy clean() {
- return flyway -> {
- flyway.clean();
- flyway.migrate();
- };
- }
-}
diff --git a/application/src/test/java/org/ehrbase/application/cors/CorsBasicAuthIT.java b/application/src/test/java/org/ehrbase/application/cors/CorsBasicAuthIT.java
deleted file mode 100644
index 98d13ada4d..0000000000
--- a/application/src/test/java/org/ehrbase/application/cors/CorsBasicAuthIT.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.ehrbase.application.cors;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.web.servlet.MockMvc;
-
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
-import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-@SpringBootTest(properties = {
- "security.authType=basic",
- "spring.cache.type=simple"
-})
-@AutoConfigureMockMvc
-class CorsBasicAuthIT {
-
- @Autowired
- private MockMvc mockMvc;
-
- @Test
- void testCors() throws Exception {
- mockMvc.perform(options("/rest/openehr/v1/definition/template/adl1.4")
- .header("Access-Control-Request-Method", "GET")
- .header("Origin", "https://client.ehrbase.org"))
- .andDo(print())
- .andExpect(status().isOk());
- }
-}
diff --git a/application/src/test/java/org/ehrbase/application/cors/CorsNoAuthIT.java b/application/src/test/java/org/ehrbase/application/cors/CorsNoAuthIT.java
deleted file mode 100644
index 422c1664f2..0000000000
--- a/application/src/test/java/org/ehrbase/application/cors/CorsNoAuthIT.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.ehrbase.application.cors;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.web.servlet.MockMvc;
-
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
-import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-@SpringBootTest(properties = {
- "spring.cache.type=simple"
-})
-@AutoConfigureMockMvc
-class CorsNoAuthIT {
-
- @Autowired
- private MockMvc mockMvc;
-
- @Test
- void testCors() throws Exception {
- mockMvc.perform(options("/rest/openehr/v1/definition/template/adl1.4")
- .header("Access-Control-Request-Method", "GET")
- .header("Origin", "https://client.ehrbase.org"))
- .andDo(print())
- .andExpect(status().isOk());
- }
-}
diff --git a/aql-engine/pom.xml b/aql-engine/pom.xml
new file mode 100644
index 0000000000..96aa580ae3
--- /dev/null
+++ b/aql-engine/pom.xml
@@ -0,0 +1,103 @@
+
+
+ 4.0.0
+
+ org.ehrbase.openehr
+ server
+ 2.13.0-SNAPSHOT
+
+
+ aql-engine
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+
+ org.ehrbase.openehr
+ rm-db-format
+
+
+ org.ehrbase.openehr
+ api
+
+
+ org.ehrbase.openehr
+ jooq-pg
+
+
+ org.ehrbase.openehr.sdk
+ validation
+
+
+
+
+ org.springframework
+ spring-context
+
+
+ org.springframework
+ spring-core
+
+
+ org.springframework
+ spring-tx
+
+
+ org.springframework
+ spring-web
+
+
+
+
+ org.springframework.boot
+ spring-boot
+
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+ client
+ org.ehrbase.openehr.sdk
+ test
+
+
+ org.ehrbase.openehr.sdk
+ test-data
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ net.java
+ quickcheck
+ 0.6
+ test
+
+
+
diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlConfigurationProperties.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlConfigurationProperties.java
new file mode 100644
index 0000000000..9a897a9a12
--- /dev/null
+++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlConfigurationProperties.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2019-2024 vitasystems GmbH.
+ *
+ * This file is part of project EHRbase
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.ehrbase.openehr.aqlengine;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * AQL features that can be optionally enabled.
+ *
+ *
+ * pg-llj-workaround
Enables fix for an old postgresql bug where filters in lateral left joins inside a left join are not respected, default: true
+ * experimental.aql-on-folder.enabled
if enabled allow to query EHR
FOLDER
using AQL, default: false
+ *
+ */
+@ConfigurationProperties(prefix = "ehrbase.aql")
+public record AqlConfigurationProperties(boolean pgLljWorkaround, Experimental experimental) {
+ public record Experimental(AqlOnFolder aqlOnFolder) {
+
+ public record AqlOnFolder(boolean enabled) {}
+ }
+}
diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlEngineModuleConfiguration.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlEngineModuleConfiguration.java
new file mode 100644
index 0000000000..c56cd87e16
--- /dev/null
+++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlEngineModuleConfiguration.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2024 vitasystems GmbH.
+ *
+ * This file is part of project EHRbase
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.ehrbase.openehr.aqlengine;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+@Configuration
+@ComponentScan
+@EnableAspectJAutoProxy
+@EnableConfigurationProperties(AqlConfigurationProperties.class)
+public class AqlEngineModuleConfiguration {}
diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacement.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacement.java
new file mode 100644
index 0000000000..be027faa35
--- /dev/null
+++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlParameterReplacement.java
@@ -0,0 +1,566 @@
+/*
+ * Copyright (c) 2024 vitasystems GmbH.
+ *
+ * This file is part of project EHRbase
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.ehrbase.openehr.aqlengine;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.apache.commons.collections4.MapUtils;
+import org.ehrbase.openehr.aqlengine.asl.model.AslRmTypeAndConcept;
+import org.ehrbase.openehr.sdk.aql.dto.AqlQuery;
+import org.ehrbase.openehr.sdk.aql.dto.condition.ComparisonOperatorCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.ExistsCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.LikeCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.LogicalOperatorCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.MatchesCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.NotCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.WhereCondition;
+import org.ehrbase.openehr.sdk.aql.dto.containment.AbstractContainmentExpression;
+import org.ehrbase.openehr.sdk.aql.dto.containment.Containment;
+import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentClassExpression;
+import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentNotOperator;
+import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentSetOperator;
+import org.ehrbase.openehr.sdk.aql.dto.containment.ContainmentVersionExpression;
+import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction;
+import org.ehrbase.openehr.sdk.aql.dto.operand.BooleanPrimitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.ComparisonLeftOperand;
+import org.ehrbase.openehr.sdk.aql.dto.operand.DoublePrimitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath;
+import org.ehrbase.openehr.sdk.aql.dto.operand.LikeOperand;
+import org.ehrbase.openehr.sdk.aql.dto.operand.LongPrimitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.MatchesOperand;
+import org.ehrbase.openehr.sdk.aql.dto.operand.Operand;
+import org.ehrbase.openehr.sdk.aql.dto.operand.PathPredicateOperand;
+import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.QueryParameter;
+import org.ehrbase.openehr.sdk.aql.dto.operand.SingleRowFunction;
+import org.ehrbase.openehr.sdk.aql.dto.operand.StringPrimitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.TemporalPrimitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.TerminologyFunction;
+import org.ehrbase.openehr.sdk.aql.dto.orderby.OrderByExpression;
+import org.ehrbase.openehr.sdk.aql.dto.path.AndOperatorPredicate;
+import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPath;
+import org.ehrbase.openehr.sdk.aql.dto.path.AqlObjectPathUtil;
+import org.ehrbase.openehr.sdk.aql.dto.path.ComparisonOperatorPredicate;
+import org.ehrbase.openehr.sdk.aql.dto.select.SelectClause;
+import org.ehrbase.openehr.sdk.aql.dto.select.SelectExpression;
+import org.ehrbase.openehr.sdk.aql.parser.AqlParseException;
+
+/**
+ * Replaces parameters in an AQL query
+ */
+public final class AqlParameterReplacement {
+
+ private AqlParameterReplacement() {
+ // NOOP
+ }
+
+ /**
+ * Replaces all parameters in the aqlQuery
with values from the parameterMap
.
+ * The replacement is performed in-place, modifying the source object.
+ * Missing parameter values are set to NULL.
+ *
+ * @param aqlQuery the query to me modified
+ * @param parameterMap a map of parameter values
+ */
+ public static void replaceParameters(AqlQuery aqlQuery, Map parameterMap) {
+ if (MapUtils.isNotEmpty(parameterMap)) {
+ // SELECT
+ SelectParams.replaceParameters(aqlQuery.getSelect(), parameterMap);
+ // FROM
+ ContainmentParams.replaceParameters(aqlQuery.getFrom(), parameterMap);
+ // WHERE
+ WhereParams.replaceParameters(aqlQuery.getWhere(), parameterMap);
+ // ORDER BY
+ OrderByParams.replaceParameters(parameterMap, aqlQuery.getOrderBy());
+ }
+ }
+
+ public static void replaceIdentifiedPathParameters(
+ IdentifiedPath identifiedPath, Map parameterMap) {
+ // revise root predicates in-place
+ Optional.of(identifiedPath).map(IdentifiedPath::getRootPredicate).stream()
+ .flatMap(List::stream)
+ .map(AndOperatorPredicate::getOperands)
+ .flatMap(List::stream)
+ .forEach(co -> ObjectPathParams.replaceComparisonOperatorParameters(co, parameterMap));
+ ObjectPathParams.replaceParameters(identifiedPath.getPath(), parameterMap)
+ .ifPresent(identifiedPath::setPath);
+ }
+
+ /**
+ * @param operand
+ * @param parameterMap
+ * @return a Stream of new primitive, if the operand needs to be replaced
+ */
+ private static Stream replaceOperandParameters(Operand operand, Map parameterMap) {
+
+ return switch (operand) {
+ case QueryParameter qp -> resolveParameters(qp, parameterMap);
+ case IdentifiedPath path -> {
+ replaceIdentifiedPathParameters(path, parameterMap);
+ yield null;
+ }
+ case SingleRowFunction func -> {
+ replaceFunctionParameters(func, parameterMap);
+ yield null;
+ }
+ case TerminologyFunction __ -> null;
+ case Primitive, ?> __ -> null;
+ };
+ }
+
+ private static void replaceFunctionParameters(SingleRowFunction func, Map parameterMap) {
+ Utils.reviseList(func.getOperandList(), o -> replaceOperandParameters(o, parameterMap));
+ }
+
+ private static Stream resolveParameters(QueryParameter param, Map parameterMap) {
+ String paramName = param.getName();
+ Object paramValue = parameterMap.get(paramName);
+
+ return switch (paramValue) {
+ case Collection> c -> c.stream().map(e -> toPrimitive(param.getName(), e));
+ case null -> throw new AqlParseException("Missing parameter '%s'".formatted(paramName));
+ default -> Stream.of(toPrimitive(param.getName(), paramValue));
+ };
+ }
+
+ private static Primitive toPrimitive(String name, Object paramValue) {
+ return switch (paramValue) {
+ case null -> throw new AqlParseException("Missing parameter '%s'".formatted(name));
+ case Integer i -> new LongPrimitive(i.longValue());
+ case Long i -> new LongPrimitive(i);
+ case Number nr -> new DoublePrimitive(nr.doubleValue());
+ case String str -> Utils.stringToPrimitive(str);
+ case Boolean b -> new BooleanPrimitive(b);
+ default -> {
+ throw new IllegalArgumentException("Type of parameter '%s' is not supported".formatted(name));
+ }
+ };
+ }
+
+ private record ModifiedElement(int index, T node) {}
+
+ private static final class OrderByParams {
+
+ public static void replaceParameters(Map parameterMap, List orderBy) {
+ if (orderBy != null) {
+ orderBy.stream()
+ .map(OrderByExpression::getStatement)
+ .forEach(path -> replaceIdentifiedPathParameters(path, parameterMap));
+ }
+ }
+ }
+
+ private static final class SelectParams {
+
+ public static void replaceParameters(SelectClause selectClause, Map parameterMap) {
+ selectClause.getStatement().stream()
+ .map(SelectExpression::getColumnExpression)
+ .forEach(ce -> {
+ switch (ce) {
+ case SingleRowFunction func -> replaceFunctionParameters(func, parameterMap);
+ case AggregateFunction aFunc -> replaceIdentifiedPathParameters(
+ aFunc.getIdentifiedPath(), parameterMap);
+ case IdentifiedPath identifiedPath -> replaceIdentifiedPathParameters(
+ identifiedPath, parameterMap);
+ case Primitive, ?> __ -> {
+ /* No parameters */
+ }
+ case TerminologyFunction __ -> {
+ /* No parameters */
+ }
+ }
+ });
+ }
+ }
+
+ private static final class WhereParams {
+ public static void replaceParameters(WhereCondition condition, Map parameterMap) {
+ switch (condition) {
+ case null -> {
+ /* NOOP */
+ }
+ case ComparisonOperatorCondition c -> {
+ replaceComparisonLeftOperandParameters(c.getStatement(), parameterMap);
+ ensureSingleElement(replaceOperandParameters(c.getValue(), parameterMap), c::setValue);
+ }
+ case NotCondition c -> replaceParameters(c.getConditionDto(), parameterMap);
+ case MatchesCondition c -> Utils.reviseList(
+ c.getValues(), o -> replaceMatchesParameters(o, parameterMap));
+ case LikeCondition c -> replaceLikeOperandParameters(c.getValue(), parameterMap)
+ .ifPresent(c::setValue);
+ case LogicalOperatorCondition c -> c.getValues().forEach(v -> replaceParameters(v, parameterMap));
+ case ExistsCondition __ -> {
+ /* NOOP */
+ }
+ default -> throw new IllegalStateException("Unexpected value: " + condition);
+ }
+ }
+
+ private static Optional replaceLikeOperandParameters(
+ LikeOperand value, Map parameterMap) {
+ if (value instanceof QueryParameter qp) {
+ return Optional.of(qp.getName())
+ .map(parameterMap::get)
+ .map(Object::toString)
+ .map(Utils::stringToPrimitive)
+ .or(() -> Optional.of(new StringPrimitive(null)));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private static Stream replaceMatchesParameters(
+ MatchesOperand operand, Map parameterMap) {
+ if (operand instanceof QueryParameter qp) {
+ return resolveParameters(qp, parameterMap);
+ } else {
+ return null;
+ }
+ }
+
+ private static void replaceComparisonLeftOperandParameters(
+ ComparisonLeftOperand statement, Map parameterMap) {
+ switch (statement) {
+ case SingleRowFunction func -> replaceFunctionParameters(func, parameterMap);
+ case IdentifiedPath path -> replaceIdentifiedPathParameters(path, parameterMap);
+ case TerminologyFunction __ -> {
+ /* cannot contain parameters */
+ }
+ }
+ }
+ }
+
+ private static final class ContainmentParams {
+ public static void replaceParameters(Containment containment, Map parameterMap) {
+ switch (containment) {
+ case null -> {
+ /*NOOP*/
+ }
+ case ContainmentSetOperator cso -> cso.getValues().forEach(c -> replaceParameters(c, parameterMap));
+ case ContainmentNotOperator cno -> replaceParameters(cno.getContainmentExpression(), parameterMap);
+ case ContainmentClassExpression cce -> replaceContainmentClassExpressionParameters(cce, parameterMap);
+ case ContainmentVersionExpression cve -> replaceContainmentVersionExpressionParameters(
+ cve, parameterMap);
+ }
+ }
+
+ private static void replaceContainmentClassExpressionParameters(
+ ContainmentClassExpression cce, Map parameterMap) {
+ streamComparisonOperatorPredicates(cce)
+ .forEach(op -> ObjectPathParams.replaceComparisonOperatorParameters(op, parameterMap));
+ replaceParameters(cce.getContains(), parameterMap);
+ }
+
+ private static void replaceContainmentVersionExpressionParameters(
+ ContainmentVersionExpression cve, Map parameterMap) {
+ Optional.of(cve)
+ .map(ContainmentVersionExpression::getPredicate)
+ .ifPresent(op -> ObjectPathParams.replaceComparisonOperatorParameters(op, parameterMap));
+ replaceParameters(cve.getContains(), parameterMap);
+ }
+
+ private static Stream streamComparisonOperatorPredicates(
+ ContainmentClassExpression cce) {
+ return Optional.of(cce)
+ .filter(AbstractContainmentExpression::hasPredicates)
+ .map(AbstractContainmentExpression::getPredicates)
+ .stream()
+ .flatMap(List::stream)
+ .map(AndOperatorPredicate::getOperands)
+ .flatMap(Collection::stream);
+ }
+ }
+
+ private static final class ObjectPathParams {
+
+ /**
+ * Replaces all parameters.
+ * If parameters were replaced, the modified AqlObjectPath is returned.
+ * The provided path
object remains unchanged.
+ *
+ * @param path
+ * @param parameterMap
+ * @return
+ */
+ public static Optional replaceParameters(AqlObjectPath path, Map parameterMap) {
+ if (path == null) {
+ return Optional.empty();
+ }
+ return Utils.replaceChildParameters(
+ path.getPathNodes(), ObjectPathParams::replacePathNodeParameters, parameterMap)
+ .map(AqlObjectPath::new);
+ }
+
+ private static Optional replaceComparisonOperatorPredicateParameters(
+ ComparisonOperatorPredicate n, Map parameterMap) {
+ Optional replacedPath = replaceParameters(n.getPath(), parameterMap);
+
+ Optional replacedValue =
+ switch (n.getValue()) {
+ case QueryParameter qp -> Optional.of((PathPredicateOperand) ensureSingleElement(
+ resolveParameters(qp, parameterMap), p -> validateParameterSyntax(n.getPath(), p)));
+ case Primitive __ -> Optional.empty();
+ case AqlObjectPath p -> replaceParameters(p, parameterMap)
+ .map(PathPredicateOperand.class::cast);
+ default -> throw new IllegalStateException("Unexpected value: " + n.getValue());
+ };
+
+ if (replacedPath.isEmpty() && replacedValue.isEmpty()) {
+ return Optional.empty();
+ }
+
+ return Optional.of(new ComparisonOperatorPredicate(
+ replacedPath.orElse(n.getPath()), n.getOperator(), replacedValue.orElse(n.getValue())));
+ }
+
+ /**
+ * if ARCHETYPE_NODE_ID: check syntax
+ * @param path
+ * @param p
+ */
+ private static void validateParameterSyntax(AqlObjectPath path, Primitive p) {
+ if (AqlObjectPathUtil.ARCHETYPE_NODE_ID.equals(path)) {
+ if (p instanceof StringPrimitive sp) {
+ try {
+ AslRmTypeAndConcept.fromArchetypeNodeId(sp.getValue());
+ } catch (IllegalArgumentException e) {
+ throw new AqlParseException(
+ "Invalid parameter for %s".formatted(AqlObjectPathUtil.ARCHETYPE_NODE_ID));
+ }
+ } else {
+ throw new AqlParseException(
+ "Invalid parameter type for %s".formatted(AqlObjectPathUtil.ARCHETYPE_NODE_ID));
+ }
+ }
+ }
+
+ private static Optional replaceAndOperatorPredicateParameters(
+ AndOperatorPredicate and, Map parameterMap) {
+ return Utils.replaceChildParameters(
+ and.getOperands(),
+ ObjectPathParams::replaceComparisonOperatorPredicateParameters,
+ parameterMap)
+ .map(AndOperatorPredicate::new);
+ }
+
+ private static Optional replacePathNodeParameters(
+ AqlObjectPath.PathNode node, Map parameterMap) {
+ return Utils.replaceChildParameters(
+ node.getPredicateOrOperands(),
+ ObjectPathParams::replaceAndOperatorPredicateParameters,
+ parameterMap)
+ .map(l -> new AqlObjectPath.PathNode(node.getAttribute(), l));
+ }
+
+ /**
+ * {@link ComparisonOperatorPredicate}s are used in
+ *
+ * ContainmentClassExpression.predicates.predicateOrOperands.operands
/li>
+ * ContainmentVersionExpression.predicate
+ * IdentifiedPath.rootPredicate.predicateOrOperands.operands
+ * IdentifiedPath.path
(via AqlObjectPath)
+ * AqlObjectPath.pathNodes.predicateOrOperands.operands
+ * code>ComparisonOperatorPredicate.path (recursively via AqlObjectPath)
+ * code>ComparisonOperatorPredicate.value (recursively via PathPredicateOperand implementation AqlObjectPath)
+ *
+ *
+ * @param op
+ * @param parameterMap
+ */
+ public static void replaceComparisonOperatorParameters(
+ ComparisonOperatorPredicate op, Map parameterMap) {
+ Optional newPath = replaceParameters(op.getPath(), parameterMap);
+
+ Object newValue =
+ switch (op.getValue()) {
+ case null -> throw new NullPointerException(
+ "Missing value for path " + op.getPath().render());
+ case QueryParameter qp -> ensureSingleElement(
+ resolveParameters(qp, parameterMap), p -> validateParameterSyntax(op.getPath(), p));
+ case Primitive, ?> __ -> null;
+ case AqlObjectPath path -> replaceParameters(path, parameterMap);
+ default -> throw new IllegalArgumentException("Unexpected type of value: "
+ + op.getValue().getClass().getSimpleName());
+ };
+
+ newPath.ifPresent(op::setPath);
+ if (newValue != null) {
+ op.setValue((PathPredicateOperand>) newValue);
+ }
+ }
+ }
+
+ static final class TemporalPrimitivePattern {
+
+ static final Pattern TEMPORAL_PATTERN;
+
+ static {
+ // see AqlLexer.g4
+ String DIGIT = "[0-9]";
+ // Year in ISO8601:2004 is 4 digits with 0-filling as needed
+ String YEAR = DIGIT + DIGIT + DIGIT + DIGIT;
+ // month in year
+ String MONTH = nonCapturing(or("[0][1-9]", "[1][0-2]"));
+ // day in month
+ String DAY = nonCapturing(or("[0][1-9]", "[12][0-9]", "[3][0-1]"));
+ // hour in 24 hour clock
+ String HOUR = nonCapturing(or("[01][0-9]", "[2][0-3]"));
+ // minutes
+ String MINUTE = "[0-5][0-9]";
+ String SECOND = MINUTE;
+
+ String DATE_SHORT = YEAR + MONTH + DAY;
+ String DATE_LONG = YEAR + '-' + MONTH + '-' + DAY;
+ String TIME_SHORT = HOUR + MINUTE + SECOND;
+ String TIME_LONG = HOUR + ':' + MINUTE + ':' + SECOND;
+ String FRACTIONAL_SECONDS = "\\." + nonCapturing(DIGIT) + "{1,9}";
+ // hour offset, e.g. `+09:30`, or else literal `Z` indicating +0000.
+ String TIMEZONE = or("Z", nonCapturing("[-+]", HOUR, optional("[:]?" + MINUTE)));
+
+ TEMPORAL_PATTERN = Pattern.compile(or(
+ // extended datetime
+ DATE_LONG + optional("T", TIME_LONG, optional(FRACTIONAL_SECONDS), optional(TIMEZONE)),
+ // compact datetime
+ DATE_SHORT + optional("T", TIME_SHORT, optional(FRACTIONAL_SECONDS), optional(TIMEZONE)),
+ // compact & extended time
+ nonCapturing(or(TIME_SHORT, TIME_LONG)) + optional(FRACTIONAL_SECONDS) + optional(TIMEZONE)));
+ }
+
+ private TemporalPrimitivePattern() {}
+
+ private static String nonCapturing(String... content) {
+ return Arrays.stream(content).collect(Collectors.joining("", "(?:", ")"));
+ }
+
+ private static String or(String... content) {
+ return String.join("|", content);
+ }
+
+ private static String optional(String... content) {
+ return nonCapturing(content) + "?";
+ }
+
+ public static boolean matches(String input) {
+ return TEMPORAL_PATTERN.matcher(input).matches();
+ }
+ }
+
+ private static final class Utils {
+
+ public static StringPrimitive stringToPrimitive(String str) {
+ return TemporalPrimitivePattern.matches(str) ? new TemporalPrimitive(str) : new StringPrimitive(str);
+ }
+
+ /**
+ * For each entry of the list the replacementFunc
is called.
+ * It if returns new entries, the old one is replaced.
+ *
+ * @param list
+ * @param replacementFunc
+ * @param
+ */
+ public static void reviseList(List list, Function> replacementFunc) {
+ if (list.isEmpty()) {
+ return;
+ }
+ final ListIterator li = list.listIterator();
+ while (li.hasNext()) {
+ Stream extends T> replacementsStream = replacementFunc.apply(li.next());
+ if (replacementsStream != null) {
+ li.remove();
+ replacementsStream.forEach(li::add);
+ }
+ }
+ if (list.isEmpty()) {
+ throw new AqlParseException("Parameter replacement resulted in empty operand list");
+ }
+ }
+
+ /**
+ * Generic function to hierarchically replace all parameters of an object.
+ * If parameters were replaced, a new list with modified objects is returned.
+ * The provided children
remain unchanged.
+ *
+ * @param children
+ * @param childReplacementFunc returns an Optional with a modified child, if applicable
+ * @param parameterMap
+ * @return
+ */
+ public static Optional> replaceChildParameters(
+ List children,
+ BiFunction, Optional> childReplacementFunc,
+ Map parameterMap) {
+
+ ModifiedElement[] modifiedElements = IntStream.range(0, children.size())
+ .mapToObj(i -> childReplacementFunc
+ .apply(children.get(i), parameterMap)
+ .map(m -> new ModifiedElement(i, m)))
+ .flatMap(Optional::stream)
+ .toArray(ModifiedElement[]::new);
+
+ if (modifiedElements.length == 0) {
+ return Optional.empty();
+ }
+
+ C[] newChildren = (C[]) children.toArray();
+ for (ModifiedElement modifiedNode : modifiedElements) {
+ newChildren[modifiedNode.index()] = modifiedNode.node();
+ }
+ return Optional.of(List.of(newChildren));
+ }
+ }
+
+ /**
+ * Makes sure that if singleElementStream is not null, it contains exactly one element.
+ * The element is presented to the elementConsumer and then returned.
+ *
+ * @param singleElementStream
+ * @param elementConsumer
+ * @return
+ * @param
+ */
+ private static T ensureSingleElement(Stream singleElementStream, Consumer elementConsumer) {
+ if (singleElementStream == null) {
+ return null;
+ }
+ Iterator it = singleElementStream.iterator();
+ if (it.hasNext()) {
+ T first = it.next();
+ if (it.hasNext()) {
+ throw new AqlParseException("One of the parameters does not support multiple values");
+ }
+ elementConsumer.accept(first);
+ return first;
+ } else {
+ throw new AqlParseException("Empty parameter replacement results");
+ }
+ }
+}
diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlQueryUtils.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlQueryUtils.java
new file mode 100644
index 0000000000..65f91af01d
--- /dev/null
+++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/AqlQueryUtils.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2024 vitasystems GmbH.
+ *
+ * This file is part of project EHRbase
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.ehrbase.openehr.aqlengine;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.stream.Stream;
+import org.ehrbase.openehr.sdk.aql.dto.AqlQuery;
+import org.ehrbase.openehr.sdk.aql.dto.condition.ComparisonOperatorCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.ExistsCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.LikeCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.LogicalOperatorCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.MatchesCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.NotCondition;
+import org.ehrbase.openehr.sdk.aql.dto.condition.WhereCondition;
+import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction;
+import org.ehrbase.openehr.sdk.aql.dto.operand.ColumnExpression;
+import org.ehrbase.openehr.sdk.aql.dto.operand.ComparisonLeftOperand;
+import org.ehrbase.openehr.sdk.aql.dto.operand.IdentifiedPath;
+import org.ehrbase.openehr.sdk.aql.dto.operand.Operand;
+import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.QueryParameter;
+import org.ehrbase.openehr.sdk.aql.dto.operand.SingleRowFunction;
+import org.ehrbase.openehr.sdk.aql.dto.orderby.OrderByExpression;
+import org.ehrbase.openehr.sdk.aql.dto.select.SelectExpression;
+
+public final class AqlQueryUtils {
+ private AqlQueryUtils() {}
+
+ public static Stream allIdentifiedPaths(AqlQuery query) {
+
+ return Stream.of(
+ query.getSelect().getStatement().stream().flatMap(AqlQueryUtils::allIdentifiedPaths),
+ streamWhereConditions(query.getWhere()).flatMap(AqlQueryUtils::allIdentifiedPaths),
+ Optional.of(query).map(AqlQuery::getOrderBy).stream()
+ .flatMap(Collection::stream)
+ .map(OrderByExpression::getStatement))
+ .flatMap(s -> s);
+ }
+
+ public static Stream allIdentifiedPaths(WhereCondition w) {
+ if (w instanceof ComparisonOperatorCondition c) {
+ return Stream.concat(allIdentifiedPaths(c.getStatement()), allIdentifiedPaths(c.getValue()));
+ } else if (w instanceof MatchesCondition c) {
+ return Stream.of(c.getStatement());
+ } else if (w instanceof LikeCondition c) {
+ return Stream.of(c.getStatement());
+ } else if (w instanceof ExistsCondition c) {
+ // XXX Should this be included in the analysis?
+ return Stream.of(c.getValue());
+ } else {
+ throw new IllegalArgumentException("Unsupported type of " + w);
+ }
+ }
+
+ public static Stream allIdentifiedPaths(SelectExpression selectExpression) {
+ ColumnExpression columnExpression = selectExpression.getColumnExpression();
+ if (columnExpression instanceof Primitive) {
+ return Stream.empty();
+ } else if (columnExpression instanceof AggregateFunction f) {
+ return Optional.of(f).map(AggregateFunction::getIdentifiedPath).stream();
+ } else if (columnExpression instanceof IdentifiedPath ip) {
+ return Stream.of(ip);
+ } else if (columnExpression instanceof SingleRowFunction f) {
+ return f.getOperandList().stream().flatMap(AqlQueryUtils::allIdentifiedPaths);
+ } else {
+ throw new IllegalArgumentException("Unsupported type of " + columnExpression);
+ }
+ }
+
+ public static Stream allIdentifiedPaths(Operand operand) {
+ if (operand instanceof Primitive) {
+ return Stream.empty();
+ } else if (operand instanceof QueryParameter) {
+ return Stream.empty();
+ } else if (operand instanceof IdentifiedPath ip) {
+ return Stream.of(ip);
+ } else if (operand instanceof SingleRowFunction f) {
+ return f.getOperandList().stream().flatMap(AqlQueryUtils::allIdentifiedPaths);
+ } else {
+ throw new IllegalArgumentException("Unsupported type of " + operand);
+ }
+ }
+
+ public static Stream allIdentifiedPaths(ComparisonLeftOperand operand) {
+ if (operand instanceof IdentifiedPath ip) {
+ return Stream.of(ip);
+ } else if (operand instanceof SingleRowFunction f) {
+ return f.getOperandList().stream().flatMap(AqlQueryUtils::allIdentifiedPaths);
+ } else {
+ throw new IllegalArgumentException("Unsupported type of " + operand);
+ }
+ }
+
+ public static Stream streamWhereConditions(WhereCondition condition) {
+ if (condition == null) {
+ return Stream.empty();
+ }
+ return Stream.of(condition).flatMap(c -> {
+ if (c instanceof ComparisonOperatorCondition
+ || c instanceof MatchesCondition
+ || c instanceof LikeCondition
+ || c instanceof ExistsCondition) {
+ return Stream.of(c);
+ } else if (c instanceof LogicalOperatorCondition logical) {
+ return logical.getValues().stream().flatMap(AqlQueryUtils::streamWhereConditions);
+ } else if (c instanceof NotCondition not) {
+ return streamWhereConditions(not.getConditionDto());
+ } else {
+ throw new IllegalStateException("Unsupported condition type %s".formatted(c.getClass()));
+ }
+ });
+ }
+}
diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/ChangeTypeUtils.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/ChangeTypeUtils.java
new file mode 100644
index 0000000000..bf71de5f08
--- /dev/null
+++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/ChangeTypeUtils.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2024 vitasystems GmbH.
+ *
+ * This file is part of project EHRbase
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.ehrbase.openehr.aqlengine;
+
+import java.util.Map;
+import java.util.Optional;
+import org.apache.commons.collections4.BidiMap;
+import org.apache.commons.collections4.bidimap.DualHashBidiMap;
+import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
+import org.ehrbase.jooq.pg.enums.ContributionChangeType;
+
+public final class ChangeTypeUtils {
+ public static final BidiMap JOOQ_CHANGE_TYPE_TO_CODE =
+ UnmodifiableBidiMap.unmodifiableBidiMap(new DualHashBidiMap<>(Map.of(
+ ContributionChangeType.creation, "249",
+ ContributionChangeType.amendment, "250",
+ ContributionChangeType.modification, "251",
+ ContributionChangeType.synthesis, "252",
+ ContributionChangeType.Unknown, "253",
+ ContributionChangeType.deleted, "523")));
+
+ private ChangeTypeUtils() {}
+
+ public static ContributionChangeType getJooqChangeTypeByCode(String code) {
+ return Optional.ofNullable(code).map(JOOQ_CHANGE_TYPE_TO_CODE::getKey).orElse(null);
+ }
+
+ public static String getCodeByJooqChangeType(ContributionChangeType cct) {
+ return Optional.ofNullable(cct).map(JOOQ_CHANGE_TYPE_TO_CODE::get).orElse(null);
+ }
+}
diff --git a/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AqlSqlLayer.java b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AqlSqlLayer.java
new file mode 100644
index 0000000000..1b4a2ae5d8
--- /dev/null
+++ b/aql-engine/src/main/java/org/ehrbase/openehr/aqlengine/asl/AqlSqlLayer.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright (c) 2024 vitasystems GmbH.
+ *
+ * This file is part of project EHRbase
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.ehrbase.openehr.aqlengine.asl;
+
+import static org.ehrbase.openehr.sdk.util.rmconstants.RmConstants.DV_DATE;
+import static org.ehrbase.openehr.sdk.util.rmconstants.RmConstants.DV_DATE_TIME;
+import static org.ehrbase.openehr.sdk.util.rmconstants.RmConstants.DV_DURATION;
+import static org.ehrbase.openehr.sdk.util.rmconstants.RmConstants.DV_TIME;
+
+import com.nedap.archie.rm.datavalues.quantity.datetime.DvDate;
+import com.nedap.archie.rm.datavalues.quantity.datetime.DvDateTime;
+import com.nedap.archie.rm.datavalues.quantity.datetime.DvDuration;
+import com.nedap.archie.rm.datavalues.quantity.datetime.DvTime;
+import java.time.LocalDate;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import javax.annotation.Nonnull;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.SetUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.ehrbase.api.knowledge.KnowledgeCacheService;
+import org.ehrbase.api.service.SystemService;
+import org.ehrbase.jooq.pg.enums.ContributionChangeType;
+import org.ehrbase.openehr.aqlengine.ChangeTypeUtils;
+import org.ehrbase.openehr.aqlengine.asl.AslUtils.AliasProvider;
+import org.ehrbase.openehr.aqlengine.asl.model.AslRmTypeAndConcept;
+import org.ehrbase.openehr.aqlengine.asl.model.AslStructureColumn;
+import org.ehrbase.openehr.aqlengine.asl.model.condition.AslDvOrderedValueQueryCondition;
+import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFalseQueryCondition;
+import org.ehrbase.openehr.aqlengine.asl.model.condition.AslFieldValueQueryCondition;
+import org.ehrbase.openehr.aqlengine.asl.model.condition.AslNotNullQueryCondition;
+import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition;
+import org.ehrbase.openehr.aqlengine.asl.model.condition.AslQueryCondition.AslConditionOperator;
+import org.ehrbase.openehr.aqlengine.asl.model.condition.AslTrueQueryCondition;
+import org.ehrbase.openehr.aqlengine.asl.model.field.AslAggregatingField;
+import org.ehrbase.openehr.aqlengine.asl.model.field.AslColumnField;
+import org.ehrbase.openehr.aqlengine.asl.model.field.AslDvOrderedColumnField;
+import org.ehrbase.openehr.aqlengine.asl.model.field.AslField;
+import org.ehrbase.openehr.aqlengine.asl.model.query.AslQuery;
+import org.ehrbase.openehr.aqlengine.asl.model.query.AslRootQuery;
+import org.ehrbase.openehr.aqlengine.querywrapper.AqlQueryWrapper;
+import org.ehrbase.openehr.aqlengine.querywrapper.select.SelectWrapper;
+import org.ehrbase.openehr.aqlengine.querywrapper.select.SelectWrapper.SelectType;
+import org.ehrbase.openehr.aqlengine.querywrapper.where.ComparisonOperatorConditionWrapper;
+import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper;
+import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper.ComparisonConditionOperator;
+import org.ehrbase.openehr.aqlengine.querywrapper.where.ConditionWrapper.LogicalConditionOperator;
+import org.ehrbase.openehr.aqlengine.querywrapper.where.LogicalOperatorConditionWrapper;
+import org.ehrbase.openehr.dbformat.StructureRmType;
+import org.ehrbase.openehr.sdk.aql.dto.operand.AggregateFunction.AggregateFunctionName;
+import org.ehrbase.openehr.sdk.aql.dto.operand.DoublePrimitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.LongPrimitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.Primitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.StringPrimitive;
+import org.ehrbase.openehr.sdk.aql.dto.operand.TemporalPrimitive;
+import org.ehrbase.openehr.sdk.aql.dto.orderby.OrderByExpression.OrderByDirection;
+import org.ehrbase.openehr.sdk.util.OpenEHRDateTimeSerializationUtils;
+import org.ehrbase.openehr.sdk.util.rmconstants.RmConstants;
+import org.jooq.SortOrder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class AqlSqlLayer {
+
+ private static final Set NUMERIC_DV_ORDERED_TYPES = Set.of(
+ RmConstants.DV_ORDINAL,
+ RmConstants.DV_SCALE,
+ RmConstants.DV_PROPORTION,
+ RmConstants.DV_COUNT,
+ RmConstants.DV_QUANTITY);
+
+ private final KnowledgeCacheService knowledgeCache;
+ private final SystemService systemService;
+
+ public AqlSqlLayer(KnowledgeCacheService knowledgeCache, SystemService systemService) {
+ this.knowledgeCache = knowledgeCache;
+ this.systemService = systemService;
+ }
+
+ public AslRootQuery buildAslRootQuery(AqlQueryWrapper query) {
+
+ AliasProvider aliasProvider = new AliasProvider();
+ AslRootQuery aslQuery = new AslRootQuery();
+
+ // FROM
+ AslFromCreator.ContainsToOwnerProvider containsToStructureSubquery =
+ new AslFromCreator(aliasProvider, knowledgeCache).addFromClause(aslQuery, query);
+
+ // Paths
+ final AslPathCreator.PathToField pathToField = new AslPathCreator(
+ aliasProvider, knowledgeCache, systemService.getSystemId())
+ .addPathQueries(query, containsToStructureSubquery, aslQuery);
+
+ // SELECT
+ if (query.nonPrimitiveSelects().findAny().isEmpty()) {
+ addSyntheticSelect(query, containsToStructureSubquery, aslQuery);
+ } else {
+ boolean usesAggregateFunction = addSelect(query, pathToField, aslQuery);
+ addOrderBy(query, pathToField, aslQuery, usesAggregateFunction);
+ }
+
+ // WHERE
+ Optional.of(query)
+ .map(AqlQueryWrapper::where)
+ .flatMap(w -> buildWhereCondition(w, pathToField))
+ .ifPresent(aslQuery::addConditionAnd);
+
+ // LIMIT
+ aslQuery.setLimit(query.limit());
+ aslQuery.setOffset(query.offset());
+
+ return aslQuery;
+ }
+
+ private static void addOrderBy(
+ AqlQueryWrapper query,
+ AslPathCreator.PathToField pathToField,
+ AslRootQuery rootQuery,
+ boolean usesAggregateFunction) {
+ CollectionUtils.emptyIfNull(query.orderBy())
+ .forEach(o -> rootQuery.addOrderBy(
+ pathToField.getField(o.identifiedPath()),
+ o.direction() == OrderByDirection.DESC ? SortOrder.DESC : SortOrder.ASC,
+ query.distinct() || usesAggregateFunction));
+ }
+
+ /**
+ *
+ * @param query
+ * @param pathToField
+ * @param rootQuery
+ * @return if the select contains aggregate functions
+ */
+ private static boolean addSelect(
+ AqlQueryWrapper query, AslPathCreator.PathToField pathToField, AslRootQuery rootQuery) {
+ // SELECT
+ query.nonPrimitiveSelects()
+ .map(select -> switch (select.type()) {
+ case PATH -> pathToField.getField(select.getIdentifiedPath().orElseThrow());
+
+ case AGGREGATE_FUNCTION -> new AslAggregatingField(
+ select.getAggregateFunctionName(),
+ // identified path is null for COUNT(*)
+ pathToField.getField(select.getIdentifiedPath().orElse(null)),
+ select.isCountDistinct());
+ case PRIMITIVE, FUNCTION -> throw new IllegalArgumentException();
+ })
+ .forEach(rootQuery.getSelect()::add);
+
+ // GROUP BY is determined by the aggregate functions in the select
+ boolean usesAggregateFunction =
+ query.nonPrimitiveSelects().anyMatch(s -> s.type() == SelectType.AGGREGATE_FUNCTION);
+ if (usesAggregateFunction) {
+ rootQuery
+ .getGroupByFields()
+ .addAll(query.nonPrimitiveSelects()
+ .filter(s -> s.type() != SelectType.AGGREGATE_FUNCTION)
+ .map(SelectWrapper::getIdentifiedPath)
+ .flatMap(Optional::stream)
+ .map(pathToField::getField)
+ .flatMap(aslField -> aslField.fieldsForAggregation(rootQuery))
+ .distinct()
+ .toList());
+
+ } else if (query.distinct()) {
+ // DISTINCT: group by all selects
+ rootQuery
+ .getGroupByFields()
+ .addAll(rootQuery.getSelect().stream()
+ .flatMap(aslField -> aslField.fieldsForAggregation(rootQuery))
+ .distinct()
+ .toList());
+ }
+ return usesAggregateFunction;
+ }
+
+ /**
+ * If a query only selects constants, the number of results is only counted.
+ * Later, when creating the result set, this determines the number of identical rows
+ * that are returned.
+ *
+ * @param query
+ * @param containsToStructureSubQuery
+ * @param rootQuery
+ */
+ private static void addSyntheticSelect(
+ AqlQueryWrapper query,
+ AslFromCreator.ContainsToOwnerProvider containsToStructureSubQuery,
+ AslRootQuery rootQuery) {
+ AslQuery ownerForSyntheticSelect = containsToStructureSubQuery
+ // We can get the first since the first chain always must have at least one entry
+ .get(query.containsChain().chain().getFirst())
+ .owner();
+ AslColumnField field = rootQuery.getAvailableFields().stream()
+ .filter(AslColumnField.class::isInstance)
+ .map(AslColumnField.class::cast)
+ .filter(f -> f.getOwner() == ownerForSyntheticSelect)
+ .filter(f -> StringUtils.equalsAny(f.getColumnName(), "id", AslStructureColumn.VO_ID.getFieldName()))
+ .findFirst()
+ .orElseThrow();
+ rootQuery.getSelect().add(new AslAggregatingField(AggregateFunctionName.COUNT, field, false));
+ }
+
+ private Optional buildWhereCondition(
+ ConditionWrapper condition, AslPathCreator.PathToField pathToField) {
+
+ return switch (condition) {
+ case LogicalOperatorConditionWrapper lcd -> logicalOperatorCondition(
+ lcd, c -> buildWhereCondition(c, pathToField));
+ case ComparisonOperatorConditionWrapper comparison -> {
+ AslField aslField =
+ pathToField.getField(comparison.leftComparisonOperand().path());
+ if (aslField == null) {
+ throw new IllegalArgumentException("unknown field: %s"
+ .formatted(comparison
+ .leftComparisonOperand()
+ .path()
+ .getPath()
+ .render()));
+ }
+
+ if (aslField instanceof AslDvOrderedColumnField dvOrderedField) {
+ yield buildDvOrderedCondition(
+ dvOrderedField, comparison.operator(), comparison.rightComparisonOperands());
+ } else {
+ yield fieldValueQueryCondition(aslField, comparison);
+ }
+ }
+ };
+ }
+
+ @Nonnull
+ private static Optional logicalOperatorCondition(
+ LogicalOperatorConditionWrapper condition,
+ Function> conditionBuilder) {
+
+ Stream operands =
+ condition.logicalOperands().stream().map(conditionBuilder).flatMap(Optional::stream);
+
+ if (condition.operator() == LogicalConditionOperator.NOT) {
+ return Optional.of(LogicalConditionOperator.NOT.build(operands.toList()));
+ } else {
+ return AslUtils.reduceConditions(condition.operator(), operands);
+ }
+ }
+
+ @Nonnull
+ private Optional fieldValueQueryCondition(
+ AslField aslField, ComparisonOperatorConditionWrapper comparison) {
+ ComparisonConditionOperator operator = comparison.operator();
+ return Optional.of(
+ switch (operator) {
+ case EXISTS -> aslField.getExtractedColumn() != null
+ ? new AslTrueQueryCondition()
+ : new AslNotNullQueryCondition(aslField);
+ case LIKE, MATCHES, EQ, GT_EQ, GT, LT_EQ, LT, NEQ -> {
+ List> values = whereConditionValues(aslField, comparison, operator);
+ if (values.isEmpty()) {
+ yield switch (operator.getAslOperator()) {
+ case AslConditionOperator.IN,
+ AslConditionOperator.EQ,
+ AslConditionOperator.LIKE -> new AslFalseQueryCondition();
+ case AslConditionOperator.NEQ -> new AslTrueQueryCondition();
+ default -> throw new IllegalArgumentException(
+ "Unexpected operator %s".formatted(operator.getAslOperator()));
+ };
+ } else {
+ yield new AslFieldValueQueryCondition<>(aslField, operator.getAslOperator(), values);
+ }
+ }
+ });
+ }
+
+ private List> whereConditionValues(
+ AslField aslField, ComparisonOperatorConditionWrapper comparison, ComparisonConditionOperator operator) {
+ return switch (aslField.getExtractedColumn()) {
+ case TEMPLATE_ID -> AslUtils.templateIdConditionValues(
+ comparison.rightComparisonOperands(), operator, knowledgeCache::findUuidByTemplateId);
+ case ARCHETYPE_NODE_ID -> AslUtils.archetypeNodeIdConditionValues(
+ comparison.rightComparisonOperands(), operator);
+ case ROOT_CONCEPT -> AslUtils.archetypeNodeIdConditionValues(comparison.rightComparisonOperands(), operator)
+ .stream()
+ // archetype must be for COMPOSITION
+ .filter(tc -> StructureRmType.COMPOSITION.getAlias().equals(tc.aliasedRmType()))
+ .map(AslRmTypeAndConcept::concept)
+ .toList();
+ case OV_TIME_COMMITTED_DV, EHR_TIME_CREATED_DV -> AslUtils.streamStringPrimitives(comparison)
+ .map(AslUtils::toOffsetDateTime)
+ .filter(Objects::nonNull)
+ .toList();
+ case AD_CHANGE_TYPE_CODE_STRING -> AslUtils.streamStringPrimitives(comparison)
+ .map(StringPrimitive::getValue)
+ .map(ChangeTypeUtils::getJooqChangeTypeByCode)
+ .filter(Objects::nonNull)
+ .toList();
+ case AD_CHANGE_TYPE_PREFERRED_TERM, AD_CHANGE_TYPE_VALUE -> AslUtils.streamStringPrimitives(comparison)
+ .map(StringPrimitive::getValue)
+ .map(v -> "unknown".equals(v)
+ ? ContributionChangeType.Unknown
+ : ContributionChangeType.lookupLiteral(v))
+ .filter(Objects::nonNull)
+ .toList();
+ case null -> AslUtils.conditionValue(comparison.rightComparisonOperands(), operator, aslField.getType());
+ default -> AslUtils.conditionValue(comparison.rightComparisonOperands(), operator, aslField.getType());
+ };
+ }
+
+ private static Optional buildDvOrderedCondition(
+ AslDvOrderedColumnField field, ComparisonConditionOperator operator, List values) {
+ if (operator == ComparisonConditionOperator.EXISTS || operator == ComparisonConditionOperator.LIKE) {
+ throw new IllegalArgumentException("LIKE/EXISTS on DV_ORDERED is not supported");
+ }
+ List, Set>> typeToValues =
+ determinePossibleDvOrderedTypesAndValues(field.getDvOrderedTypes(), operator, values);
+ if (typeToValues.isEmpty()) {
+ return Optional.of(new AslFalseQueryCondition());
+ }
+ return AslUtils.reduceConditions(
+ LogicalConditionOperator.OR,
+ typeToValues.stream()
+ .map(e -> new AslDvOrderedValueQueryCondition<>(
+ e.getKey(), field, operator.getAslOperator(), List.copyOf(e.getValue()))));
+ }
+
+ /**
+ *
+ * @param allowedTypes
+ * @param values
+ * @return <Set<DvOrdered type>, Set<magnitude value>>
+ */
+ private static List, Set>> determinePossibleDvOrderedTypesAndValues(
+ Set allowedTypes, ComparisonConditionOperator operator, Collection values) {
+ // non-numeric DvOrdered cannot be handled together
+ HashMap> nonNumericDvOrderedTypeToValues = new HashMap<>();
+ boolean hasNumericDvOrdered = CollectionUtils.containsAny(allowedTypes, NUMERIC_DV_ORDERED_TYPES);
+ Set numericValues = new HashSet<>();
+ boolean isEqualsOp =
+ operator == ComparisonConditionOperator.EQ || operator == ComparisonConditionOperator.MATCHES;
+ for (Primitive value : values) {
+ if (value instanceof TemporalPrimitive p) {
+ handleTemporalPrimitiveForDvOrdered(
+ allowedTypes, p.getTemporal(), isEqualsOp, nonNumericDvOrderedTypeToValues);
+ } else if (value instanceof StringPrimitive p) {
+ handleStringPrimitiveForDvOrdered(allowedTypes, p, isEqualsOp, nonNumericDvOrderedTypeToValues);
+ } else if (value instanceof DoublePrimitive || value instanceof LongPrimitive) {
+ if (hasNumericDvOrdered) numericValues.add(value.getValue());
+ }
+ }
+
+ List, Set>> result = new ArrayList<>();
+ if (!numericValues.isEmpty()) {
+ Set numericDvOrderedTypes = SetUtils.intersection(allowedTypes, NUMERIC_DV_ORDERED_TYPES);
+ result.add(Pair.of(numericDvOrderedTypes, numericValues));
+ }
+ nonNumericDvOrderedTypeToValues.entrySet().stream()
+ .filter(e -> !e.getValue().isEmpty())
+ .map(e -> Pair.of(Set.of(e.getKey()), e.getValue()))
+ .forEach(result::add);
+ return result;
+ }
+
+ private static void handleStringPrimitiveForDvOrdered(
+ Set allowedTypes, StringPrimitive p, boolean isEqualsOp, HashMap