diff --git a/.gitignore b/.gitignore index e12c663bf..be25738e7 100755 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/bundle.xml ### # dsf-docker-test-setup ignores ### -dsf-docker-test-setup/bpe/last_event/time.file dsf-docker-test-setup/bpe/log/*.log dsf-docker-test-setup/bpe/log/*.log.gz dsf-docker-test-setup/bpe/plugin/*.jar @@ -56,7 +55,6 @@ dsf-docker-test-setup-3medic-ttp/**/fhir/.env ### # dsf-docker-test-setup-3medic-ttp-docker ignores ### -dsf-docker-test-setup-3medic-ttp-docker/**/bpe/last_event/time.file dsf-docker-test-setup-3medic-ttp-docker/**/bpe/log/*.log dsf-docker-test-setup-3medic-ttp-docker/**/bpe/log/*.log.gz dsf-docker-test-setup-3medic-ttp-docker/**/bpe/plugin/*.jar diff --git a/CITATION.cff b/CITATION.cff index 263b525b8..2647bf3b0 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -24,8 +24,8 @@ preferred-citation: doi: 10.3233/SHTI210060 type: proceedings title: "HiGHmed Data Sharing Framework (HiGHmed DSF)" -version: 0.7.0 -date-released: 2022-06-21 +version: 0.8.0 +date-released: 2022-10-11 url: https://github.com/highmed/highmed-dsf/wiki repository-code: https://github.com/highmed/highmed-dsf repository-artifact: https://github.com/highmed/highmed-dsf/releases @@ -95,3 +95,83 @@ references: year: 2021 pages: 111-118 doi: 10.3233/SHTI210548 + - type: proceedings + authors: + - family-names: Prokosch + given-names: Hans-Ulrich + - family-names: Bahls + given-names: Thomas + - family-names: Bialke + given-names: Martin + - family-names: Eils + given-names: Jürgen + - family-names: Fegeler + given-names: Christian + - family-names: Gruendner + given-names: Julian + - family-names: Haarbrandt + given-names: Birger + - family-names: Hampf + given-names: Christopher + - family-names: Hoffmann + given-names: Wolfgang + - family-names: Hund + given-names: Hauke + - family-names: Kampf + given-names: Marvin + - family-names: Kapsner + given-names: Lorenz A. + - family-names: Kasprzak + given-names: Piotr + - family-names: Kohlbacher + given-names: Oliver + - family-names: Krefting + given-names: Dagmar + - family-names: Mang + given-names: Jonathan M. + - family-names: Marschollek + given-names: Michael + - family-names: Mate + given-names: Sebastian + - family-names: Müller + given-names: Armin + - family-names: Prasser + given-names: Fabian + - family-names: Sass + given-names: Julian + - family-names: Semler + given-names: Sebastian + - family-names: Stenzhorn + given-names: Holger + - family-names: Thun + given-names: Sylvia + - family-names: Zenker + given-names: Sven + - family-names: Eils + given-names: Roland + title: "The COVID-19 Data Exchange Platform of the German University Medicine" + journal: Stud Health Technol Inform + volume: 294 + year: 2022 + pages: 674-678 + doi: 10.3233/SHTI220554 + - type: proceedings + authors: + - family-names: Wettstein + given-names: Reto + - family-names: Kussel + given-names: Tobias + - family-names: Hund + given-names: Hauke + - family-names: Fegeler + given-names: Christian + - family-names: Dugas + given-names: Martin + - family-names: Hamacher + given-names: Kay + title: "Secure Multi-Party Computation Based Distributed Feasibility Queries – A HiGHmed Use Case" + journal: Stud Health Technol Inform + volume: 296 + year: 2022 + pages: 41-49 + doi: 10.3233/SHTI220802 diff --git a/dsf-bpe/dsf-bpe-process-base/pom.xml b/dsf-bpe/dsf-bpe-process-base/pom.xml index 21610d117..c09d953f2 100755 --- a/dsf-bpe/dsf-bpe-process-base/pom.xml +++ b/dsf-bpe/dsf-bpe-process-base/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-bpe-pom - 0.7.0 + 0.8.0 @@ -86,6 +86,11 @@ org.slf4j jul-to-slf4j + + + com.sun.mail + jakarta.mail + diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/ConstantsBase.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/ConstantsBase.java index c3f473e34..4050578df 100755 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/ConstantsBase.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/ConstantsBase.java @@ -16,6 +16,8 @@ public interface ConstantsBase String BPMN_EXECUTION_VARIABLE_TTP_IDENTIFIER = "ttpIdentifier"; String BPMN_EXECUTION_VARIABLE_LEADING_MEDIC_IDENTIFIER = "leadingMedicIdentifier"; String BPMN_EXECUTION_VARIABLE_ALTERNATIVE_BUSINESS_KEY = "alternativeBusinessKey"; + String BPMN_EXECUTION_VARIABLE_QUESTIONNAIRE_RESPONSE_ID = "questionnaireResponseId"; + String BPMN_EXECUTION_VARIABLE_QUESTIONNAIRE_RESPONSE_COMPLETED = "questionnaireResponseCompleted"; /** * Used to distinguish if I am at the moment in a process called by another process by a CallActivity or not @@ -38,6 +40,10 @@ public interface ConstantsBase String CODESYSTEM_HIGHMED_BPMN_VALUE_CORRELATION_KEY = "correlation-key"; String CODESYSTEM_HIGHMED_BPMN_VALUE_ERROR = "error"; + String CODESYSTEM_HIGHMED_BPMN_USER_TASK = "http://highmed.org/fhir/CodeSystem/bpmn-user-task"; + String CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY = "business-key"; + String CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID = "user-task-id"; + /** * @deprecated as of release 0.6.0, use {@link #CODESYSTEM_HIGHMED_ORGANIZATION_ROLE} instead */ diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/delegate/AbstractServiceDelegate.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/delegate/AbstractServiceDelegate.java index 6dd3016ce..3979c6602 100755 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/delegate/AbstractServiceDelegate.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/delegate/AbstractServiceDelegate.java @@ -1,7 +1,5 @@ package org.highmed.dsf.bpe.delegate; -import static org.highmed.dsf.bpe.ConstantsBase.BPMN_EXECUTION_VARIABLE_LEADING_TASK; -import static org.highmed.dsf.bpe.ConstantsBase.BPMN_EXECUTION_VARIABLE_TASK; import static org.highmed.dsf.bpe.ConstantsBase.CODESYSTEM_HIGHMED_BPMN; import static org.highmed.dsf.bpe.ConstantsBase.CODESYSTEM_HIGHMED_BPMN_VALUE_ERROR; @@ -14,7 +12,6 @@ import org.highmed.dsf.fhir.authorization.read.ReadAccessHelper; import org.highmed.dsf.fhir.client.FhirWebserviceClientProvider; import org.highmed.dsf.fhir.task.TaskHelper; -import org.highmed.dsf.fhir.variables.FhirResourceValues; import org.hl7.fhir.r4.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +25,10 @@ public abstract class AbstractServiceDelegate implements JavaDelegate, Initializ private final TaskHelper taskHelper; private final ReadAccessHelper readAccessHelper; + /** + * @deprecated as of release 0.8.0, use {@link #getExecution()} instead + */ + @Deprecated protected DelegateExecution execution; public AbstractServiceDelegate(FhirWebserviceClientProvider clientProvider, TaskHelper taskHelper, @@ -60,7 +61,7 @@ public final void execute(DelegateExecution execution) throws Exception // Error boundary event, do not stop process execution catch (BpmnError error) { - Task task = getTask(execution); + Task task = getTask(); logger.debug("Error while executing service delegate " + getClass().getName(), error); logger.error( @@ -73,7 +74,7 @@ public final void execute(DelegateExecution execution) throws Exception // Not an error boundary event, stop process execution catch (Exception exception) { - Task task = getTask(execution); + Task task = getTask(); logger.debug("Error while executing service delegate " + getClass().getName(), exception); logger.error("Process {} has fatal error in step {} for task with id {}, reason: {}", @@ -95,13 +96,6 @@ public final void execute(DelegateExecution execution) throws Exception } } - private Task getTask(DelegateExecution execution) - { - return execution.getParentId() == null || execution.getParentId().equals(execution.getProcessInstanceId()) - ? getLeadingTaskFromExecutionVariables() - : getCurrentTaskFromExecutionVariables(); - } - /** * Method called by a BPMN service task * @@ -115,6 +109,11 @@ private Task getTask(DelegateExecution execution) */ protected abstract void doExecute(DelegateExecution execution) throws BpmnError, Exception; + protected final DelegateExecution getExecution() + { + return execution; + } + protected final TaskHelper getTaskHelper() { return taskHelper; @@ -130,6 +129,18 @@ protected final ReadAccessHelper getReadAccessHelper() return readAccessHelper; } + /** + * @return the active task from execution variables, i.e. the leading task if the main process is running or the + * current task if a subprocess is running. + * @throws IllegalStateException + * if execution of this service delegate has not been started + * @see ConstantsBase#BPMN_EXECUTION_VARIABLE_TASK + */ + protected final Task getTask() + { + return taskHelper.getTask(execution); + } + /** * @return the current task from execution variables, the task resource that started the current process or * subprocess @@ -139,10 +150,7 @@ protected final ReadAccessHelper getReadAccessHelper() */ protected final Task getCurrentTaskFromExecutionVariables() { - if (execution == null) - throw new IllegalStateException("execution not started"); - - return (Task) execution.getVariable(BPMN_EXECUTION_VARIABLE_TASK); + return taskHelper.getCurrentTaskFromExecutionVariables(execution); } /** @@ -153,15 +161,11 @@ protected final Task getCurrentTaskFromExecutionVariables() */ protected final Task getLeadingTaskFromExecutionVariables() { - if (execution == null) - throw new IllegalStateException("execution not started"); - - Task leadingTask = (Task) execution.getVariable(BPMN_EXECUTION_VARIABLE_LEADING_TASK); - return leadingTask != null ? leadingTask : getCurrentTaskFromExecutionVariables(); + return taskHelper.getLeadingTaskFromExecutionVariables(execution); } /** - * Uses this method to update the process engine variable {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_TASK}, + * Use this method to update the process engine variable {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_TASK}, * after modifying the {@link Task}. * * @param task @@ -172,17 +176,13 @@ protected final Task getLeadingTaskFromExecutionVariables() */ protected final void updateCurrentTaskInExecutionVariables(Task task) { - if (execution == null) - throw new IllegalStateException("execution not started"); - - Objects.requireNonNull(task, "task"); - execution.setVariable(BPMN_EXECUTION_VARIABLE_TASK, FhirResourceValues.create(task)); + taskHelper.updateCurrentTaskInExecutionVariables(execution, task); } /** - * Uses this method to update the process engine variable + * Use this method to update the process engine variable * {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_LEADING_TASK}, after modifying the {@link Task}. - * + *

* Updates the current task if no leading task is set. * * @param task @@ -193,15 +193,6 @@ protected final void updateCurrentTaskInExecutionVariables(Task task) */ protected final void updateLeadingTaskInExecutionVariables(Task task) { - if (execution == null) - throw new IllegalStateException("execution not started"); - - Objects.requireNonNull(task, "task"); - Task leadingTask = (Task) execution.getVariable(BPMN_EXECUTION_VARIABLE_LEADING_TASK); - - if (leadingTask != null) - execution.setVariable(BPMN_EXECUTION_VARIABLE_LEADING_TASK, FhirResourceValues.create(task)); - else - updateCurrentTaskInExecutionVariables(task); + taskHelper.updateLeadingTaskInExecutionVariables(execution, task); } } diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/listener/DefaultUserTaskListener.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/listener/DefaultUserTaskListener.java new file mode 100644 index 000000000..86bc1bbae --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/listener/DefaultUserTaskListener.java @@ -0,0 +1,302 @@ +package org.highmed.dsf.bpe.listener; + +import static org.highmed.dsf.bpe.ConstantsBase.BPMN_EXECUTION_VARIABLE_QUESTIONNAIRE_RESPONSE_ID; +import static org.highmed.dsf.bpe.ConstantsBase.CODESYSTEM_HIGHMED_BPMN; +import static org.highmed.dsf.bpe.ConstantsBase.CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY; +import static org.highmed.dsf.bpe.ConstantsBase.CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID; +import static org.highmed.dsf.bpe.ConstantsBase.CODESYSTEM_HIGHMED_BPMN_VALUE_ERROR; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.delegate.DelegateTask; +import org.camunda.bpm.engine.delegate.TaskListener; +import org.highmed.dsf.bpe.ConstantsBase; +import org.highmed.dsf.fhir.authorization.read.ReadAccessHelper; +import org.highmed.dsf.fhir.client.FhirWebserviceClientProvider; +import org.highmed.dsf.fhir.organization.OrganizationProvider; +import org.highmed.dsf.fhir.questionnaire.QuestionnaireResponseHelper; +import org.highmed.dsf.fhir.task.TaskHelper; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +public class DefaultUserTaskListener implements TaskListener, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(DefaultUserTaskListener.class); + + private final FhirWebserviceClientProvider clientProvider; + private final OrganizationProvider organizationProvider; + private final QuestionnaireResponseHelper questionnaireResponseHelper; + private final TaskHelper taskHelper; + private final ReadAccessHelper readAccessHelper; + + /** + * @deprecated as of release 0.8.0, use {@link #getExecution()} instead + */ + @Deprecated + protected DelegateExecution execution; + + public DefaultUserTaskListener(FhirWebserviceClientProvider clientProvider, + OrganizationProvider organizationProvider, QuestionnaireResponseHelper questionnaireResponseHelper, + TaskHelper taskHelper, ReadAccessHelper readAccessHelper) + { + this.clientProvider = clientProvider; + this.organizationProvider = organizationProvider; + this.questionnaireResponseHelper = questionnaireResponseHelper; + this.taskHelper = taskHelper; + this.readAccessHelper = readAccessHelper; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(clientProvider, "clientProvider"); + Objects.requireNonNull(organizationProvider, "organizationProvider"); + Objects.requireNonNull(questionnaireResponseHelper, "questionnaireResponseHelper"); + Objects.requireNonNull(taskHelper, "taskHelper"); + Objects.requireNonNull(readAccessHelper, "readAccessHelper"); + } + + @Override + public final void notify(DelegateTask userTask) + { + this.execution = userTask.getExecution(); + + try + { + logger.trace("Execution of user task with id='{}'", execution.getCurrentActivityId()); + + String questionnaireUrlWithVersion = userTask.getBpmnModelElementInstance().getCamundaFormKey(); + Questionnaire questionnaire = readQuestionnaire(questionnaireUrlWithVersion); + + String businessKey = execution.getBusinessKey(); + String userTaskId = userTask.getId(); + + QuestionnaireResponse questionnaireResponse = createDefaultQuestionnaireResponse( + questionnaireUrlWithVersion, businessKey, userTaskId); + addPlaceholderAnswersToQuestionnaireResponse(questionnaireResponse, questionnaire); + + modifyQuestionnaireResponse(userTask, questionnaireResponse); + + checkQuestionnaireResponse(questionnaireResponse); + + IdType created = clientProvider.getLocalWebserviceClient().withRetryForever(60000) + .create(questionnaireResponse).getIdElement(); + execution.setVariable(BPMN_EXECUTION_VARIABLE_QUESTIONNAIRE_RESPONSE_ID, created.getIdPart()); + + logger.info("Created user task with id={}, process waiting for it's completion", created.getValue()); + } + catch (Exception exception) + { + Task task = getTask(); + + logger.debug("Error while executing user task listener " + getClass().getName(), exception); + logger.error("Process {} has fatal error in step {} for task with id {}, reason: {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), task.getId(), + exception.getMessage()); + + String errorMessage = "Process " + execution.getProcessDefinitionId() + " has fatal error in step " + + execution.getActivityInstanceId() + ", reason: " + exception.getMessage(); + + task.addOutput(taskHelper.createOutput(CODESYSTEM_HIGHMED_BPMN, CODESYSTEM_HIGHMED_BPMN_VALUE_ERROR, + errorMessage)); + task.setStatus(Task.TaskStatus.FAILED); + + clientProvider.getLocalWebserviceClient().withMinimalReturn().update(task); + + // TODO evaluate throwing exception as alternative to stopping the process instance + execution.getProcessEngine().getRuntimeService().deleteProcessInstance(execution.getProcessInstanceId(), + exception.getMessage()); + } + } + + private Questionnaire readQuestionnaire(String urlWithVersion) + { + Bundle search = clientProvider.getLocalWebserviceClient().search(Questionnaire.class, + Map.of("url", Collections.singletonList(urlWithVersion))); + + List questionnaires = search.getEntry().stream().filter(Bundle.BundleEntryComponent::hasResource) + .map(Bundle.BundleEntryComponent::getResource).filter(r -> r instanceof Questionnaire) + .map(r -> (Questionnaire) r).collect(Collectors.toList()); + + if (questionnaires.size() < 1) + throw new RuntimeException("Could not find Questionnaire resource with url|version=" + urlWithVersion); + + if (questionnaires.size() > 1) + logger.info("Found {} Questionnaire resources with url|version={}, using the first", questionnaires.size(), + urlWithVersion); + + return questionnaires.get(0); + } + + private QuestionnaireResponse createDefaultQuestionnaireResponse(String questionnaireUrlWithVersion, + String businessKey, String userTaskId) + { + QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse(); + questionnaireResponse.setQuestionnaire(questionnaireUrlWithVersion); + questionnaireResponse.setStatus(QuestionnaireResponse.QuestionnaireResponseStatus.INPROGRESS); + + questionnaireResponse.setAuthor(new Reference().setType(ResourceType.Organization.name()) + .setIdentifier(organizationProvider.getLocalIdentifier())); + + questionnaireResponseHelper.addItemLeave(questionnaireResponse, + CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY, "The business-key of the process execution", + new StringType(businessKey)); + questionnaireResponseHelper.addItemLeave(questionnaireResponse, + CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID, "The user-task-id of the process execution", + new StringType(userTaskId)); + + readAccessHelper.addLocal(questionnaireResponse); + + return questionnaireResponse; + } + + private void addPlaceholderAnswersToQuestionnaireResponse(QuestionnaireResponse questionnaireResponse, + Questionnaire questionnaire) + { + questionnaire.getItem().stream() + .filter(i -> !CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY.equals(i.getLinkId())) + .filter(i -> !CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID.equals(i.getLinkId())) + .forEach(i -> createAndAddAnswerPlaceholder(questionnaireResponse, i)); + } + + private void createAndAddAnswerPlaceholder(QuestionnaireResponse questionnaireResponse, + Questionnaire.QuestionnaireItemComponent question) + { + Type answer = questionnaireResponseHelper.transformQuestionTypeToAnswerType(question); + questionnaireResponseHelper.addItemLeave(questionnaireResponse, question.getLinkId(), question.getText(), + answer); + } + + private void checkQuestionnaireResponse(QuestionnaireResponse questionnaireResponse) + { + questionnaireResponse.getItem().stream() + .filter(i -> CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY.equals(i.getLinkId())).findFirst() + .orElseThrow(() -> new RuntimeException("QuestionnaireResponse does not contain an item with linkId='" + + CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY + "'")); + + questionnaireResponse.getItem().stream() + .filter(i -> CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID.equals(i.getLinkId())).findFirst() + .orElseThrow(() -> new RuntimeException("QuestionnaireResponse does not contain an item with linkId='" + + CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID + "'")); + + if (!QuestionnaireResponse.QuestionnaireResponseStatus.INPROGRESS.equals(questionnaireResponse.getStatus())) + throw new RuntimeException("QuestionnaireResponse must be in status 'in-progress'"); + } + + /** + * Use this method to modify the {@link QuestionnaireResponse} before it will be created in state + * {@link QuestionnaireResponse.QuestionnaireResponseStatus#INPROGRESS} + * + * @param userTask + * not null, user task on which this {@link QuestionnaireResponse} is based + * @param questionnaireResponse + * not null, containing an answer placeholder for every item in the corresponding + * {@link Questionnaire} + */ + protected void modifyQuestionnaireResponse(DelegateTask userTask, QuestionnaireResponse questionnaireResponse) + { + // Nothing to do in default behaviour + } + + protected final DelegateExecution getExecution() + { + return execution; + } + + protected final TaskHelper getTaskHelper() + { + return taskHelper; + } + + protected final FhirWebserviceClientProvider getFhirWebserviceClientProvider() + { + return clientProvider; + } + + protected final ReadAccessHelper getReadAccessHelper() + { + return readAccessHelper; + } + + /** + * @return the active task from execution variables, i.e. the leading task if the main process is running or the + * current task if a subprocess is running. + * @throws IllegalStateException + * if execution of this service delegate has not been started + * @see ConstantsBase#BPMN_EXECUTION_VARIABLE_TASK + */ + protected final Task getTask() + { + return taskHelper.getTask(execution); + } + + /** + * @return the current task from execution variables, the task resource that started the current process or + * subprocess + * @throws IllegalStateException + * if execution of this service delegate has not been started + * @see ConstantsBase#BPMN_EXECUTION_VARIABLE_TASK + */ + protected final Task getCurrentTaskFromExecutionVariables() + { + return taskHelper.getCurrentTaskFromExecutionVariables(execution); + } + + /** + * @return the leading task from execution variables, same as current task if not in a subprocess + * @throws IllegalStateException + * if execution of this service delegate has not been started + * @see ConstantsBase#BPMN_EXECUTION_VARIABLE_LEADING_TASK + */ + protected final Task getLeadingTaskFromExecutionVariables() + { + return taskHelper.getLeadingTaskFromExecutionVariables(execution); + } + + /** + * Use this method to update the process engine variable {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_TASK}, + * after modifying the {@link Task}. + * + * @param task + * not null + * @throws IllegalStateException + * if execution of this service delegate has not been started + * @see ConstantsBase#BPMN_EXECUTION_VARIABLE_TASK + */ + protected final void updateCurrentTaskInExecutionVariables(Task task) + { + taskHelper.updateCurrentTaskInExecutionVariables(execution, task); + } + + /** + * Use this method to update the process engine variable + * {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_LEADING_TASK}, after modifying the {@link Task}. + *

+ * Updates the current task if no leading task is set. + * + * @param task + * not null + * @throws IllegalStateException + * if execution of this service delegate has not been started + * @see ConstantsBase#BPMN_EXECUTION_VARIABLE_LEADING_TASK + */ + protected final void updateLeadingTaskInExecutionVariables(Task task) + { + taskHelper.updateLeadingTaskInExecutionVariables(execution, task); + } +} diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/service/MailService.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/service/MailService.java new file mode 100644 index 000000000..2560e2849 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/bpe/service/MailService.java @@ -0,0 +1,155 @@ +package org.highmed.dsf.bpe.service; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.function.Consumer; + +import javax.mail.Message.RecipientType; +import javax.mail.MessagingException; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +public interface MailService +{ + /** + * Sends a plain text mail to the BPE wide configured recipients. + * + * @param subject + * not null + * @param message + * not null + */ + default void send(String subject, String message) + { + send(subject, message, (String) null); + } + + /** + * Sends a plain text mail to the given address (to) if not null or the BPE wide configured + * recipients. + * + * @param subject + * not null + * @param message + * not null + * @param to + * BPE wide configured recipients if parameter is null + */ + default void send(String subject, String message, String to) + { + send(subject, message, to == null ? null : Collections.singleton(to)); + } + + /** + * Sends a plain text mail to the given addresses (to) if not null and not empty or the BPE wide + * configured recipients. + * + * @param subject + * not null + * @param message + * not null + * @param to + * BPE wide configured recipients if parameter is null or empty + */ + default void send(String subject, String message, Collection to) + { + try + { + MimeBodyPart body = new MimeBodyPart(); + body.setText(message, StandardCharsets.UTF_8.displayName()); + + send(subject, body, to); + } + catch (MessagingException e) + { + throw new RuntimeException(e); + } + } + + /** + * Sends the given {@link MimeBodyPart} as content of a mail to the BPE wide configured recipients. + * + * @param subject + * not null + * @param body + * not null + */ + default void send(String subject, MimeBodyPart body) + { + send(subject, body, (String) null); + } + + /** + * Sends the given {@link MimeBodyPart} as content of a mail to the given address (to) if not + * null or the BPE wide configured recipients. + * + * @param subject + * not null + * @param body + * not null + * @param to + * BPE wide configured recipients if parameter is null + */ + default void send(String subject, MimeBodyPart body, String to) + { + send(subject, body, to == null ? null : Collections.singleton(to)); + } + + /** + * Sends the given {@link MimeBodyPart} as content of a mail to the given addresses (to) if not + * null and not empty or the BPE wide configured recipients. + * + * @param subject + * not null + * @param body + * not null + * @param to + * BPE wide configured recipients if parameter is null or empty + */ + default void send(String subject, MimeBodyPart body, Collection to) + { + if (to == null || to.isEmpty()) + send(subject, body, (Consumer) null); + else + send(subject, body, m -> + { + try + { + m.setRecipients(RecipientType.TO, to.stream().map(t -> + { + try + { + return new InternetAddress(t); + } + catch (AddressException e) + { + throw new RuntimeException(e); + } + }).toArray(InternetAddress[]::new)); + + m.saveChanges(); + } + catch (MessagingException e) + { + throw new RuntimeException(e); + } + }); + } + + /** + * Sends the given {@link MimeBodyPart} as content of a mail to the BPE wide configured recipients, the + * messageModifier can be used to modify elements of the generated {@link MimeMessage} before it is send to + * the SMTP server. + * + * @param subject + * not null + * @param body + * not null + * @param messageModifier + * may be null + */ + void send(String subject, MimeBodyPart body, Consumer messageModifier); +} diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseHelper.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseHelper.java new file mode 100644 index 000000000..e0fcf307a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseHelper.java @@ -0,0 +1,41 @@ +package org.highmed.dsf.fhir.questionnaire; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Type; + +public interface QuestionnaireResponseHelper +{ + default Optional getFirstItemLeaveMatchingLinkId( + QuestionnaireResponse questionnaireResponse, String linkId) + { + return getItemLeavesMatchingLinkIdAsStream(questionnaireResponse, linkId).findFirst(); + } + + default List getItemLeavesMatchingLinkIdAsList( + QuestionnaireResponse questionnaireResponse, String linkId) + { + return getItemLeavesMatchingLinkIdAsStream(questionnaireResponse, linkId).collect(Collectors.toList()); + } + + Stream getItemLeavesMatchingLinkIdAsStream( + QuestionnaireResponse questionnaireResponse, String linkId); + + default List getItemLeavesAsList( + QuestionnaireResponse questionnaireResponse) + { + return getItemLeavesAsStream(questionnaireResponse).collect(Collectors.toList()); + } + + Stream getItemLeavesAsStream( + QuestionnaireResponse questionnaireResponse); + + Type transformQuestionTypeToAnswerType(Questionnaire.QuestionnaireItemComponent question); + + void addItemLeave(QuestionnaireResponse questionnaireResponse, String linkId, String text, Type answer); +} diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ActivityDefinitionResource.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ActivityDefinitionResource.java index 7f87af589..a0e4900cb 100644 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ActivityDefinitionResource.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ActivityDefinitionResource.java @@ -3,26 +3,23 @@ import java.util.Objects; import org.hl7.fhir.r4.model.ActivityDefinition; -import org.hl7.fhir.r4.model.MetadataResource; public class ActivityDefinitionResource extends AbstractResource { - private ActivityDefinitionResource(Class type, String dependecyJarName, String url, - String version, String name, String fileName) + private ActivityDefinitionResource(String dependecyJarName, String url, String version, String name, + String fileName) { - super(type, dependecyJarName, url, version, name, fileName); + super(ActivityDefinition.class, dependecyJarName, url, version, name, fileName); } public static ActivityDefinitionResource file(String fileName) { - return new ActivityDefinitionResource(ActivityDefinition.class, null, null, null, null, - Objects.requireNonNull(fileName, "fileName")); + return new ActivityDefinitionResource(null, null, null, null, Objects.requireNonNull(fileName, "fileName")); } public static ActivityDefinitionResource dependency(String dependecyJarName, String url, String version) { - return new ActivityDefinitionResource(ActivityDefinition.class, - Objects.requireNonNull(dependecyJarName, "dependecyJarName"), Objects.requireNonNull(url, "url"), - Objects.requireNonNull(version, "version"), null, null); + return new ActivityDefinitionResource(Objects.requireNonNull(dependecyJarName, "dependecyJarName"), + Objects.requireNonNull(url, "url"), Objects.requireNonNull(version, "version"), null, null); } } diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/CodeSystemResource.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/CodeSystemResource.java index ae8404677..b9038387a 100644 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/CodeSystemResource.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/CodeSystemResource.java @@ -3,27 +3,25 @@ import java.util.Objects; import org.hl7.fhir.r4.model.CodeSystem; -import org.hl7.fhir.r4.model.MetadataResource; public class CodeSystemResource extends AbstractResource { - private CodeSystemResource(Class type, String dependencyNameAndVersion, - String codeSystemUrl, String codeSystemVersion, String codeSystemFileName) + private CodeSystemResource(String dependencyNameAndVersion, String codeSystemUrl, String codeSystemVersion, + String codeSystemFileName) { - super(type, dependencyNameAndVersion, codeSystemUrl, codeSystemVersion, null, codeSystemFileName); + super(CodeSystem.class, dependencyNameAndVersion, codeSystemUrl, codeSystemVersion, null, codeSystemFileName); } public static CodeSystemResource file(String codeSystemFileName) { - return new CodeSystemResource(CodeSystem.class, null, null, null, + return new CodeSystemResource(null, null, null, Objects.requireNonNull(codeSystemFileName, "codeSystemFileName")); } public static CodeSystemResource dependency(String dependencyNameAndVersion, String codeSystemUrl, String codeSystemVersion) { - return new CodeSystemResource(CodeSystem.class, - Objects.requireNonNull(dependencyNameAndVersion, "dependencyNameAndVersion"), + return new CodeSystemResource(Objects.requireNonNull(dependencyNameAndVersion, "dependencyNameAndVersion"), Objects.requireNonNull(codeSystemUrl, "codeSystemUrl"), Objects.requireNonNull(codeSystemVersion, "codeSystemVersion"), null); } diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/NamingSystemResource.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/NamingSystemResource.java index 81694150d..fa69d78dd 100644 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/NamingSystemResource.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/NamingSystemResource.java @@ -2,27 +2,24 @@ import java.util.Objects; -import org.hl7.fhir.r4.model.MetadataResource; import org.hl7.fhir.r4.model.NamingSystem; public class NamingSystemResource extends AbstractResource { - private NamingSystemResource(Class type, String dependencyNameAndVersion, - String namingSystemName, String namingSystemFileName) + private NamingSystemResource(String dependencyNameAndVersion, String namingSystemName, String namingSystemFileName) { - super(type, dependencyNameAndVersion, null, null, namingSystemName, namingSystemFileName); + super(NamingSystem.class, dependencyNameAndVersion, null, null, namingSystemName, namingSystemFileName); } public static NamingSystemResource file(String namingSystemFileName) { - return new NamingSystemResource(NamingSystem.class, null, null, + return new NamingSystemResource(null, null, Objects.requireNonNull(namingSystemFileName, "namingSystemFileName")); } public static NamingSystemResource dependency(String dependencyNameAndVersion, String namingSystemName) { - return new NamingSystemResource(NamingSystem.class, - Objects.requireNonNull(dependencyNameAndVersion, "dependencyNameAndVersion"), + return new NamingSystemResource(Objects.requireNonNull(dependencyNameAndVersion, "dependencyNameAndVersion"), Objects.requireNonNull(namingSystemName, "namingSystemName"), null); } } diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/QuestionnaireResource.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/QuestionnaireResource.java new file mode 100644 index 000000000..89dbe075d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/QuestionnaireResource.java @@ -0,0 +1,29 @@ +package org.highmed.dsf.fhir.resources; + +import java.util.Objects; + +import org.hl7.fhir.r4.model.Questionnaire; + +public class QuestionnaireResource extends AbstractResource +{ + private QuestionnaireResource(String dependencyNameAndVersion, String questionnaireUrl, String questionnaireVersion, + String questionnaireFileName) + { + super(Questionnaire.class, dependencyNameAndVersion, questionnaireUrl, questionnaireVersion, null, + questionnaireFileName); + } + + public static QuestionnaireResource file(String questionnaireFileName) + { + return new QuestionnaireResource(null, null, null, + Objects.requireNonNull(questionnaireFileName, "questionnaireFileName")); + } + + public static QuestionnaireResource dependency(String dependencyNameAndVersion, String questionnaireUrl, + String questionnaireVersion) + { + return new QuestionnaireResource(Objects.requireNonNull(dependencyNameAndVersion, "dependencyNameAndVersion"), + Objects.requireNonNull(questionnaireUrl, "questionnaireUrl"), + Objects.requireNonNull(questionnaireVersion, "questionnaireVersion"), null); + } +} diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ResourceProvider.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ResourceProvider.java index 673427ebc..7c1dec88e 100644 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ResourceProvider.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ResourceProvider.java @@ -12,6 +12,7 @@ import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.MetadataResource; import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.ValueSet; import org.springframework.core.env.PropertyResolver; @@ -26,6 +27,8 @@ public interface ResourceProvider Optional getNamingSystem(String name); + Optional getQuestionnaire(String url, String version); + Optional getStructureDefinition(String url, String version); Optional getValueSet(String url, String version); @@ -57,6 +60,12 @@ public Optional getNamingSystem(String name) return Optional.empty(); } + @Override + public Optional getQuestionnaire(String url, String version) + { + return Optional.empty(); + } + @Override public Optional getStructureDefinition(String url, String version) { diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ResourceProviderImpl.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ResourceProviderImpl.java index febc6bf2e..5a1332f35 100644 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ResourceProviderImpl.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ResourceProviderImpl.java @@ -25,6 +25,7 @@ import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.MetadataResource; import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.ValueSet; import org.slf4j.Logger; @@ -54,10 +55,10 @@ class ResourceProviderImpl implements ResourceProvider private static final String PLACEHOLDER_PREFIX = "#{"; private static final Pattern PLACEHOLDER_PREFIX_PATTERN = Pattern.compile(Pattern.quote(PLACEHOLDER_PREFIX)); - private final Map> activityDefinitionsByProcessKeyAndVersion = new HashMap<>(); private final Map> codeSystemsByProcessKeyAndVersion = new HashMap<>(); private final Map> namingSystemsByProcessKeyAndVersion = new HashMap<>(); + private final Map> questionnairesByProcessKeyAndVersion = new HashMap<>(); private final Map> structureDefinitionsByProcessKeyAndVersion = new HashMap<>(); private final Map> valueSetsByProcessKeyAndVersion = new HashMap<>(); @@ -66,6 +67,7 @@ class ResourceProviderImpl implements ResourceProvider ResourceProviderImpl(Map> activityDefinitionsByProcessKeyAndVersion, Map> codeSystemsByProcessKeyAndVersion, Map> namingSystemsByProcessKeyAndVersion, + Map> questionnairesByProcessKeyAndVersion, Map> structureDefinitionsByProcessKeyAndVersion, Map> valueSetsByProcessKeyAndVersion, Map> dependencyResourcesByProcessKeyAndVersion) @@ -76,6 +78,8 @@ class ResourceProviderImpl implements ResourceProvider this.codeSystemsByProcessKeyAndVersion.putAll(codeSystemsByProcessKeyAndVersion); if (namingSystemsByProcessKeyAndVersion != null) this.namingSystemsByProcessKeyAndVersion.putAll(namingSystemsByProcessKeyAndVersion); + if (questionnairesByProcessKeyAndVersion != null) + this.questionnairesByProcessKeyAndVersion.putAll(questionnairesByProcessKeyAndVersion); if (structureDefinitionsByProcessKeyAndVersion != null) this.structureDefinitionsByProcessKeyAndVersion.putAll(structureDefinitionsByProcessKeyAndVersion); if (valueSetsByProcessKeyAndVersion != null) @@ -111,6 +115,12 @@ public Optional getNamingSystem(String name) return opt; } + @Override + public Optional getQuestionnaire(String url, String version) + { + return getMetadataResource(url, version, questionnairesByProcessKeyAndVersion, Questionnaire.class); + } + @Override public Optional getStructureDefinition(String url, String version) { @@ -155,6 +165,8 @@ else if (CodeSystem.class.equals(resource.getType())) return getCodeSystem(resource.getUrl(), resource.getVersion()).map(r -> (MetadataResource) r); else if (NamingSystem.class.equals(resource.getType())) return getNamingSystem(resource.getName()).map(r -> (MetadataResource) r); + else if (Questionnaire.class.equals(resource.getType())) + return getQuestionnaire(resource.getUrl(), resource.getVersion()).map(r -> (MetadataResource) r); else if (StructureDefinition.class.equals(resource.getType())) return getStructureDefinition(resource.getUrl(), resource.getVersion()).map(r -> (MetadataResource) r); else if (ValueSet.class.equals(resource.getType())) @@ -177,15 +189,14 @@ public Stream getResources(String processKeyAndVersion, .map(r -> providerByNameAndVersion.apply(r.getDependencyNameAndVersion()).getMetadataResouce(r)) .filter(Optional::isPresent).map(Optional::get); - Stream resources = Arrays - .asList(activityDefinitionsByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, - Collections.emptyList()), - codeSystemsByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, Collections.emptyList()), - namingSystemsByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, Collections.emptyList()), - structureDefinitionsByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, - Collections.emptyList()), - valueSetsByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, Collections.emptyList())) - .stream().flatMap(List::stream); + Stream resources = Arrays.asList( + activityDefinitionsByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, Collections.emptyList()), + codeSystemsByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, Collections.emptyList()), + namingSystemsByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, Collections.emptyList()), + questionnairesByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, Collections.emptyList()), + structureDefinitionsByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, Collections.emptyList()), + valueSetsByProcessKeyAndVersion.getOrDefault(processKeyAndVersion, Collections.emptyList())).stream() + .flatMap(List::stream); return Stream.concat(resources, dependencyResources); } @@ -201,6 +212,7 @@ static ResourceProvider of(Map> resourcesByProces Map> activityDefinitionsByProcessKeyAndVersion = new HashMap<>(); Map> codeSystemsByProcessKeyAndVersion = new HashMap<>(); Map> namingSystemsByProcessKeyAndVersion = new HashMap<>(); + Map> questionnairesByProcessKeyAndVersion = new HashMap<>(); Map> structureDefinitionsByProcessKeyAndVersion = new HashMap<>(); Map> valueSetsByProcessKeyAndVersion = new HashMap<>(); @@ -214,6 +226,8 @@ else if (r instanceof CodeSystem) addOrInsert(codeSystemsByProcessKeyAndVersion, e.getKey(), (CodeSystem) r); else if (r instanceof NamingSystem) addOrInsert(namingSystemsByProcessKeyAndVersion, e.getKey(), (NamingSystem) r); + else if (r instanceof Questionnaire) + addOrInsert(questionnairesByProcessKeyAndVersion, e.getKey(), (Questionnaire) r); else if (r instanceof StructureDefinition) addOrInsert(structureDefinitionsByProcessKeyAndVersion, e.getKey(), (StructureDefinition) r); else if (r instanceof ValueSet) @@ -225,8 +239,9 @@ else if (r instanceof ValueSet) }); return new ResourceProviderImpl(activityDefinitionsByProcessKeyAndVersion, codeSystemsByProcessKeyAndVersion, - namingSystemsByProcessKeyAndVersion, structureDefinitionsByProcessKeyAndVersion, - valueSetsByProcessKeyAndVersion, dependencyResourcesByProcessKeyAndVersion); + namingSystemsByProcessKeyAndVersion, questionnairesByProcessKeyAndVersion, + structureDefinitionsByProcessKeyAndVersion, valueSetsByProcessKeyAndVersion, + dependencyResourcesByProcessKeyAndVersion); } private static void addOrInsert(Map> map, String processKeyAndVersion, diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/StructureDefinitionResource.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/StructureDefinitionResource.java index c9479e11a..75b9fbf74 100644 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/StructureDefinitionResource.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/StructureDefinitionResource.java @@ -2,28 +2,27 @@ import java.util.Objects; -import org.hl7.fhir.r4.model.MetadataResource; import org.hl7.fhir.r4.model.StructureDefinition; public class StructureDefinitionResource extends AbstractResource { - private StructureDefinitionResource(Class type, String dependecyNameAndVersion, - String structureDefinitionUrl, String structureDefinitionVersion, String structureDefinitionFileName) + private StructureDefinitionResource(String dependecyNameAndVersion, String structureDefinitionUrl, + String structureDefinitionVersion, String structureDefinitionFileName) { - super(type, dependecyNameAndVersion, structureDefinitionUrl, structureDefinitionVersion, null, - structureDefinitionFileName); + super(StructureDefinition.class, dependecyNameAndVersion, structureDefinitionUrl, structureDefinitionVersion, + null, structureDefinitionFileName); } public static StructureDefinitionResource file(String structureDefinitionFileName) { - return new StructureDefinitionResource(StructureDefinition.class, null, null, null, + return new StructureDefinitionResource(null, null, null, Objects.requireNonNull(structureDefinitionFileName, "structureDefinitionFileName")); } public static StructureDefinitionResource dependency(String dependencyNameAndVersion, String structureDefinitionUrl, String structureDefinitionVersion) { - return new StructureDefinitionResource(StructureDefinition.class, + return new StructureDefinitionResource( Objects.requireNonNull(dependencyNameAndVersion, "dependencyNameAndVersion"), Objects.requireNonNull(structureDefinitionUrl, "structureDefinitionUrl"), Objects.requireNonNull(structureDefinitionVersion, "structureDefinitionVersion"), null); diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ValueSetResource.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ValueSetResource.java index 2fe8d6c1c..7a68ff6b8 100644 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ValueSetResource.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/resources/ValueSetResource.java @@ -2,28 +2,25 @@ import java.util.Objects; -import org.hl7.fhir.r4.model.MetadataResource; import org.hl7.fhir.r4.model.ValueSet; public class ValueSetResource extends AbstractResource { - private ValueSetResource(Class type, String dependencyNameAndVersion, - String valueSetUrl, String valueSetVersion, String valueSetFileName) + private ValueSetResource(String dependencyNameAndVersion, String valueSetUrl, String valueSetVersion, + String valueSetFileName) { - super(type, dependencyNameAndVersion, valueSetUrl, valueSetVersion, null, valueSetFileName); + super(ValueSet.class, dependencyNameAndVersion, valueSetUrl, valueSetVersion, null, valueSetFileName); } public static ValueSetResource file(String valueSetFileName) { - return new ValueSetResource(ValueSet.class, null, null, null, - Objects.requireNonNull(valueSetFileName, "valueSetFileName")); + return new ValueSetResource(null, null, null, Objects.requireNonNull(valueSetFileName, "valueSetFileName")); } public static ValueSetResource dependency(String dependencyNameAndVersion, String valueSetUrl, String valueSetVersion) { - return new ValueSetResource(ValueSet.class, - Objects.requireNonNull(dependencyNameAndVersion, "dependencyNameAndVersion"), + return new ValueSetResource(Objects.requireNonNull(dependencyNameAndVersion, "dependencyNameAndVersion"), Objects.requireNonNull(valueSetUrl, "valueSetUrl"), Objects.requireNonNull(valueSetVersion, "valueSetVersion"), null); } diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/task/AbstractTaskMessageSend.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/task/AbstractTaskMessageSend.java index 0d76eff6c..36053a38d 100755 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/task/AbstractTaskMessageSend.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/task/AbstractTaskMessageSend.java @@ -125,7 +125,7 @@ protected void handleIntermediateThrowEventError(Exception exception, String err logger.debug("Error while executing Task message send " + getClass().getName(), exception); logger.error("Process {} has fatal error in step {} for task with id {}, reason: {}", - execution.getProcessDefinitionId(), execution.getActivityInstanceId(), + getExecution().getProcessDefinitionId(), getExecution().getActivityInstanceId(), task == null ? "?" : task.getId(), exception.getMessage()); try @@ -141,8 +141,8 @@ protected void handleIntermediateThrowEventError(Exception exception, String err } finally { - execution.getProcessEngine().getRuntimeService().deleteProcessInstance(execution.getProcessInstanceId(), - exception.getMessage()); + getExecution().getProcessEngine().getRuntimeService() + .deleteProcessInstance(getExecution().getProcessInstanceId(), exception.getMessage()); } } @@ -152,7 +152,7 @@ protected void handleEndEventError(Exception exception, String errorMessage) logger.debug("Error while executing Task message send " + getClass().getName(), exception); logger.error("Process {} has fatal error in step {} for task with id {}, reason: {}", - execution.getProcessDefinitionId(), execution.getActivityInstanceId(), + getExecution().getProcessDefinitionId(), getExecution().getActivityInstanceId(), task == null ? "?" : task.getId(), exception.getMessage()); if (task != null) @@ -189,7 +189,7 @@ protected void handleSendTaskError(Exception exception, String errorMessage) { logger.debug("Error while executing Task message send " + getClass().getName(), exception); logger.error("Process {} has fatal error in step {} for task with id {}, last reason: {}", - execution.getProcessDefinitionId(), execution.getActivityInstanceId(), + getExecution().getProcessDefinitionId(), getExecution().getActivityInstanceId(), task == null ? "?" : task.getId(), exception.getMessage()); try @@ -204,8 +204,8 @@ protected void handleSendTaskError(Exception exception, String errorMessage) } finally { - execution.getProcessEngine().getRuntimeService() - .deleteProcessInstance(execution.getProcessInstanceId(), exception.getMessage()); + getExecution().getProcessEngine().getRuntimeService() + .deleteProcessInstance(getExecution().getProcessInstanceId(), exception.getMessage()); } } } @@ -234,10 +234,10 @@ protected Target getTarget(DelegateExecution execution) */ protected Target getTarget() { - if (execution == null) + if (getExecution() == null) throw new IllegalStateException("execution not started"); - return (Target) execution.getVariable(BPMN_EXECUTION_VARIABLE_TARGET); + return (Target) getExecution().getVariable(BPMN_EXECUTION_VARIABLE_TARGET); } /** @@ -248,10 +248,10 @@ protected Target getTarget() */ protected Targets getTargets() { - if (execution == null) + if (getExecution() == null) throw new IllegalStateException("execution not started"); - return (Targets) execution.getVariable(BPMN_EXECUTION_VARIABLE_TARGETS); + return (Targets) getExecution().getVariable(BPMN_EXECUTION_VARIABLE_TARGETS); } /** @@ -263,10 +263,10 @@ protected Targets getTargets() */ protected void updateTargets(Targets targets) { - if (execution == null) + if (getExecution() == null) throw new IllegalStateException("execution not started"); - execution.setVariable(BPMN_EXECUTION_VARIABLE_TARGETS, TargetsValues.create(targets)); + getExecution().setVariable(BPMN_EXECUTION_VARIABLE_TARGETS, TargetsValues.create(targets)); } /** @@ -306,7 +306,8 @@ protected Stream getAdditionalInputParameters(DelegateExecut protected final String createAndSaveAlternativeBusinessKey() { String alternativeBusinessKey = UUID.randomUUID().toString(); - execution.setVariable(BPMN_EXECUTION_VARIABLE_ALTERNATIVE_BUSINESS_KEY, alternativeBusinessKey); + getExecution().setVariable(BPMN_EXECUTION_VARIABLE_ALTERNATIVE_BUSINESS_KEY, alternativeBusinessKey); + return alternativeBusinessKey; } diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/task/TaskHelper.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/task/TaskHelper.java index 5ac0c0a2d..0e25de098 100755 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/task/TaskHelper.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/task/TaskHelper.java @@ -3,6 +3,7 @@ import java.util.Optional; import java.util.stream.Stream; +import org.camunda.bpm.engine.delegate.DelegateExecution; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Task.ParameterComponent; @@ -48,4 +49,14 @@ public interface TaskHelper TaskOutputComponent createOutputUnsignedInt(String system, String code, int value); TaskOutputComponent createOutput(String system, String code, Reference reference); + + Task getTask(DelegateExecution execution); + + Task getCurrentTaskFromExecutionVariables(DelegateExecution execution); + + Task getLeadingTaskFromExecutionVariables(DelegateExecution execution); + + void updateCurrentTaskInExecutionVariables(DelegateExecution execution, Task task); + + void updateLeadingTaskInExecutionVariables(DelegateExecution execution, Task task); } diff --git a/dsf-bpe/dsf-bpe-server-jetty/pom.xml b/dsf-bpe/dsf-bpe-server-jetty/pom.xml index 346a9ec47..9d1ea2ce0 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/pom.xml +++ b/dsf-bpe/dsf-bpe-server-jetty/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-bpe-pom - 0.7.0 + 0.8.0 @@ -50,11 +50,6 @@ compile - - javax.mail - mail - - org.slf4j jul-to-slf4j diff --git a/dsf-bpe/dsf-bpe-server/pom.xml b/dsf-bpe/dsf-bpe-server/pom.xml index 2f3fce2cb..08f531e0d 100755 --- a/dsf-bpe/dsf-bpe-server/pom.xml +++ b/dsf-bpe/dsf-bpe-server/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-bpe-pom - 0.7.0 + 0.8.0 @@ -139,13 +139,21 @@ - de.hs-heilbronn.mi - db-test-utils - test + com.sun.mail + jakarta.mail + + + org.bouncycastle + bcmail-jdk15on de.hs-heilbronn.mi log4j2-utils + + + + de.hs-heilbronn.mi + db-test-utils test @@ -167,5 +175,85 @@ + + + + org.apache.maven.plugins + maven-surefire-plugin + + + log4j2-maven-surefire-config.xml + + + **/*DaoTest + **/*IntegrationTest + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + log4j2-maven-surefire-config.xml + + + **/*DaoTest + **/*IntegrationTest + + + + + + integration-test + verify + + + + + + io.fabric8 + docker-maven-plugin + true + + + start-postgres + pre-integration-test + + start + + + + + postgres:13 + + + 127.0.0.1:54321:5432 + + + Europe/Berlin + postgres + password + db + + + + + + + + + + + + stop-postgres + post-integration-test + + stop + + + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/camunda/MultiVersionBpmnParse.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/camunda/MultiVersionBpmnParse.java index 5fd514d46..fc9f40893 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/camunda/MultiVersionBpmnParse.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/camunda/MultiVersionBpmnParse.java @@ -1,15 +1,21 @@ package org.highmed.dsf.bpe.camunda; +import java.util.ArrayList; import java.util.List; +import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.camunda.bpm.engine.delegate.TaskListener; import org.camunda.bpm.engine.impl.bpmn.behavior.ClassDelegateActivityBehavior; import org.camunda.bpm.engine.impl.bpmn.parser.BpmnParse; import org.camunda.bpm.engine.impl.bpmn.parser.BpmnParser; import org.camunda.bpm.engine.impl.bpmn.parser.FieldDeclaration; import org.camunda.bpm.engine.impl.pvm.process.ActivityImpl; import org.camunda.bpm.engine.impl.pvm.process.ScopeImpl; +import org.camunda.bpm.engine.impl.task.TaskDefinition; import org.camunda.bpm.engine.impl.util.xml.Element; import org.highmed.dsf.bpe.delegate.DelegateProvider; +import org.highmed.dsf.bpe.listener.DefaultUserTaskListener; +import org.highmed.dsf.bpe.process.ProcessKeyAndVersion; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,6 +23,10 @@ public class MultiVersionBpmnParse extends BpmnParse { private static final Logger logger = LoggerFactory.getLogger(MultiVersionBpmnParse.class); + protected static final String TAGNAME_PROCESS = "process"; + protected static final String PROPERTYNAME_ID = "id"; + protected static final String PROPERTYNAME_VERSION = "http://camunda.org/schema/1.0/bpmn:versionTag"; + private final DelegateProvider delegateProvider; public MultiVersionBpmnParse(BpmnParser parser, DelegateProvider delegateProvider) @@ -37,11 +47,123 @@ public void parseServiceTaskLike(ActivityImpl activity, String elementName, Elem String className = serviceTaskElement.attributeNS(CAMUNDA_BPMN_EXTENSIONS_NS, PROPERTYNAME_CLASS); List fieldDeclarations = parseFieldDeclarations(serviceTaskElement); - logger.debug("Modifying {} for {}", activity.getActivityBehavior().getClass().getSimpleName(), className); + logger.debug("Modifying {} for {} in BPMN element with id '{}'", + activity.getActivityBehavior().getClass().getSimpleName(), className, + getElementId(serviceTaskElement)); activity.setActivityBehavior( new MultiVersionClassDelegateActivityBehavior(className, fieldDeclarations, delegateProvider)); } else - logger.debug("Not modifying {}", activity.getActivityBehavior().getClass().getCanonicalName()); + logger.debug("Not modifying {} in BPMN element with id '{}'", + activity.getActivityBehavior().getClass().getCanonicalName(), getElementId(serviceTaskElement)); + } + + @Override + protected void parseTaskListeners(Element taskListenerElement, ActivityImpl timerActivity, + TaskDefinition taskDefinition) + { + super.parseTaskListeners(taskListenerElement, timerActivity, taskDefinition); + + if (taskDefinition.getTaskListeners().getOrDefault(TaskListener.EVENTNAME_CREATE, new ArrayList<>()).stream() + .filter(l -> l instanceof MultiVersionClassDelegateTaskListener) + .map(l -> (MultiVersionClassDelegateTaskListener) l) + .noneMatch(this::containsDefaultUserTaskListenerOrSuperClassOf)) + { + logger.debug("Adding new {} for event '{}' to BPMN element with id '{}'", + DefaultUserTaskListener.class.getName(), TaskListener.EVENTNAME_CREATE, + getElementId(taskListenerElement)); + + List fieldDeclarations = parseFieldDeclarations(taskListenerElement); + TaskListener defaultUserTaskListener = new MultiVersionClassDelegateTaskListener( + DefaultUserTaskListener.class.getName(), fieldDeclarations, delegateProvider); + taskDefinition.addTaskListener(TaskListener.EVENTNAME_CREATE, defaultUserTaskListener); + } + else + { + logger.debug("Custom UserTaskListener extending {} is defined for event '{}' in BPMN element with id '{}'", + DefaultUserTaskListener.class.getName(), TaskListener.EVENTNAME_CREATE, + getElementId(taskListenerElement)); + } + } + + private boolean containsDefaultUserTaskListenerOrSuperClassOf( + MultiVersionClassDelegateTaskListener multiVersionClassDelegateTaskListener) + { + try + { + Element process = getRootElement().elements().stream().filter(e -> TAGNAME_PROCESS.equals(e.getTagName())) + .findFirst() + .orElseThrow(() -> new RuntimeException("Root element does not contain process element")); + + ProcessKeyAndVersion processKeyAndVersion = new ProcessKeyAndVersion(getElementId(process), + getElementVersion(process)); + + Class clazz = delegateProvider.getClassLoader(processKeyAndVersion) + .loadClass(multiVersionClassDelegateTaskListener.getClassName()); + + return DefaultUserTaskListener.class.isAssignableFrom(clazz); + } + catch (Exception exception) + { + throw new RuntimeException("Could not check if '" + DefaultUserTaskListener.class.getName() + + "' is assignable from '" + multiVersionClassDelegateTaskListener.getClassName() + "'", exception); + } + } + + @Override + protected TaskListener parseTaskListener(Element taskListenerElement, String taskElementId) + { + String className = taskListenerElement.attribute(PROPERTYNAME_CLASS); + + if (className != null) + { + List fieldDeclarations = parseFieldDeclarations(taskListenerElement); + + logger.debug("Modifying {} for {} in BPMN element with id '{}'", + MultiVersionClassDelegateTaskListener.class.getName(), className, + getElementId(taskListenerElement)); + return new MultiVersionClassDelegateTaskListener(className, fieldDeclarations, delegateProvider); + } + else + { + TaskListener taskListener = super.parseTaskListener(taskListenerElement, taskElementId); + logger.debug("Not modifying {} in BPMN element with id '{}", taskListener.getClass().getName(), + getElementId(taskListenerElement)); + return taskListener; + } + } + + @Override + public ExecutionListener parseExecutionListener(Element executionListenerElement, String ancestorElementId) + { + String className = executionListenerElement.attribute(PROPERTYNAME_CLASS); + + if (className != null) + { + List fieldDeclarations = parseFieldDeclarations(executionListenerElement); + + logger.debug("Modifying {} for {} in BPMN element with id '{}'", + MultiVersionClassDelegateTaskListener.class.getName(), className, + getElementId(executionListenerElement)); + return new MultiVersionClassDelegateExecutionListener(className, fieldDeclarations, delegateProvider); + } + else + { + ExecutionListener executionListener = super.parseExecutionListener(executionListenerElement, + ancestorElementId); + logger.debug("Not modifying {} in BPMN element with id '{}'", executionListener.getClass().getName(), + getElementId(executionListenerElement)); + return executionListener; + } + } + + private String getElementId(Element element) + { + return element.attribute(PROPERTYNAME_ID); + } + + private String getElementVersion(Element element) + { + return element.attribute(PROPERTYNAME_VERSION); } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/camunda/MultiVersionClassDelegateExecutionListener.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/camunda/MultiVersionClassDelegateExecutionListener.java new file mode 100644 index 000000000..752d0f52a --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/camunda/MultiVersionClassDelegateExecutionListener.java @@ -0,0 +1,85 @@ +package org.highmed.dsf.bpe.camunda; + +import java.util.List; + +import org.camunda.bpm.engine.ProcessEngineException; +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.camunda.bpm.engine.delegate.TaskListener; +import org.camunda.bpm.engine.impl.ProcessEngineLogger; +import org.camunda.bpm.engine.impl.bpmn.delegate.ExecutionListenerInvocation; +import org.camunda.bpm.engine.impl.bpmn.listener.ClassDelegateExecutionListener; +import org.camunda.bpm.engine.impl.bpmn.parser.FieldDeclaration; +import org.camunda.bpm.engine.impl.context.Context; +import org.camunda.bpm.engine.impl.persistence.entity.ExecutionEntity; +import org.camunda.bpm.engine.impl.util.ClassDelegateUtil; +import org.highmed.dsf.bpe.delegate.DelegateProvider; +import org.highmed.dsf.bpe.process.ProcessKeyAndVersion; + +public class MultiVersionClassDelegateExecutionListener extends ClassDelegateExecutionListener +{ + private final DelegateProvider delegateProvider; + + public MultiVersionClassDelegateExecutionListener(String className, List fieldDeclarations, + DelegateProvider delegateProvider) + { + super(className, fieldDeclarations); + + this.delegateProvider = delegateProvider; + } + + @Override + public void notify(DelegateExecution execution) + { + ExecutionEntity e = (ExecutionEntity) execution; + + ProcessKeyAndVersion processKeyAndVersion = new ProcessKeyAndVersion(e.getProcessDefinition().getKey(), + e.getProcessDefinition().getVersionTag()); + + ExecutionListener executionListenerInstance = getExecutionListenerInstance(processKeyAndVersion); + + try + { + Context.getProcessEngineConfiguration().getDelegateInterceptor() + .handleInvocation(new ExecutionListenerInvocation(executionListenerInstance, execution)); + + } + catch (Exception exception) + { + throw new ProcessEngineException("Exception while invoking ExecutionListener: " + exception.getMessage(), + exception); + } + } + + protected ExecutionListener getExecutionListenerInstance(ProcessKeyAndVersion processKeyAndVersion) + { + Object delegateInstance = instantiateDelegate(processKeyAndVersion, className, fieldDeclarations); + + if (delegateInstance instanceof TaskListener) + { + return (ExecutionListener) delegateInstance; + } + else + { + throw new ProcessEngineException( + delegateInstance.getClass().getName() + " doesn't implement " + ExecutionListener.class); + } + } + + private Object instantiateDelegate(ProcessKeyAndVersion processKeyAndVersion, String className, + List fieldDeclarations) + { + try + { + Class clazz = delegateProvider.getClassLoader(processKeyAndVersion).loadClass(className); + Object bean = delegateProvider.getApplicationContext(processKeyAndVersion).getBean(clazz); + + ClassDelegateUtil.applyFieldDeclaration(fieldDeclarations, bean); + return bean; + } + catch (Exception e) + { + throw ProcessEngineLogger.UTIL_LOGGER.exceptionWhileInstantiatingClass(className, e); + } + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/camunda/MultiVersionClassDelegateTaskListener.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/camunda/MultiVersionClassDelegateTaskListener.java new file mode 100644 index 000000000..5df8ec849 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/camunda/MultiVersionClassDelegateTaskListener.java @@ -0,0 +1,83 @@ +package org.highmed.dsf.bpe.camunda; + +import java.util.List; + +import org.camunda.bpm.engine.ProcessEngineException; +import org.camunda.bpm.engine.delegate.DelegateTask; +import org.camunda.bpm.engine.delegate.TaskListener; +import org.camunda.bpm.engine.impl.ProcessEngineLogger; +import org.camunda.bpm.engine.impl.bpmn.parser.FieldDeclaration; +import org.camunda.bpm.engine.impl.context.Context; +import org.camunda.bpm.engine.impl.persistence.entity.TaskEntity; +import org.camunda.bpm.engine.impl.task.delegate.TaskListenerInvocation; +import org.camunda.bpm.engine.impl.task.listener.ClassDelegateTaskListener; +import org.camunda.bpm.engine.impl.util.ClassDelegateUtil; +import org.highmed.dsf.bpe.delegate.DelegateProvider; +import org.highmed.dsf.bpe.process.ProcessKeyAndVersion; + +public class MultiVersionClassDelegateTaskListener extends ClassDelegateTaskListener +{ + private final DelegateProvider delegateProvider; + + public MultiVersionClassDelegateTaskListener(String className, List fieldDeclarations, + DelegateProvider delegateProvider) + { + super(className, fieldDeclarations); + + this.delegateProvider = delegateProvider; + } + + @Override + public void notify(DelegateTask delegateTask) + { + TaskEntity te = (TaskEntity) delegateTask; + + ProcessKeyAndVersion processKeyAndVersion = new ProcessKeyAndVersion(te.getProcessDefinition().getKey(), + te.getProcessDefinition().getVersionTag()); + + TaskListener taskListenerInstance = getTaskListenerInstance(processKeyAndVersion); + + try + { + Context.getProcessEngineConfiguration().getDelegateInterceptor() + .handleInvocation(new TaskListenerInvocation(taskListenerInstance, delegateTask)); + + } + catch (Exception e) + { + throw new ProcessEngineException("Exception while invoking TaskListener: " + e.getMessage(), e); + } + } + + protected TaskListener getTaskListenerInstance(ProcessKeyAndVersion processKeyAndVersion) + { + Object delegateInstance = instantiateDelegate(processKeyAndVersion, className, fieldDeclarations); + + if (delegateInstance instanceof TaskListener) + { + return (TaskListener) delegateInstance; + } + else + { + throw new ProcessEngineException( + delegateInstance.getClass().getName() + " doesn't implement " + TaskListener.class); + } + } + + private Object instantiateDelegate(ProcessKeyAndVersion processKeyAndVersion, String className, + List fieldDeclarations) + { + try + { + Class clazz = delegateProvider.getClassLoader(processKeyAndVersion).loadClass(className); + Object bean = delegateProvider.getApplicationContext(processKeyAndVersion).getBean(clazz); + + ClassDelegateUtil.applyFieldDeclaration(fieldDeclarations, bean); + return bean; + } + catch (Exception e) + { + throw ProcessEngineLogger.UTIL_LOGGER.exceptionWhileInstantiatingClass(className, e); + } + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/dao/LastEventTimeDao.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/dao/LastEventTimeDao.java new file mode 100644 index 000000000..32cf67b15 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/dao/LastEventTimeDao.java @@ -0,0 +1,30 @@ +package org.highmed.dsf.bpe.dao; + +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.Optional; + +public interface LastEventTimeDao +{ + Optional readLastEventTime() throws SQLException; + + /** + * @param lastEvent + * not null + * @return the given lastEvent with millisecond precision + * @throws SQLException + * if a database access error occurs + * @see LocalDateTime#truncatedTo(java.time.temporal.TemporalUnit) + */ + LocalDateTime writeLastEventTime(LocalDateTime lastEvent) throws SQLException; + + default Date writeLastEventTime(Date lastEvent) throws SQLException + { + LocalDateTime ldt = writeLastEventTime( + lastEvent == null ? null : LocalDateTime.ofInstant(lastEvent.toInstant(), ZoneId.systemDefault())); + + return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/dao/LastEventTimeDaoJdbc.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/dao/LastEventTimeDaoJdbc.java new file mode 100644 index 000000000..d0b53de26 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/dao/LastEventTimeDaoJdbc.java @@ -0,0 +1,87 @@ +package org.highmed.dsf.bpe.dao; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +public class LastEventTimeDaoJdbc extends AbstractDaoJdbc implements LastEventTimeDao, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(LastEventTimeDaoJdbc.class); + + private final String type; + + public LastEventTimeDaoJdbc(BasicDataSource dataSource, String type) + { + super(dataSource); + + this.type = type; + } + + @Override + public void afterPropertiesSet() throws Exception + { + super.afterPropertiesSet(); + + Objects.requireNonNull(type, "type"); + if (type.isBlank()) + throw new IllegalArgumentException("type is blank"); + } + + @Override + public Optional readLastEventTime() throws SQLException + { + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT last_event FROM last_events WHERE type = ? AND last_event IS NOT NULL")) + { + statement.setString(1, type); + + logger.trace("Executing query '{}'", statement); + try (ResultSet result = statement.executeQuery()) + { + if (result.next()) + return Optional.of(result.getTimestamp(1).toLocalDateTime()); + else + return Optional.empty(); + } + } + } + + @Override + public LocalDateTime writeLastEventTime(LocalDateTime lastEvent) throws SQLException + { + Objects.requireNonNull(lastEvent, "lastEvent"); + + lastEvent = lastEvent.truncatedTo(ChronoUnit.MILLIS); + + try (Connection connection = dataSource.getConnection()) + { + connection.setReadOnly(false); + + try (PreparedStatement statement = connection.prepareStatement( + "INSERT INTO last_events VALUES (?, ?) ON CONFLICT (type) WHERE type = ? DO UPDATE SET last_event = ?")) + { + statement.setString(1, type); + statement.setTimestamp(2, Timestamp.valueOf(lastEvent)); + statement.setString(3, type); + statement.setTimestamp(4, Timestamp.valueOf(lastEvent)); + + logger.trace("Executing query '{}'", statement); + statement.execute(); + } + } + + return lastEvent; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/process/ProcessesResource.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/process/ProcessesResource.java index bca5593e0..651b8987a 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/process/ProcessesResource.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/process/ProcessesResource.java @@ -15,6 +15,7 @@ import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; import org.hl7.fhir.r4.model.MetadataResource; import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.ValueSet; @@ -30,13 +31,15 @@ else if (resource instanceof CodeSystem) return from((CodeSystem) resource); else if (resource instanceof NamingSystem) return from((NamingSystem) resource); + else if (resource instanceof Questionnaire) + return from((Questionnaire) resource); else if (resource instanceof StructureDefinition) return from((StructureDefinition) resource); else if (resource instanceof ValueSet) return from((ValueSet) resource); else throw new IllegalArgumentException( - "MetadataResource of type" + resource.getClass().getName() + " not supported"); + "MetadataResource of type " + resource.getClass().getName() + " not supported"); } public static ProcessesResource from(ActivityDefinition resource) @@ -59,6 +62,13 @@ public static ProcessesResource from(NamingSystem resource) new ResourceInfo(resource.getResourceType().name(), null, null, resource.getName()), resource); } + public static ProcessesResource from(Questionnaire resource) + { + return new ProcessesResource( + new ResourceInfo(resource.getResourceType().name(), resource.getUrl(), resource.getVersion(), null), + resource); + } + public static ProcessesResource from(StructureDefinition resource) { return new ProcessesResource( diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/service/BpmnServiceDelegateValidationServiceImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/service/BpmnServiceDelegateValidationServiceImpl.java index 0f31c24b3..b89ee21d0 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/service/BpmnServiceDelegateValidationServiceImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/service/BpmnServiceDelegateValidationServiceImpl.java @@ -7,12 +7,18 @@ import org.camunda.bpm.engine.RepositoryService; import org.camunda.bpm.engine.repository.ProcessDefinition; import org.camunda.bpm.model.bpmn.instance.EndEvent; +import org.camunda.bpm.model.bpmn.instance.ExtensionElements; +import org.camunda.bpm.model.bpmn.instance.FlowNode; import org.camunda.bpm.model.bpmn.instance.IntermediateThrowEvent; import org.camunda.bpm.model.bpmn.instance.MessageEventDefinition; import org.camunda.bpm.model.bpmn.instance.Process; import org.camunda.bpm.model.bpmn.instance.SendTask; import org.camunda.bpm.model.bpmn.instance.ServiceTask; import org.camunda.bpm.model.bpmn.instance.SubProcess; +import org.camunda.bpm.model.bpmn.instance.UserTask; +import org.camunda.bpm.model.bpmn.instance.camunda.CamundaExecutionListener; +import org.camunda.bpm.model.bpmn.instance.camunda.CamundaTaskListener; +import org.camunda.bpm.model.xml.instance.ModelElementInstance; import org.highmed.dsf.bpe.delegate.DelegateProvider; import org.highmed.dsf.bpe.process.ProcessKeyAndVersion; import org.slf4j.Logger; @@ -60,50 +66,48 @@ private void validateBeanAvailabilityForProcess(Process process) { logger.debug("Checking bean availability for process {}/{}", process.getId(), process.getCamundaVersionTag()); - process.getChildElementsByType(ServiceTask.class).stream().filter(t -> t != null).map(t -> t.getCamundaClass()) - .forEach(c -> validateBeanAvailability(process, c)); - - process.getChildElementsByType(SendTask.class).stream().filter(t -> t != null).map(t -> t.getCamundaClass()) - .forEach(c -> validateBeanAvailability(process, c)); - - process.getChildElementsByType(IntermediateThrowEvent.class).stream().filter(e -> e != null) - .flatMap(e -> e.getEventDefinitions().stream() - .filter(def -> def != null && def instanceof MessageEventDefinition)) - .map(def -> (MessageEventDefinition) def).map(def -> def.getCamundaClass()) - .forEach(c -> validateBeanAvailability(process, c)); - - process.getChildElementsByType(EndEvent.class).stream().filter(e -> e != null) - .flatMap(e -> e.getEventDefinitions().stream() - .filter(def -> def != null && def instanceof MessageEventDefinition)) - .map(def -> (MessageEventDefinition) def).map(def -> def.getCamundaClass()) - .forEach(c -> validateBeanAvailability(process, c)); - - process.getChildElementsByType(SubProcess.class).stream().filter(s -> s != null) - .forEach(sp -> validateBeanAvailabilityForSubProcess(process, sp)); + validateBeanAvailabilityForProcess(process, process); } - private void validateBeanAvailabilityForSubProcess(Process process, SubProcess sProcess) + private void validateBeanAvailabilityForProcess(ModelElementInstance parent, Process process) { - sProcess.getChildElementsByType(ServiceTask.class).stream().filter(t -> t != null).map(t -> t.getCamundaClass()) - .forEach(c -> validateBeanAvailability(process, c)); + // service tasks + parent.getChildElementsByType(ServiceTask.class).stream().filter(t -> t != null) + .map(ServiceTask::getCamundaClass).forEach(c -> validateBeanAvailability(process, c)); - sProcess.getChildElementsByType(SendTask.class).stream().filter(t -> t != null).map(t -> t.getCamundaClass()) + // send tasks + parent.getChildElementsByType(SendTask.class).stream().filter(t -> t != null).map(SendTask::getCamundaClass) .forEach(c -> validateBeanAvailability(process, c)); - sProcess.getChildElementsByType(IntermediateThrowEvent.class).stream().filter(e -> e != null) + // user tasks: task listeners + parent.getChildElementsByType(UserTask.class).stream().filter(t -> t != null) + .flatMap(u -> u.getChildElementsByType(ExtensionElements.class).stream()).filter(e -> e != null) + .flatMap(e -> e.getChildElementsByType(CamundaTaskListener.class).stream()).filter(t -> t != null) + .map(CamundaTaskListener::getCamundaClass).forEach(c -> validateBeanAvailability(process, c)); + + // all elements: execution listeners + parent.getChildElementsByType(FlowNode.class).stream().filter(t -> t != null) + .flatMap(u -> u.getChildElementsByType(ExtensionElements.class).stream()).filter(e -> e != null) + .flatMap(e -> e.getChildElementsByType(CamundaExecutionListener.class).stream()).filter(t -> t != null) + .map(CamundaExecutionListener::getCamundaClass).forEach(c -> validateBeanAvailability(process, c)); + + // intermediate message throw events + parent.getChildElementsByType(IntermediateThrowEvent.class).stream().filter(e -> e != null) .flatMap(e -> e.getEventDefinitions().stream() .filter(def -> def != null && def instanceof MessageEventDefinition)) - .map(def -> (MessageEventDefinition) def).map(def -> def.getCamundaClass()) + .map(def -> (MessageEventDefinition) def).map(MessageEventDefinition::getCamundaClass) .forEach(c -> validateBeanAvailability(process, c)); - sProcess.getChildElementsByType(EndEvent.class).stream().filter(e -> e != null) + // end events + parent.getChildElementsByType(EndEvent.class).stream().filter(e -> e != null) .flatMap(e -> e.getEventDefinitions().stream() .filter(def -> def != null && def instanceof MessageEventDefinition)) - .map(def -> (MessageEventDefinition) def).map(def -> def.getCamundaClass()) + .map(def -> (MessageEventDefinition) def).map(MessageEventDefinition::getCamundaClass) .forEach(c -> validateBeanAvailability(process, c)); - sProcess.getChildElementsByType(SubProcess.class).stream().filter(s -> s != null) - .forEach(sp -> validateBeanAvailabilityForSubProcess(process, sp)); + // sub processes + parent.getChildElementsByType(SubProcess.class).stream().filter(s -> s != null) + .forEach(subProcess -> validateBeanAvailabilityForProcess(subProcess, process)); } private void validateBeanAvailability(Process process, String className) diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/service/LoggingMailService.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/service/LoggingMailService.java new file mode 100644 index 000000000..b13c49138 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/service/LoggingMailService.java @@ -0,0 +1,61 @@ +package org.highmed.dsf.bpe.service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Properties; +import java.util.function.Consumer; + +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoggingMailService implements MailService +{ + private static final Logger logger = LoggerFactory.getLogger(LoggingMailService.class); + private static final Logger mailLogger = LoggerFactory.getLogger("mail-logger"); + + private MimeMessage createMimeMessage(String subject, MimeBodyPart body) + { + try + { + MimeMessage mimeMessage = new MimeMessage(Session.getInstance(new Properties())); + mimeMessage.setSubject(subject); + mimeMessage.setContent(new MimeMultipart(body)); + mimeMessage.saveChanges(); + + return mimeMessage; + } + catch (MessagingException e) + { + throw new RuntimeException(e); + } + } + + @Override + public void send(String subject, MimeBodyPart body, Consumer messageModifier) + { + logger.info("SMTP mail service not configured, see debug log for mail subject / content"); + try + { + if (logger.isDebugEnabled() || mailLogger.isInfoEnabled()) + { + MimeMessage message = createMimeMessage(subject, body); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + message.writeTo(out); + + logger.debug("Subject: {}, Content: {}", subject, out.toString()); + mailLogger.info("Subject: {}, Content: {}", subject, out.toString()); + } + } + catch (IOException | MessagingException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/service/SmtpMailService.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/service/SmtpMailService.java new file mode 100644 index 000000000..122e6c250 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/service/SmtpMailService.java @@ -0,0 +1,543 @@ +package org.highmed.dsf.bpe.service; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.Key; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import javax.mail.Authenticator; +import javax.mail.Message.RecipientType; +import javax.mail.MessagingException; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import javax.net.ssl.SSLSocketFactory; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.StringLayout; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.appender.AbstractManager; +import org.apache.logging.log4j.core.filter.ThresholdFilter; +import org.apache.logging.log4j.core.layout.ByteBufferDestination; +import org.apache.logging.log4j.core.layout.HtmlLayout; +import org.apache.logging.log4j.core.net.MailManager; +import org.apache.logging.log4j.core.net.MailManager.FactoryData; +import org.apache.logging.log4j.core.net.MailManagerFactory; +import org.apache.logging.log4j.core.net.SmtpManager; +import org.apache.logging.log4j.util.Strings; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.cms.AttributeTable; +import org.bouncycastle.asn1.smime.SMIMECapabilitiesAttribute; +import org.bouncycastle.asn1.smime.SMIMECapability; +import org.bouncycastle.asn1.smime.SMIMECapabilityVector; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.mail.smime.SMIMEException; +import org.bouncycastle.mail.smime.SMIMESignedGenerator; +import org.bouncycastle.operator.OperatorCreationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +import de.rwh.utils.crypto.context.SSLContextFactory; + +public class SmtpMailService implements MailService, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(SmtpMailService.class); + + private static final class Layout implements StringLayout + { + final HtmlLayout delegate = HtmlLayout.newBuilder().setDatePattern("yyyy-MM-dd HH:mm:ss.nnnn").build(); + + final String debugLogLocation; + + Layout(String debugLogLocation) + { + this.debugLogLocation = debugLogLocation; + } + + @Override + public byte[] getFooter() + { + StringBuilder sbuf = new StringBuilder(); + sbuf.append("").append(Strings.LINE_SEPARATOR); + sbuf.append("
").append(Strings.LINE_SEPARATOR); + sbuf.append("For more details see debug log at ").append(Strings.LINE_SEPARATOR); + sbuf.append(debugLogLocation).append(Strings.LINE_SEPARATOR); + sbuf.append("").append(Strings.LINE_SEPARATOR); + sbuf.append("
").append(Strings.LINE_SEPARATOR); + sbuf.append("").append(Strings.LINE_SEPARATOR); + + return sbuf.toString().getBytes(getCharset()); + } + + @Override + public byte[] getHeader() + { + return delegate.getHeader(); + } + + @Override + public byte[] toByteArray(LogEvent event) + { + return delegate.toByteArray(event); + } + + @Override + public String getContentType() + { + return delegate.getContentType(); + } + + @Override + public Map getContentFormat() + { + return delegate.getContentFormat(); + } + + @Override + public void encode(LogEvent source, ByteBufferDestination destination) + { + delegate.encode(source, destination); + } + + @Override + public Charset getCharset() + { + return delegate.getCharset(); + } + + @Override + public String toSerializable(LogEvent event) + { + return delegate.toSerializable(event); + } + } + + private static final class Log4jAppender extends AbstractAppender + { + private final MailManager manager; + + private Log4jAppender(Session session, MimeMessage message, String subject, int messageBufferSize, + String debugLogLocation) + { + super("SmtpMailService.Log4jAppender", ThresholdFilter.createFilter(null, null, null), + new Layout(debugLogLocation), false, null); + + MailManagerFactory factory = (name, data) -> + { + return new SmtpManager(name, session, message, data) + { + }; + }; + FactoryData data = new FactoryData(null, null, null, null, null, null, event -> subject, null, null, 0, + null, null, false, messageBufferSize, null, null); + manager = AbstractManager.getManager("SmtpMailService.Log4jAppender.Manager", factory, data); + } + + @Override + public boolean isFiltered(LogEvent event) + { + boolean filtered = super.isFiltered(event); + + if (filtered) + manager.add(event); + + return filtered; + } + + @Override + public void append(LogEvent event) + { + manager.sendEvents(getLayout(), event); + } + } + + public static final String DEFAULT_DEBUG_LOG_LOCATION = "/opt/bpe/log/bpe.log"; + + private final InternetAddress fromAddress; + private final InternetAddress[] toAddresses; + private final InternetAddress[] toAddressesCc; + private final InternetAddress[] replyToAddresses; + + private final Session session; + private final SMIMESignedGenerator smimeSignedGenerator; + + private final Log4jAppender log4jAppender; + + /** + * SMTP, non authentication, mails not signed, no mails on error log events, value of + * {@link #DEFAULT_DEBUG_LOG_LOCATION} as debug log location + * + * @param fromAddress + * not null + * @param toAddresses + * not null, at least one + * @param mailServerHostname + * not null + * @param mailServerPort + * not null + */ + public SmtpMailService(String fromAddress, List toAddresses, String mailServerHostname, int mailServerPort) + { + this(fromAddress, toAddresses, null, null, false, mailServerHostname, mailServerPort, null, null, null, null, + null, null, null, false, 0, DEFAULT_DEBUG_LOG_LOCATION); + } + + /** + * @param fromAddress + * not null + * @param toAddresses + * not null, at least one + * @param toAddressesCc + * may be null + * @param replyToAddresses + * may be null + * @param useSmtps + * @param mailServerHostname + * not null + * @param mailServerPort + * > 0 + * @param mailServerUsername + * may be null + * @param mailServerPassword + * may be null + * @param trustStore + * may be null + * @param keyStore + * may be null + * @param keyStorePassword + * @param signStore + * may be null + * @param signStorePassword + * @param mailOnErrorLogEvent + * true if mail should be send for error log events + * @param mailOnErrorLogEventBufferSize + * >= 0 + * @param debugLogLocation + * not null + */ + public SmtpMailService(String fromAddress, List toAddresses, List toAddressesCc, + List replyToAddresses, boolean useSmtps, String mailServerHostname, int mailServerPort, + String mailServerUsername, char[] mailServerPassword, KeyStore trustStore, KeyStore keyStore, + char[] keyStorePassword, KeyStore signStore, char[] signStorePassword, boolean mailOnErrorLogEvent, + int mailOnErrorLogEventBufferSize, String debugLogLocation) + { + this.fromAddress = toInternetAddress(fromAddress).orElse(null); + this.toAddresses = toAddresses == null ? new InternetAddress[0] + : toAddresses.stream().flatMap(s -> toInternetAddress(s).stream()).toArray(InternetAddress[]::new); + this.toAddressesCc = toAddressesCc == null ? new InternetAddress[0] + : toAddressesCc.stream().flatMap(s -> toInternetAddress(s).stream()).toArray(InternetAddress[]::new); + this.replyToAddresses = replyToAddresses == null ? new InternetAddress[0] + : replyToAddresses.stream().flatMap(s -> toInternetAddress(s).stream()).toArray(InternetAddress[]::new); + + session = createSession(useSmtps, mailServerHostname, mailServerPort, mailServerUsername, mailServerPassword, + trustStore, keyStore, keyStorePassword); + + smimeSignedGenerator = createSmimeSignedGenerator(fromAddress, signStore, signStorePassword); + + log4jAppender = !mailOnErrorLogEvent ? null + : new Log4jAppender(session, createMimeMessage("DSF BPE Error", null), "DSF BPE Error", + mailOnErrorLogEventBufferSize, debugLogLocation); + } + + private Optional toInternetAddress(String fromAddress) + { + if (fromAddress == null || fromAddress.isBlank()) + return Optional.empty(); + + try + { + return Optional.of(new InternetAddress(fromAddress)); + } + catch (AddressException e) + { + logger.warn("Unable to create {} from {}: {} - {}", InternetAddress.class.getName(), fromAddress, + e.getClass().getName(), e.getMessage()); + + return Optional.empty(); + } + } + + @Override + public void afterPropertiesSet() throws Exception + { + if (fromAddress == null) + throw new IllegalArgumentException("no valid from address configured"); + if (toAddresses.length == 0) + throw new IllegalArgumentException("no valid to addresses configured"); + } + + private Session createSession(boolean useSmtps, String mailServerHostname, int mailServerPort, + String mailServerUsername, char[] mailServerPassword, KeyStore trustStore, KeyStore keyStore, + char[] keyStorePassword) + { + Properties properties = new Properties(); + + Authenticator authenticator = null; + if (mailServerUsername != null && mailServerPassword != null) + { + authenticator = new Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication(mailServerUsername, String.copyValueOf(mailServerPassword)); + } + }; + + properties.put("mail.smtp.auth", "true"); + + if (!useSmtps) + logger.warn( + "Username/Password configured, SMTPS not enabled. Password will be send without encryption! Consider activating/using SMTP over TLS (aka SMTPS)"); + } + + if (useSmtps) + { + properties.put("mail.smtp.ssl.enable", "true"); + properties.put("mail.transport.protocol", "smtps"); + properties.put("mail.smtp.socketFactory.fallback", "false"); + properties.put("mail.smtp.ssl.checkserveridentity", "true"); + properties.put("mail.smtp.ssl.socketFactory", + createSslSocketFactory(trustStore, keyStore, keyStorePassword)); + } + + properties.put("mail.smtp.host", mailServerHostname); + properties.put("mail.smtp.port", mailServerPort); + + return Session.getInstance(properties, authenticator); + } + + public SSLSocketFactory createSslSocketFactory(KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword) + { + try + { + // uses default jvm trust / keys if not configured (respective trustStore/keyStore fields null) + return new SSLContextFactory().createSSLContext(trustStore, keyStore, keyStorePassword).getSocketFactory(); + } + catch (UnrecoverableKeyException | KeyManagementException | KeyStoreException | NoSuchAlgorithmException e) + { + logger.warn("Unable to create custom ssl socket factory: {} - {}", e.getClass().getName(), e.getMessage()); + throw new RuntimeException(e); + } + } + + private SMIMESignedGenerator createSmimeSignedGenerator(String fromAddress, KeyStore signStore, + char[] signStorePassword) + { + if (signStore == null) + return null; + + try + { + Optional certificates = getFirstCertificateChain(signStore) + .filter(hasCertificateForAddress(fromAddress)); + if (certificates.isEmpty()) + { + logger.warn("Mail signing certificate store has no S/MIME certificate for {}, not signing mails", + fromAddress); + return null; + } + + Optional pivateKey = getFirstPrivateKey(signStore, signStorePassword); + if (pivateKey.isEmpty()) + { + logger.warn("Mail signing certificate store has no private key, not signing mails", fromAddress); + return null; + } + + Certificate certificate = certificates + .flatMap(c -> Stream.of(c).filter(hasSubjectAlternativeNameRfc822Name(fromAddress)).findFirst()) + .get(); + + ASN1EncodableVector signedAttrs = new ASN1EncodableVector(); + SMIMECapabilityVector caps = new SMIMECapabilityVector(); + caps.addCapability(SMIMECapability.aES128_CBC); + caps.addCapability(SMIMECapability.aES256_CBC); + signedAttrs.add(new SMIMECapabilitiesAttribute(caps)); + + SMIMESignedGenerator generator = new SMIMESignedGenerator(); + generator.addSignerInfoGenerator( + new JcaSimpleSignerInfoGeneratorBuilder().setProvider(new BouncyCastleProvider()) + .setSignedAttributeGenerator(new AttributeTable(signedAttrs)).build("SHA256withRSA", + pivateKey.get(), new X509CertificateHolder(certificate.getEncoded()))); + generator.addCertificates(new JcaCertStore(certificates.map(Arrays::asList).get())); + + return generator; + } + catch (KeyStoreException | CertificateException | OperatorCreationException | IOException e) + { + throw new RuntimeException(e); + } + } + + private Optional getFirstCertificateChain(KeyStore store) throws KeyStoreException + { + return Collections.list(store.aliases()).stream().map(getCertificateChain(store)).findFirst(); + } + + private Function getCertificateChain(KeyStore store) + { + return alias -> + { + try + { + return store.getCertificateChain(alias); + } + catch (KeyStoreException e) + { + throw new RuntimeException(e); + } + }; + } + + private Predicate hasCertificateForAddress(String address) + { + return chain -> hasSubjectAlternativeNameRfc822Name(address).test(chain[0]); + } + + private Predicate hasSubjectAlternativeNameRfc822Name(String address) + { + return certificate -> + { + try + { + X509CertificateHolder holder = new X509CertificateHolder(certificate.getEncoded()); + X509Certificate x509Certificate = new JcaX509CertificateConverter().getCertificate(holder); + Collection> sanCollections = x509Certificate.getSubjectAlternativeNames(); + if (sanCollections == null) + return false; + else + // first entry SAN type (1 = Rfc822Name), second entry SAN value + return sanCollections.stream() + .anyMatch(l -> Objects.equals(1, l.get(0)) && Objects.equals(address, l.get(1))); + } + catch (CertificateException | IOException e) + { + throw new RuntimeException(e); + } + }; + } + + private Optional getFirstPrivateKey(KeyStore store, char[] keyPassword) throws KeyStoreException + { + return Collections.list(store.aliases()).stream().map(getPrivateKey(store, keyPassword)).findFirst() + .filter(k -> k instanceof PrivateKey).map(k -> (PrivateKey) k); + } + + private Function getPrivateKey(KeyStore store, char[] keyPassword) + { + return alias -> + { + try + { + return store.getKey(alias, keyPassword); + } + catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + }; + } + + private MimeMessage createMimeMessage(String subject, MimeMultipart mimeMultipart) + { + try + { + MimeMessage mimeMessage = new MimeMessage(session); + mimeMessage.setFrom(fromAddress); + mimeMessage.setRecipients(RecipientType.TO, toAddresses); + mimeMessage.setRecipients(RecipientType.CC, toAddressesCc); + mimeMessage.setReplyTo(replyToAddresses); + mimeMessage.setSubject(subject); + if (mimeMultipart != null) + mimeMessage.setContent(mimeMultipart); + mimeMessage.saveChanges(); + + return mimeMessage; + } + catch (MessagingException e) + { + throw new RuntimeException(e); + } + } + + private MimeMultipart signMessage(MimeBodyPart body) + { + if (smimeSignedGenerator != null) + { + try + { + return smimeSignedGenerator.generate(body); + } + catch (SMIMEException e) + { + throw new RuntimeException(e); + } + } + else + { + try + { + return new MimeMultipart(body); + } + catch (MessagingException e) + { + throw new RuntimeException(e); + } + } + } + + @Override + public void send(String subject, MimeBodyPart body, Consumer messageModifier) + { + MimeMessage message = createMimeMessage(subject, signMessage(body)); + + if (messageModifier != null) + messageModifier.accept(message); + + try + { + Transport.send(message); + } + catch (MessagingException e) + { + logger.warn("Unable to send message: {} - {}", e.getClass().getName(), e.getMessage()); + throw new RuntimeException(e); + } + } + + public Log4jAppender getLog4jAppender() + { + return log4jAppender; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/CamundaConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/CamundaConfig.java index b8032566a..2e27b3676 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/CamundaConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/CamundaConfig.java @@ -16,6 +16,7 @@ import org.highmed.dsf.bpe.delegate.DelegateProviderImpl; import org.highmed.dsf.bpe.listener.CallActivityListener; import org.highmed.dsf.bpe.listener.DefaultBpmnParseListener; +import org.highmed.dsf.bpe.listener.DefaultUserTaskListener; import org.highmed.dsf.bpe.listener.EndListener; import org.highmed.dsf.bpe.listener.StartListener; import org.highmed.dsf.bpe.plugin.ProcessPluginProvider; @@ -112,7 +113,7 @@ public DefaultBpmnParseListener defaultBpmnParseListener() public SpringProcessEngineConfiguration processEngineConfiguration() throws IOException { var c = new MultiVersionSpringProcessEngineConfiguration(delegateProvider()); - c.setProcessEngineName("highmed"); + c.setProcessEngineName("dsf"); c.setDataSource(transactionAwareDataSource()); c.setTransactionManager(transactionManager()); c.setDatabaseSchemaUpdate("false"); @@ -162,4 +163,11 @@ public ProcessPluginProvider processPluginProvider() return new ProcessPluginProviderImpl(fhirConfig.fhirContext(), processPluginDirectoryPath, applicationContext, environment); } + + @Bean + public DefaultUserTaskListener defaultUserTaskListener() + { + return new DefaultUserTaskListener(fhirConfig.clientProvider(), fhirConfig.organizationProvider(), + fhirConfig.questionnaireResponseHelper(), fhirConfig.taskHelper(), fhirConfig.readAccessHelper()); + } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/DaoConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/DaoConfig.java index 244f5abf5..6b81b657f 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/DaoConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/DaoConfig.java @@ -1,6 +1,8 @@ package org.highmed.dsf.bpe.spring.config; import org.apache.commons.dbcp2.BasicDataSource; +import org.highmed.dsf.bpe.dao.LastEventTimeDao; +import org.highmed.dsf.bpe.dao.LastEventTimeDaoJdbc; import org.highmed.dsf.bpe.dao.ProcessPluginResourcesDao; import org.highmed.dsf.bpe.dao.ProcessPluginResourcesDaoJdbc; import org.highmed.dsf.bpe.dao.ProcessStateDao; @@ -47,4 +49,16 @@ public ProcessStateDao processStateDao() { return new ProcessStateDaoJdbc(dataSource()); } + + @Bean + public LastEventTimeDao lastEventTimeDaoTask() + { + return new LastEventTimeDaoJdbc(dataSource(), "Task"); + } + + @Bean + public LastEventTimeDao lastEventTimeDaoQuestionnaireResponse() + { + return new LastEventTimeDaoJdbc(dataSource(), "QuestionnaireResponse"); + } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/FhirConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/FhirConfig.java index e5aaa995a..45111e477 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/FhirConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/FhirConfig.java @@ -26,16 +26,24 @@ import org.highmed.dsf.fhir.organization.EndpointProvider; import org.highmed.dsf.fhir.organization.OrganizationProvider; import org.highmed.dsf.fhir.organization.OrganizationProviderImpl; +import org.highmed.dsf.fhir.questionnaire.QuestionnaireResponseHandler; +import org.highmed.dsf.fhir.questionnaire.QuestionnaireResponseHelper; +import org.highmed.dsf.fhir.questionnaire.QuestionnaireResponseHelperImpl; +import org.highmed.dsf.fhir.questionnaire.QuestionnaireResponseSubscriptionHandlerFactory; import org.highmed.dsf.fhir.service.ReferenceCleaner; import org.highmed.dsf.fhir.service.ReferenceCleanerImpl; import org.highmed.dsf.fhir.service.ReferenceExtractor; import org.highmed.dsf.fhir.service.ReferenceExtractorImpl; +import org.highmed.dsf.fhir.subscription.SubscriptionHandlerFactory; import org.highmed.dsf.fhir.task.TaskHandler; import org.highmed.dsf.fhir.task.TaskHelper; import org.highmed.dsf.fhir.task.TaskHelperImpl; +import org.highmed.dsf.fhir.task.TaskSubscriptionHandlerFactory; import org.highmed.dsf.fhir.websocket.FhirConnector; import org.highmed.dsf.fhir.websocket.FhirConnectorImpl; -import org.highmed.dsf.fhir.websocket.LastEventTimeIo; +import org.highmed.dsf.fhir.websocket.ResourceHandler; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; @@ -61,6 +69,9 @@ public class FhirConfig implements InitializingBean @Autowired private PropertiesConfig propertiesConfig; + @Autowired + private DaoConfig daoConfig; + @Autowired @Lazy private ProcessEngine processEngine; @@ -83,19 +94,6 @@ public ReferenceExtractor referenceExtractor() return new ReferenceExtractorImpl(); } - @Bean - public LastEventTimeIo lastEventTimeIo() - { - return new LastEventTimeIo(propertiesConfig.getLastEventTimeFile()); - } - - @Bean - public TaskHandler taskHandler() - { - return new TaskHandler(processEngine.getRuntimeService(), processEngine.getRepositoryService(), - clientProvider().getLocalWebserviceClient(), taskHelper()); - } - @Bean public FhirWebsocketClientProvider clientProvider() { @@ -187,17 +185,53 @@ public EndpointProvider endpointProvider() } @Bean - public FhirConnector fhirConnector() + public ResourceHandler taskHandler() + { + return new TaskHandler(processEngine.getRuntimeService(), processEngine.getRepositoryService(), + clientProvider().getLocalWebserviceClient(), taskHelper()); + } + + @Bean + public SubscriptionHandlerFactory taskSubscriptionHandlerFactory() + { + return new TaskSubscriptionHandlerFactory(taskHandler(), daoConfig.lastEventTimeDaoTask()); + } + + @Bean + public FhirConnector fhirConnectorTask() { - return new FhirConnectorImpl(clientProvider(), taskHandler(), lastEventTimeIo(), fhirContext(), - propertiesConfig.getSubscriptionSearchParameter(), propertiesConfig.getWebsocketRetrySleepMillis(), + return new FhirConnectorImpl<>("Task", clientProvider(), taskSubscriptionHandlerFactory(), fhirContext(), + propertiesConfig.getTaskSubscriptionSearchParameter(), propertiesConfig.getWebsocketRetrySleepMillis(), propertiesConfig.getWebsocketMaxRetries()); } + @Bean + public ResourceHandler questionnaireResponseHandler() + { + return new QuestionnaireResponseHandler(processEngine.getTaskService()); + } + + @Bean + public SubscriptionHandlerFactory questionnaireResponseSubscriptionHandlerFactory() + { + return new QuestionnaireResponseSubscriptionHandlerFactory(questionnaireResponseHandler(), + daoConfig.lastEventTimeDaoQuestionnaireResponse()); + } + + @Bean + public FhirConnector fhirConnectorQuestionnaireResponse() + { + return new FhirConnectorImpl<>("QuestionnaireResponse", clientProvider(), + questionnaireResponseSubscriptionHandlerFactory(), fhirContext(), + propertiesConfig.getQuestionnaireResponseSubscriptionSearchParameter(), + propertiesConfig.getWebsocketRetrySleepMillis(), propertiesConfig.getWebsocketMaxRetries()); + } + @EventListener({ ContextRefreshedEvent.class }) public void onContextRefreshedEvent(ContextRefreshedEvent event) { - fhirConnector().connect(); + fhirConnectorTask().connect(); + fhirConnectorQuestionnaireResponse().connect(); } @Bean @@ -212,6 +246,12 @@ public GroupHelper groupHelper() return new GroupHelperImpl(); } + @Bean + public QuestionnaireResponseHelper questionnaireResponseHelper() + { + return new QuestionnaireResponseHelperImpl(); + } + @Bean public ReadAccessHelper readAccessHelper() { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/MailConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/MailConfig.java new file mode 100644 index 000000000..a591202f5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/MailConfig.java @@ -0,0 +1,232 @@ +package org.highmed.dsf.bpe.spring.config; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.UUID; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Filter.Result; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.filter.ThresholdFilter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.pkcs.PKCSException; +import org.highmed.dsf.bpe.service.LoggingMailService; +import org.highmed.dsf.bpe.service.MailService; +import org.highmed.dsf.bpe.service.SmtpMailService; +import org.highmed.dsf.tools.build.BuildInfoReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; + +import de.rwh.utils.crypto.CertificateHelper; +import de.rwh.utils.crypto.io.CertificateReader; +import de.rwh.utils.crypto.io.PemIo; + +@Configuration +public class MailConfig implements InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(MailConfig.class); + + private static final BouncyCastleProvider provider = new BouncyCastleProvider(); + + @Autowired + private PropertiesConfig propertiesConfig; + + @Autowired + BuildInfoReaderConfig buildInfoReaderConfig; + + @Bean + public MailService mailService() + { + if (isConfigured()) + { + try + { + return newSmptMailService(); + } + catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException | PKCSException e) + { + throw new RuntimeException(e); + } + } + else + return new LoggingMailService(); + } + + private boolean isConfigured() + { + return propertiesConfig.getMailServerHostname() != null && propertiesConfig.getMailServerPort() > 0; + } + + private MailService newSmptMailService() + throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, PKCSException + { + String fromAddress = propertiesConfig.getMailFromAddress(); + List toAddresses = propertiesConfig.getMailToAddresses(); + List toAddressesCc = propertiesConfig.getMailToAddressesCc(); + List replyToAddresses = propertiesConfig.getMailReplyToAddresses(); + + boolean useSmtps = propertiesConfig.getMailUseSmtps(); + + String mailServerHostname = propertiesConfig.getMailServerHostname(); + int mailServerPort = propertiesConfig.getMailServerPort(); + + String mailServerUsername = propertiesConfig.getMailServerUsername(); + char[] mailServerPassword = propertiesConfig.getMailServerPassword(); + + KeyStore trustStore = toTrustStore(propertiesConfig.getMailServerTrustStoreFile()); + char[] keyStorePassword = UUID.randomUUID().toString().toCharArray(); + KeyStore keyStore = toKeyStore(propertiesConfig.getMailServerClientCertificateFile(), + propertiesConfig.getMailServerClientCertificatePrivateKeyFile(), + propertiesConfig.getMailServerClientCertificatePrivateKeyFilePassword(), keyStorePassword); + + KeyStore signStore = toSmimeSigningStore(propertiesConfig.getMailSmimeSigingKeyStoreFile(), + propertiesConfig.getMailSmimeSigingKeyStorePassword()); + + return new SmtpMailService(fromAddress, toAddresses, toAddressesCc, replyToAddresses, useSmtps, + mailServerHostname, mailServerPort, mailServerUsername, mailServerPassword, trustStore, keyStore, + keyStorePassword, signStore, propertiesConfig.getMailSmimeSigingKeyStorePassword(), + propertiesConfig.getSendMailOnErrorLogEvent(), propertiesConfig.getMailOnErrorLogEventBufferSize(), + propertiesConfig.getMailOnErrorLogEventDebugLogLocation()); + } + + private KeyStore toTrustStore(String trustStoreFile) + throws IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException + { + if (trustStoreFile == null) + return null; + + Path trustStorePath = Paths.get(trustStoreFile); + + if (!Files.isReadable(trustStorePath)) + throw new IOException("Mail server trust store file '" + trustStorePath.toString() + "' not readable"); + + return CertificateReader.allFromCer(trustStorePath); + } + + private KeyStore toKeyStore(String certificateFile, String privateKeyFile, char[] privateKeyPassword, + char[] keyStorePassword) + throws IOException, CertificateException, PKCSException, KeyStoreException, NoSuchAlgorithmException + { + if (certificateFile == null && privateKeyFile == null) + return null; + + Path certificatePath = Paths.get(certificateFile); + Path privateKeyPath = Paths.get(privateKeyFile); + + if (!Files.isReadable(certificatePath)) + throw new IOException( + "Mail server client certificate file '" + certificatePath.toString() + "' not readable"); + if (!Files.isReadable(certificatePath)) + throw new IOException( + "Mail server client certificate private key file '" + privateKeyPath.toString() + "' not readable"); + + X509Certificate certificate = PemIo.readX509CertificateFromPem(certificatePath); + PrivateKey privateKey = PemIo.readPrivateKeyFromPem(provider, privateKeyPath, privateKeyPassword); + + String subjectCommonName = CertificateHelper.getSubjectCommonName(certificate); + return CertificateHelper.toJksKeyStore(privateKey, new Certificate[] { certificate }, subjectCommonName, + keyStorePassword); + } + + private KeyStore toSmimeSigningStore(String mailSmimeSigingKeyStoreFile, char[] mailSmimeSigingKeyStorePassword) + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException + { + if (mailSmimeSigingKeyStoreFile == null) + return null; + + Path keyStorePath = Paths.get(mailSmimeSigingKeyStoreFile); + + if (!Files.isReadable(keyStorePath)) + throw new IOException( + "S/MIME mail signing certificate file '" + keyStorePath.toString() + "' not readable"); + + return CertificateReader.fromPkcs12(keyStorePath, mailSmimeSigingKeyStorePassword); + } + + @Override + public void afterPropertiesSet() throws Exception + { + if (isConfigured()) + { + logger.info( + "Mail client config: {fromAddress: {}, toAddresses: {}, toAddressesCc: {}, replyToAddresses: {}," + + " useSmtps: {}, mailServerHostname: {}, mailServerPort: {}, mailServerUsername: {}," + + " mailServerPassword: {}, trustStore: {}, clientCertificate: {}, clientCertificatePrivateKey: {}," + + " clientCertificatePrivateKeyPassword: {}, smimeSigingKeyStore: {}, smimeSigingKeyStorePassword: {}," + + " sendTestMailOnStartup: {}, sendMailOnErrorLogEvent: {}, mailOnErrorLogEventBufferSize: {}," + + " mailOnErrorLogEventDebugLogLocation: {}}", + propertiesConfig.getMailFromAddress(), propertiesConfig.getMailToAddresses(), + propertiesConfig.getMailToAddressesCc(), propertiesConfig.getMailReplyToAddresses(), + propertiesConfig.getMailUseSmtps(), propertiesConfig.getMailServerHostname(), + propertiesConfig.getMailServerPort(), propertiesConfig.getMailServerUsername(), + propertiesConfig.getMailServerPassword() != null ? "***" : "null", + propertiesConfig.getMailServerTrustStoreFile(), + propertiesConfig.getMailServerClientCertificateFile(), + propertiesConfig.getMailServerClientCertificatePrivateKeyFile(), + propertiesConfig.getMailServerClientCertificatePrivateKeyFilePassword() != null ? "***" : "null", + propertiesConfig.getMailSmimeSigingKeyStoreFile(), + propertiesConfig.getMailSmimeSigingKeyStorePassword() != null ? "***" : "null", + propertiesConfig.getSendTestMailOnStartup(), propertiesConfig.getSendMailOnErrorLogEvent(), + propertiesConfig.getMailOnErrorLogEventBufferSize(), + propertiesConfig.getMailOnErrorLogEventDebugLogLocation()); + } + else + { + logger.info( + "Mail client config: SMTP client not configured, sending mails to debug log, configure at least SMTP server host and port"); + } + + if (isConfigured()) + { + Appender appender = ((SmtpMailService) mailService()).getLog4jAppender(); + if (appender != null) + { + appender.start(); + + LoggerContext context = (LoggerContext) LogManager.getContext(false); + context.getConfiguration().getRootLogger().addAppender(appender, Level.INFO, + ThresholdFilter.createFilter(Level.INFO, Result.ACCEPT, Result.DENY)); + } + } + } + + @EventListener({ ContextRefreshedEvent.class }) + public void onContextRefreshedEvent(ContextRefreshedEvent event) throws IOException + { + if (propertiesConfig.getSendTestMailOnStartup()) + { + DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + BuildInfoReader buildInfoReader = buildInfoReaderConfig.buildInfoReader(); + mailService().send("DSF BPE Test Mail", + "BPE startup test mail\n\nArtifact: " + buildInfoReader.getProjectArtifact() + "\nVersion: " + + buildInfoReader.getProjectVersion() + "\nBuild: " + + buildInfoReader.getBuildDate().withZoneSameInstant(ZoneId.systemDefault()) + .format(formatter) + + "\nBranch: " + buildInfoReader.getBuildBranch() + "\nCommit: " + + buildInfoReader.getBuildNumber() + "\n\nSend on " + + ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()).format(formatter)); + } + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/PropertiesConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/PropertiesConfig.java index 8152a79de..c3c19baed 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/PropertiesConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/spring/config/PropertiesConfig.java @@ -122,13 +122,13 @@ public class PropertiesConfig @Value("${org.highmed.dsf.bpe.fhir.client.local.websocket.proxy.password:#{null}}") private char[] websocketClientProxyPassword; - @Documentation(description = "Subscription to receive notifications about resources from the DSF FHIR server", recommendation = "Change only if you need other subscriptions then the default Task subscription of the DSF") + @Documentation(description = "Subscription to receive notifications about task resources from the DSF FHIR server") @Value("${org.highmed.dsf.bpe.fhir.task.subscription.search.parameter:?criteria=Task%3Fstatus%3Drequested&status=active&type=websocket&payload=application/fhir%2Bjson}") - private String subscriptionSearchParameter; + private String taskSubscriptionSearchParameter; - @Documentation(description = "File storing the last event received and processed on the DFS BPE server from the DSF FHIR server, used to load events that occurred on the DSF FHIR server while the DSF BPE server was turned off") - @Value("${org.highmed.dsf.bpe.fhir.task.subscription.last.event.time:last_event/time.file}") - private String lastEventTimeFile; + @Documentation(description = "Subscription to receive notifications about questionnaire response resources from the DSF FHIR server") + @Value("${org.highmed.dsf.bpe.fhir.questionnaire.response.subscription.search.parameter:?criteria=QuestionnaireResponse%3Fstatus%3Dcompleted&status=active&type=websocket&payload=application/fhir%2Bjson}") + private String questionnaireResponseSubscriptionSearchParameter; @Documentation(description = "Number of retries until a websocket connection can be established with the DSF FHIR server, `-1` means infinite number of retries") @Value("${org.highmed.dsf.bpe.fhir.task.subscription.retry.max:-1}") @@ -174,6 +174,82 @@ public class PropertiesConfig @Value("${org.highmed.dsf.bpe.process.fhir.server.retry.sleep:5000}") private long fhirServerRetryDelayMillis; + @Documentation(description = "Mail service sender address", example = "sender@localhost") + @Value("${org.highmed.dsf.bpe.mail.fromAddress:}") + private String mailFromAddress; + + @Documentation(description = "Mail service recipient addresses, configure at least one; comma or space separated list, YAML block scalars supported", example = "recipient@localhost") + @Value("#{'${org.highmed.dsf.bpe.mail.toAddresses:}'.trim().split('(,[ ]?)|(\\n)')}") + private List mailToAddresses; + + @Documentation(description = "Mail service CC recipient addresses; comma or space separated list, YAML block scalars supported", example = "cc.recipient@localhost") + @Value("#{'${org.highmed.dsf.bpe.mail.toAddressesCc:}'.trim().split('(,[ ]?)|(\\n)')}") + private List mailToAddressesCc; + + @Documentation(description = "Mail service reply to addresses; comma or space separated list, YAML block scalars supported", example = "reply.to@localhost") + @Value("#{'${org.highmed.dsf.bpe.mail.replyToAddresses:}'.trim().split('(,[ ]?)|(\\n)')}") + private List mailReplyToAddresses; + + @Documentation(description = "To enable SMTP over TLS (smtps), set to `true`") + @Value("${org.highmed.dsf.bpe.mail.useSmtps:false}") + private boolean mailUseSmtps; + + @Documentation(description = "SMTP server hostname", example = "smtp.server.de") + @Value("${org.highmed.dsf.bpe.mail.host:#{null}}") + private String mailServerHostname; + + @Documentation(description = "SMTP server port", example = "465") + @Value("${org.highmed.dsf.bpe.mail.port:0}") + private int mailServerPort; + + @Documentation(description = "SMTP server authentication username", recommendation = "Configure if the SMTP server reqiures username/password authentication; enable SMTP over TLS via *ORG_HIGHMED_DSF_BPE_MAIL_USESMTPS*") + @Value("${org.highmed.dsf.bpe.mail.username:#{null}}") + private String mailServerUsername; + + @Documentation(description = "SMTP server authentication password", recommendation = "Configure if the SMTP server reqiures username/password authentication; use docker secret file to configure using *${env_variable}_FILE*; enable SMTP over TLS via *ORG_HIGHMED_DSF_BPE_MAIL_USESMTPS*") + @Value("${org.highmed.dsf.bpe.mail.password:#{null}}") + private char[] mailServerPassword; + + @Documentation(description = "PEM encoded file with one or more trusted root certificates to validate the server certificate of the SMTP server. Requires SMTP over TLS to be enabled via *ORG_HIGHMED_DSF_BPE_MAIL_USESMTPS*", recommendation = "Use docker secret file to configure", example = "/run/secrets/smtp_server_trust_certificates.pem") + @Value("${org.highmed.dsf.bpe.mail.trust.certificates:#{null}}") + private String mailServerTrustStoreFile; + + @Documentation(description = "PEM encoded file with client certificate used to authenticate against the SMTP server. Requires SMTP over TLS to be enabled via *ORG_HIGHMED_DSF_BPE_MAIL_USESMTPS*", recommendation = "Use docker secret file to configure", example = "/run/secrets/smtp_server_client_certificate.pem") + @Value("${org.highmed.dsf.bpe.mail.client.certificate:#{null}}") + private String mailServerClientCertificateFile; + + @Documentation(description = "Private key corresponging to the SMTP server client certificate as PEM encoded file. Use ${env_variable}_PASSWORD* or *${env_variable}_PASSWORD_FILE* if private key is encrypted. Requires SMTP over TLS to be enabled via *ORG_HIGHMED_DSF_BPE_MAIL_USESMTPS*", recommendation = "Use docker secret file to configure", example = "/run/secrets/smtp_server_client_certificate_private_key.pem") + @Value("${org.highmed.dsf.bpe.mail.client.certificate.private.key:#{null}}") + private String mailServerClientCertificatePrivateKeyFile; + + @Documentation(description = "Password to decrypt the local client certificate encrypted private key", recommendation = "Use docker secret file to configure using *${env_variable}_FILE*", example = "/run/secrets/smtp_server_client_certificate_private_key.pem.password") + @Value("${org.highmed.dsf.bpe.mail.client.certificate.private.key.password:#{null}}") + private char[] mailServerClientCertificatePrivateKeyFilePassword; + + @Documentation(description = "PKCS12 encoded file with S/MIME certificate, private key and certificate chain to enable send mails to be S/MIME signed", recommendation = "Use docker secret file to configure", example = "/run/secrets/smime_certificate.p12") + @Value("${org.highmed.dsf.bpe.mail.smime.p12Keystore:#{null}}") + private String mailSmimeSigingKeyStoreFile; + + @Documentation(description = "Password to decrypt the PKCS12 encoded S/MIMIE certificate file", recommendation = "Use docker secret file to configure using *${env_variable}_FILE*", example = "/run/secrets/smime_certificate.p12.password") + @Value("${org.highmed.dsf.bpe.mail.smime.p12Keystore.password:#{null}}") + private char[] mailSmimeSigingKeyStorePassword; + + @Documentation(description = "To enable a test mail being send on startup of the BPE, set to `true`. Requires SMTP server to be configured.") + @Value("${org.highmed.dsf.bpe.mail.sendTestMailOnStartup:false}") + private boolean sendTestMailOnStartup; + + @Documentation(description = "To enable mails being send for every ERROR logged, set to `true`. Requires SMTP server to be configured.") + @Value("${org.highmed.dsf.bpe.mail.sendMailOnErrorLogEvent:false}") + private boolean sendMailOnErrorLogEvent; + + @Documentation(description = "Number of previous INFO, WARN log messages to include in ERROR log event mails (>=0). Requires send mail on ERROR log event option to be enabled to have an effect.") + @Value("${org.highmed.dsf.bpe.mail.mailOnErrorLogEventBufferSize:4}") + private int mailOnErrorLogEventBufferSize; + + @Documentation(description = "Location of the BPE debug log as displayed in the footer of ERROR log event mails, does not modify the actual location of the debug log file. Requires send mail on ERROR log event option to be enabled to have an effect.") + @Value("${org.highmed.dsf.bpe.mail.mailOnErrorLogEventDebugLogLocation:/opt/bpe/log/bpe.log}") + private String mailOnErrorLogEventDebugLogLocation; + @Bean // static in order to initialize before @Configuration classes public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer( ConfigurableEnvironment environment) @@ -313,14 +389,14 @@ public char[] getWebsocketClientProxyPassword() return websocketClientProxyPassword; } - public String getSubscriptionSearchParameter() + public String getTaskSubscriptionSearchParameter() { - return subscriptionSearchParameter; + return taskSubscriptionSearchParameter; } - public Path getLastEventTimeFile() + public String getQuestionnaireResponseSubscriptionSearchParameter() { - return Paths.get(lastEventTimeFile); + return questionnaireResponseSubscriptionSearchParameter; } public long getWebsocketRetrySleepMillis() @@ -377,4 +453,99 @@ public long getFhirServerRetryDelayMillis() { return fhirServerRetryDelayMillis; } + + public String getMailFromAddress() + { + return mailFromAddress; + } + + public List getMailToAddresses() + { + return mailToAddresses; + } + + public List getMailToAddressesCc() + { + return mailToAddressesCc; + } + + public List getMailReplyToAddresses() + { + return mailReplyToAddresses; + } + + public boolean getMailUseSmtps() + { + return mailUseSmtps; + } + + public String getMailServerHostname() + { + return mailServerHostname; + } + + public int getMailServerPort() + { + return mailServerPort; + } + + public String getMailServerUsername() + { + return mailServerUsername; + } + + public char[] getMailServerPassword() + { + return mailServerPassword; + } + + public String getMailServerTrustStoreFile() + { + return mailServerTrustStoreFile; + } + + public String getMailServerClientCertificateFile() + { + return mailServerClientCertificateFile; + } + + public String getMailServerClientCertificatePrivateKeyFile() + { + return mailServerClientCertificatePrivateKeyFile; + } + + public char[] getMailServerClientCertificatePrivateKeyFilePassword() + { + return mailServerClientCertificatePrivateKeyFilePassword; + } + + public String getMailSmimeSigingKeyStoreFile() + { + return mailSmimeSigingKeyStoreFile; + } + + public char[] getMailSmimeSigingKeyStorePassword() + { + return mailSmimeSigingKeyStorePassword; + } + + public boolean getSendTestMailOnStartup() + { + return sendTestMailOnStartup; + } + + public boolean getSendMailOnErrorLogEvent() + { + return sendMailOnErrorLogEvent; + } + + public int getMailOnErrorLogEventBufferSize() + { + return mailOnErrorLogEventBufferSize; + } + + public String getMailOnErrorLogEventDebugLogLocation() + { + return mailOnErrorLogEventDebugLogLocation; + } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseHandler.java new file mode 100644 index 000000000..2cfd534fd --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseHandler.java @@ -0,0 +1,93 @@ +package org.highmed.dsf.fhir.questionnaire; + +import static org.highmed.dsf.bpe.ConstantsBase.BPMN_EXECUTION_VARIABLE_QUESTIONNAIRE_RESPONSE_COMPLETED; +import static org.highmed.dsf.bpe.ConstantsBase.CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY; +import static org.highmed.dsf.bpe.ConstantsBase.CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.camunda.bpm.engine.TaskService; +import org.highmed.dsf.fhir.variables.FhirResourceValues; +import org.highmed.dsf.fhir.websocket.ResourceHandler; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.StringType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +public class QuestionnaireResponseHandler implements ResourceHandler, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(QuestionnaireResponseHandler.class); + + private final TaskService userTaskService; + + public QuestionnaireResponseHandler(TaskService userTaskService) + { + this.userTaskService = userTaskService; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(userTaskService, "taskService"); + } + + public void onResource(QuestionnaireResponse questionnaireResponse) + { + try + { + List items = questionnaireResponse.getItem(); + + String questionnaireResponseId = questionnaireResponse.getId(); + String questionnaire = questionnaireResponse.getQuestionnaire(); + String user = questionnaireResponse.getAuthor().getIdentifier().getValue(); + String userType = questionnaireResponse.getAuthor().getType(); + String businessKey = getStringValueFromItems(items, CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY, + questionnaireResponseId) + .orElseThrow(() -> new RuntimeException( + "Missing linkId " + CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY)); + String taskId = getStringValueFromItems(items, CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID, + questionnaireResponseId) + .orElseThrow(() -> new RuntimeException( + "Missing linkId " + CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID)); + + logger.info("User task '{}' for Questionnaire '{}' completed [userTaskId: {}, businessKey: {}, user: {}]", + questionnaireResponseId, questionnaire, taskId, businessKey, user + "|" + userType); + + Map variables = Map.of(BPMN_EXECUTION_VARIABLE_QUESTIONNAIRE_RESPONSE_COMPLETED, + FhirResourceValues.create(questionnaireResponse)); + userTaskService.complete(taskId, variables); + } + catch (Exception exception) + { + // TODO handle exception + throw new RuntimeException(exception); + } + } + + private Optional getStringValueFromItems( + List items, String linkId, + String questionnaireResponseId) + { + List answers = items.stream().filter(i -> linkId.equals(i.getLinkId())) + .flatMap(i -> i.getAnswer().stream()).filter(a -> a.getValue() instanceof StringType) + .map(a -> ((StringType) a.getValue()).getValue()).collect(Collectors.toList()); + + if (answers.size() == 0) + { + logger.warn("QuestionnaireResponse with id '{}' did not contain any linkId '{}'", questionnaireResponseId, + linkId); + return Optional.empty(); + } + + if (answers.size() > 1) + logger.warn("QuestionnaireResponse with id '{}' contained {} linkIds '{}', using the first", + questionnaireResponseId, answers.size(), linkId); + + return Optional.of(answers.get(0)); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseHelperImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseHelperImpl.java new file mode 100644 index 000000000..5ef5b2c49 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseHelperImpl.java @@ -0,0 +1,89 @@ +package org.highmed.dsf.fhir.questionnaire; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.TimeType; +import org.hl7.fhir.r4.model.Type; +import org.hl7.fhir.r4.model.UriType; + +public class QuestionnaireResponseHelperImpl implements QuestionnaireResponseHelper +{ + @Override + public Stream getItemLeavesMatchingLinkIdAsStream( + QuestionnaireResponse questionnaireResponse, String linkId) + { + return getItemLeavesAsStream(questionnaireResponse).filter(i -> linkId.equals(i.getLinkId())); + } + + @Override + public Stream getItemLeavesAsStream( + QuestionnaireResponse questionnaireResponse) + { + return flatItems(questionnaireResponse.getItem()); + } + + private Stream flatItems( + List toFlat) + { + return toFlat.stream().flatMap(this::leaves); + } + + private Stream leaves( + QuestionnaireResponse.QuestionnaireResponseItemComponent component) + { + if (component.getItem().size() > 0) + return component.getItem().stream().flatMap(this::leaves); + else + return Stream.of(component); + } + + @Override + public void addItemLeave(QuestionnaireResponse questionnaireResponse, String linkId, String text, Type answer) + { + List answerComponent = Collections + .singletonList(new QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(answer)); + + questionnaireResponse.addItem().setLinkId(linkId).setText(text).setAnswer(answerComponent); + } + + @Override + public Type transformQuestionTypeToAnswerType(Questionnaire.QuestionnaireItemComponent question) + { + switch (question.getType()) + { + case STRING: + case TEXT: + return new StringType("Placeholder.."); + case INTEGER: + return new IntegerType(0); + case DECIMAL: + return new DecimalType(0.00); + case BOOLEAN: + return new BooleanType(false); + case DATE: + return new DateType("1900-01-01"); + case TIME: + return new TimeType("00:00:00"); + case DATETIME: + return new DateTimeType("1900-01-01T00:00:00.000Z"); + case URL: + return new UriType("http://example.org/foo"); + case REFERENCE: + return new Reference("http://example.org/fhir/Placeholder/id"); + default: + throw new RuntimeException( + "Type '" + question.getType().getDisplay() + "' in Questionnaire.item is not supported"); + } + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseSubscriptionHandlerFactory.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseSubscriptionHandlerFactory.java new file mode 100644 index 000000000..0bd899081 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseSubscriptionHandlerFactory.java @@ -0,0 +1,56 @@ +package org.highmed.dsf.fhir.questionnaire; + +import java.util.Objects; + +import org.highmed.dsf.bpe.dao.LastEventTimeDao; +import org.highmed.dsf.fhir.subscription.EventResourceHandler; +import org.highmed.dsf.fhir.subscription.EventResourceHandlerImpl; +import org.highmed.dsf.fhir.subscription.ExistingResourceLoader; +import org.highmed.dsf.fhir.subscription.ExistingResourceLoaderImpl; +import org.highmed.dsf.fhir.subscription.PingEventResourceHandler; +import org.highmed.dsf.fhir.subscription.SubscriptionHandlerFactory; +import org.highmed.dsf.fhir.websocket.ResourceHandler; +import org.highmed.fhir.client.FhirWebserviceClient; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.springframework.beans.factory.InitializingBean; + +public class QuestionnaireResponseSubscriptionHandlerFactory + implements SubscriptionHandlerFactory, InitializingBean +{ + private final ResourceHandler resourceHandler; + private final LastEventTimeDao lastEventTimeDao; + + public QuestionnaireResponseSubscriptionHandlerFactory(ResourceHandler resourceHandler, + LastEventTimeDao lastEventTimeDao) + { + this.resourceHandler = resourceHandler; + this.lastEventTimeDao = lastEventTimeDao; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(resourceHandler, "resourceHandler"); + Objects.requireNonNull(lastEventTimeDao, "lastEventTimeDao"); + } + + @Override + public ExistingResourceLoader createExistingResourceLoader(FhirWebserviceClient client) + { + return new ExistingResourceLoaderImpl<>(lastEventTimeDao, resourceHandler, client, "QuestionnaireResponse", + QuestionnaireResponse.class); + } + + @Override + public EventResourceHandler createEventResourceHandler() + { + return new EventResourceHandlerImpl<>(lastEventTimeDao, resourceHandler, QuestionnaireResponse.class); + } + + @Override + public PingEventResourceHandler createPingEventResourceHandler( + ExistingResourceLoader existingResourceLoader) + { + return new PingEventResourceHandler<>(existingResourceLoader); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/EventResourceHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/EventResourceHandler.java new file mode 100644 index 000000000..afac8c670 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/EventResourceHandler.java @@ -0,0 +1,8 @@ +package org.highmed.dsf.fhir.subscription; + +import org.hl7.fhir.r4.model.Resource; + +public interface EventResourceHandler +{ + void onResource(Resource resource); +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/EventResourceHandlerImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/EventResourceHandlerImpl.java new file mode 100755 index 000000000..7dbc57cbc --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/EventResourceHandlerImpl.java @@ -0,0 +1,59 @@ +package org.highmed.dsf.fhir.subscription; + +import java.sql.SQLException; +import java.util.Date; + +import org.highmed.dsf.bpe.dao.LastEventTimeDao; +import org.highmed.dsf.fhir.websocket.ResourceHandler; +import org.hl7.fhir.r4.model.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ca.uhn.fhir.model.api.annotation.ResourceDef; + +public class EventResourceHandlerImpl implements EventResourceHandler +{ + private static final Logger logger = LoggerFactory.getLogger(EventResourceHandlerImpl.class); + + private final LastEventTimeDao lastEventTimeDao; + private final ResourceHandler handler; + private final Class resourceClass; + + public EventResourceHandlerImpl(LastEventTimeDao lastEventTimeDao, ResourceHandler handler, + Class resourceClass) + { + this.lastEventTimeDao = lastEventTimeDao; + this.handler = handler; + this.resourceClass = resourceClass; + } + + public void onResource(Resource resource) + { + logger.trace("Resource of type {} received", resource.getClass().getAnnotation(ResourceDef.class).name()); + + if (resourceClass.isInstance(resource)) + { + @SuppressWarnings("unchecked") + R cast = (R) resource; + handler.onResource(cast); + writeLastEventTime(cast.getMeta().getLastUpdated()); + } + else + { + logger.warn("Ignoring resource of type {}", resource.getClass().getAnnotation(ResourceDef.class).name()); + } + } + + private void writeLastEventTime(Date lastUpdated) + { + try + { + lastEventTimeDao.writeLastEventTime(lastUpdated); + } + catch (SQLException e) + { + logger.warn("Unable to write last event time to db: {} - {}", e.getClass().getName(), e.getMessage()); + throw new RuntimeException(e); + } + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/ExistingResourceLoader.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/ExistingResourceLoader.java new file mode 100644 index 000000000..ec5d80aea --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/ExistingResourceLoader.java @@ -0,0 +1,11 @@ +package org.highmed.dsf.fhir.subscription; + +import java.util.List; +import java.util.Map; + +import org.hl7.fhir.r4.model.Resource; + +public interface ExistingResourceLoader +{ + void readExistingResources(Map> searchCriteriaQueryParameters); +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/ExistingResourceLoaderImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/ExistingResourceLoaderImpl.java new file mode 100644 index 000000000..45c3cbe01 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/ExistingResourceLoaderImpl.java @@ -0,0 +1,134 @@ +package org.highmed.dsf.fhir.subscription; + +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.ws.rs.core.UriBuilder; + +import org.highmed.dsf.bpe.dao.LastEventTimeDao; +import org.highmed.dsf.fhir.websocket.ResourceHandler; +import org.highmed.fhir.client.FhirWebserviceClient; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ca.uhn.fhir.model.api.annotation.ResourceDef; + +public class ExistingResourceLoaderImpl implements ExistingResourceLoader +{ + private static final Logger logger = LoggerFactory.getLogger(ExistingResourceLoaderImpl.class); + + private static final String PARAM_LAST_UPDATED = "_lastUpdated"; + private static final String PARAM_COUNT = "_count"; + private static final String PARAM_PAGE = "_page"; + private static final String PARAM_SORT = "_sort"; + private static final int RESULT_PAGE_COUNT = 20; + + private final LastEventTimeDao lastEventTimeDao; + private final FhirWebserviceClient webserviceClient; + private final ResourceHandler handler; + private final String resourceName; + private final Class resourceClass; + + public ExistingResourceLoaderImpl(LastEventTimeDao lastEventTimeDao, ResourceHandler handler, + FhirWebserviceClient webserviceClient, String resourceName, Class resourceClass) + { + this.lastEventTimeDao = lastEventTimeDao; + this.handler = handler; + this.webserviceClient = webserviceClient; + this.resourceName = resourceName; + this.resourceClass = resourceClass; + } + + public void readExistingResources(Map> searchCriteriaQueryParameters) + { + // executing search until call results in no more found tasks + while (doReadExistingResources(searchCriteriaQueryParameters)) + ; + } + + private boolean doReadExistingResources(Map> searchCriteriaQueryParameters) + { + Map> queryParams = new HashMap<>(searchCriteriaQueryParameters); + Optional readLastEventTime = readLastEventTime(); + + readLastEventTime.ifPresent(lastEventTime -> queryParams.put(PARAM_LAST_UPDATED, + Collections.singletonList("gt" + lastEventTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)))); + + queryParams.put(PARAM_COUNT, Collections.singletonList(String.valueOf(RESULT_PAGE_COUNT))); + queryParams.put(PARAM_PAGE, Collections.singletonList("1")); + queryParams.put(PARAM_SORT, Collections.singletonList(PARAM_LAST_UPDATED)); + + UriBuilder builder = UriBuilder.fromPath(resourceName); + queryParams.forEach((k, v) -> builder.replaceQueryParam(k, v.toArray())); + + logger.debug("Executing search {}", builder.toString()); + Bundle bundle = webserviceClient.searchWithStrictHandling(resourceClass, queryParams); + + if (bundle.getTotal() <= 0) + { + logger.debug("Result bundle.total <= 0"); + return false; + } + + for (BundleEntryComponent entry : bundle.getEntry()) + { + if (entry.hasResource()) + { + if (resourceClass.isInstance(entry.getResource())) + { + @SuppressWarnings("unchecked") + R resource = (R) entry.getResource(); + handler.onResource(resource); + writeLastEventTime(resource.getMeta().getLastUpdated()); + } + else + { + logger.warn("Ignoring resource of type {}", + entry.getResource().getClass().getAnnotation(ResourceDef.class).name()); + } + } + else + { + logger.warn("Bundle entry did not contain resource"); + } + } + + return true; + } + + private Optional readLastEventTime() + { + try + { + return lastEventTimeDao.readLastEventTime(); + } + catch (SQLException e) + { + logger.warn("Unable to read last event time from db: {} - {}", e.getClass().getName(), e.getMessage()); + throw new RuntimeException(e); + } + } + + private void writeLastEventTime(Date lastUpdated) + { + try + { + lastEventTimeDao.writeLastEventTime(lastUpdated); + } + catch (SQLException e) + { + logger.warn("Unable to write last event time to db: {} - {}", e.getClass().getName(), e.getMessage()); + throw new RuntimeException(e); + } + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/PingEventHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/PingEventResourceHandler.java similarity index 59% rename from dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/PingEventHandler.java rename to dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/PingEventResourceHandler.java index 391254912..ffc73a58c 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/PingEventHandler.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/subscription/PingEventResourceHandler.java @@ -1,20 +1,21 @@ -package org.highmed.dsf.fhir.websocket; +package org.highmed.dsf.fhir.subscription; import java.util.List; import java.util.Map; +import org.hl7.fhir.r4.model.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class PingEventHandler +public class PingEventResourceHandler { - private static final Logger logger = LoggerFactory.getLogger(PingEventHandler.class); + private static final Logger logger = LoggerFactory.getLogger(PingEventResourceHandler.class); - private final ExistingTaskLoader taskLoader; + private final ExistingResourceLoader loader; - public PingEventHandler(ExistingTaskLoader taskLoader) + public PingEventResourceHandler(ExistingResourceLoader loader) { - this.taskLoader = taskLoader; + this.loader = loader; } public void onPing(String ping, String subscriptionIdPart, Map> searchCriteriaQueryParameters) @@ -27,6 +28,6 @@ public void onPing(String ping, String subscriptionIdPart, Map +{ + ExistingResourceLoader createExistingResourceLoader(FhirWebserviceClient client); + + EventResourceHandler createEventResourceHandler(); + + PingEventResourceHandler createPingEventResourceHandler(ExistingResourceLoader existingResourceLoader); +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskHandler.java index c774f3363..4815e3ed4 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskHandler.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskHandler.java @@ -26,13 +26,14 @@ import org.camunda.bpm.engine.runtime.ProcessInstance; import org.camunda.bpm.engine.runtime.ProcessInstanceQuery; import org.highmed.dsf.fhir.variables.FhirResourceValues; +import org.highmed.dsf.fhir.websocket.ResourceHandler; import org.highmed.fhir.client.FhirWebserviceClient; import org.hl7.fhir.r4.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; -public class TaskHandler implements InitializingBean +public class TaskHandler implements ResourceHandler, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(TaskHandler.class); @@ -60,7 +61,7 @@ public void afterPropertiesSet() throws Exception Objects.requireNonNull(repositoryService, "repositoryService"); } - public void onTask(Task task) + public void onResource(Task task) { task.setStatus(Task.TaskStatus.INPROGRESS); task = webserviceClient.update(task); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskHelperImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskHelperImpl.java index b94c189e0..44c52f987 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskHelperImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskHelperImpl.java @@ -1,8 +1,14 @@ package org.highmed.dsf.fhir.task; +import static org.highmed.dsf.bpe.ConstantsBase.BPMN_EXECUTION_VARIABLE_LEADING_TASK; +import static org.highmed.dsf.bpe.ConstantsBase.BPMN_EXECUTION_VARIABLE_TASK; + +import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.highmed.dsf.fhir.variables.FhirResourceValues; import org.hl7.fhir.r4.model.Base64BinaryType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CodeableConcept; @@ -144,4 +150,59 @@ public TaskOutputComponent createOutput(String system, String code, Reference re { return new TaskOutputComponent(new CodeableConcept(new Coding(system, code, null)), reference); } + + @Override + public Task getTask(DelegateExecution execution) + { + if (execution == null) + throw new IllegalStateException("execution not started"); + + return execution.getParentId() == null || execution.getParentId().equals(execution.getProcessInstanceId()) + ? getLeadingTaskFromExecutionVariables(execution) + : getCurrentTaskFromExecutionVariables(execution); + } + + @Override + public Task getCurrentTaskFromExecutionVariables(DelegateExecution execution) + { + if (execution == null) + throw new IllegalStateException("execution not started"); + + return (Task) execution.getVariable(BPMN_EXECUTION_VARIABLE_TASK); + } + + @Override + public Task getLeadingTaskFromExecutionVariables(DelegateExecution execution) + { + if (execution == null) + throw new IllegalStateException("execution not started"); + + Task leadingTask = (Task) execution.getVariable(BPMN_EXECUTION_VARIABLE_LEADING_TASK); + return leadingTask != null ? leadingTask : getCurrentTaskFromExecutionVariables(execution); + } + + @Override + public void updateCurrentTaskInExecutionVariables(DelegateExecution execution, Task task) + { + if (execution == null) + throw new IllegalStateException("execution not started"); + + Objects.requireNonNull(task, "task"); + execution.setVariable(BPMN_EXECUTION_VARIABLE_TASK, FhirResourceValues.create(task)); + } + + @Override + public void updateLeadingTaskInExecutionVariables(DelegateExecution execution, Task task) + { + if (execution == null) + throw new IllegalStateException("execution not started"); + + Objects.requireNonNull(task, "task"); + Task leadingTask = (Task) execution.getVariable(BPMN_EXECUTION_VARIABLE_LEADING_TASK); + + if (leadingTask != null) + execution.setVariable(BPMN_EXECUTION_VARIABLE_LEADING_TASK, FhirResourceValues.create(task)); + else + updateCurrentTaskInExecutionVariables(execution, task); + } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskSubscriptionHandlerFactory.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskSubscriptionHandlerFactory.java new file mode 100644 index 000000000..d9415cb16 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/task/TaskSubscriptionHandlerFactory.java @@ -0,0 +1,53 @@ +package org.highmed.dsf.fhir.task; + +import java.util.Objects; + +import org.highmed.dsf.bpe.dao.LastEventTimeDao; +import org.highmed.dsf.fhir.subscription.EventResourceHandler; +import org.highmed.dsf.fhir.subscription.EventResourceHandlerImpl; +import org.highmed.dsf.fhir.subscription.ExistingResourceLoader; +import org.highmed.dsf.fhir.subscription.ExistingResourceLoaderImpl; +import org.highmed.dsf.fhir.subscription.PingEventResourceHandler; +import org.highmed.dsf.fhir.subscription.SubscriptionHandlerFactory; +import org.highmed.dsf.fhir.websocket.ResourceHandler; +import org.highmed.fhir.client.FhirWebserviceClient; +import org.hl7.fhir.r4.model.Task; +import org.springframework.beans.factory.InitializingBean; + +public class TaskSubscriptionHandlerFactory implements SubscriptionHandlerFactory, InitializingBean +{ + private final ResourceHandler resourceHandler; + private final LastEventTimeDao lastEventTimeDao; + + public TaskSubscriptionHandlerFactory(ResourceHandler resourceHandler, LastEventTimeDao lastEventTimeDao) + { + this.resourceHandler = resourceHandler; + this.lastEventTimeDao = lastEventTimeDao; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(resourceHandler, "resourceHandler"); + Objects.requireNonNull(lastEventTimeDao, "lastEventTimeDao"); + } + + @Override + public ExistingResourceLoader createExistingResourceLoader(FhirWebserviceClient client) + { + return new ExistingResourceLoaderImpl<>(lastEventTimeDao, resourceHandler, client, "Task", Task.class); + } + + @Override + public EventResourceHandler createEventResourceHandler() + { + return new EventResourceHandlerImpl<>(lastEventTimeDao, resourceHandler, Task.class); + } + + @Override + public PingEventResourceHandler createPingEventResourceHandler( + ExistingResourceLoader existingResourceLoader) + { + return new PingEventResourceHandler<>(existingResourceLoader); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/ExistingTaskLoader.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/ExistingTaskLoader.java deleted file mode 100644 index 2a5b5eb70..000000000 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/ExistingTaskLoader.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.highmed.dsf.fhir.websocket; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import javax.ws.rs.core.UriBuilder; - -import org.highmed.dsf.fhir.task.TaskHandler; -import org.highmed.fhir.client.FhirWebserviceClient; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.Task; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ExistingTaskLoader -{ - private static final Logger logger = LoggerFactory.getLogger(ExistingTaskLoader.class); - - private static final String PARAM_LAST_UPDATED = "_lastUpdated"; - private static final String PARAM_COUNT = "_count"; - private static final String PARAM_PAGE = "_page"; - private static final String PARAM_SORT = "_sort"; - private static final int RESULT_PAGE_COUNT = 20; - - private final LastEventTimeIo lastEventTimeIo; - private final FhirWebserviceClient webserviceClient; - private final TaskHandler taskHandler; - - public ExistingTaskLoader(LastEventTimeIo lastEventTimeIo, TaskHandler taskHandler, - FhirWebserviceClient webserviceClient) - { - this.lastEventTimeIo = lastEventTimeIo; - this.taskHandler = taskHandler; - this.webserviceClient = webserviceClient; - } - - public void readExistingTasks(Map> searchCriteriaQueryParameters) - { - // executing search until call results in no more found tasks - while (doReadExistingTasks(searchCriteriaQueryParameters)) - ; - } - - private boolean doReadExistingTasks(Map> searchCriteriaQueryParameters) - { - Map> queryParams = new HashMap<>(searchCriteriaQueryParameters); - Optional readLastEventTime = lastEventTimeIo.readLastEventTime(); - - readLastEventTime.ifPresent(lastEventTime -> queryParams.put(PARAM_LAST_UPDATED, - Collections.singletonList("gt" + lastEventTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)))); - - queryParams.put(PARAM_COUNT, Collections.singletonList(String.valueOf(RESULT_PAGE_COUNT))); - queryParams.put(PARAM_PAGE, Collections.singletonList("1")); - queryParams.put(PARAM_SORT, Collections.singletonList(PARAM_LAST_UPDATED)); - - UriBuilder builder = UriBuilder.fromPath("Task"); - queryParams.forEach((k, v) -> builder.replaceQueryParam(k, v.toArray())); - - logger.debug("Executing search {}", builder.toString()); - Bundle bundle = webserviceClient.searchWithStrictHandling(Task.class, queryParams); - - if (bundle.getTotal() <= 0) - { - logger.debug("Result bundle.total <= 0"); - return false; - } - - for (BundleEntryComponent entry : bundle.getEntry()) - { - if (entry.getResource() instanceof Task) - { - Task task = (Task) entry.getResource(); - taskHandler.onTask(task); - lastEventTimeIo.writeLastEventTime(task.getAuthoredOn()); - } - else - logger.warn("Ignoring resource of type {}"); - } - - return true; - } -} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/FhirConnectorImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/FhirConnectorImpl.java old mode 100755 new mode 100644 index f892ef168..dc017493d --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/FhirConnectorImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/FhirConnectorImpl.java @@ -8,11 +8,14 @@ import java.util.function.Supplier; import org.highmed.dsf.fhir.client.FhirWebsocketClientProvider; -import org.highmed.dsf.fhir.task.TaskHandler; +import org.highmed.dsf.fhir.subscription.EventResourceHandler; +import org.highmed.dsf.fhir.subscription.ExistingResourceLoader; +import org.highmed.dsf.fhir.subscription.PingEventResourceHandler; +import org.highmed.dsf.fhir.subscription.SubscriptionHandlerFactory; import org.highmed.fhir.client.FhirWebserviceClient; import org.highmed.fhir.client.WebsocketClient; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,26 +29,25 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.Constants; -public class FhirConnectorImpl implements InitializingBean, FhirConnector +public class FhirConnectorImpl implements FhirConnector, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(FhirConnectorImpl.class); + private final String resourcePath; private final FhirWebsocketClientProvider clientProvider; - private final TaskHandler taskHandler; - private final LastEventTimeIo lastEventTimeIo; private final FhirContext fhirContext; - + private final SubscriptionHandlerFactory subscriptionHandlerFactory; private final long retrySleepMillis; private final int maxRetries; private final Map> subscriptionSearchParameter; - public FhirConnectorImpl(FhirWebsocketClientProvider clientProvider, TaskHandler taskHandler, - LastEventTimeIo lastEventTimeIo, FhirContext fhirContext, String subscriptionSearchParameter, - long retrySleepMillis, int maxRetries) + public FhirConnectorImpl(String resourcePath, FhirWebsocketClientProvider clientProvider, + SubscriptionHandlerFactory subscriptionHandlerFactory, FhirContext fhirContext, + String subscriptionSearchParameter, long retrySleepMillis, int maxRetries) { + this.resourcePath = resourcePath; this.clientProvider = clientProvider; - this.taskHandler = taskHandler; - this.lastEventTimeIo = lastEventTimeIo; + this.subscriptionHandlerFactory = subscriptionHandlerFactory; this.fhirContext = fhirContext; this.subscriptionSearchParameter = parse(subscriptionSearchParameter, null); this.retrySleepMillis = retrySleepMillis; @@ -75,8 +77,6 @@ private static Map> parse(String queryParameters, String ex public void afterPropertiesSet() throws Exception { Objects.requireNonNull(clientProvider, "clientProvider"); - Objects.requireNonNull(taskHandler, "taskHandler"); - Objects.requireNonNull(lastEventTimeIo, "lastEventTimeIo"); Objects.requireNonNull(fhirContext, "fhirContext"); } @@ -86,15 +86,15 @@ public void connect() logger.debug("Retrieving Subscription and connecting to websocket"); CompletableFuture.supplyAsync(this::retrieveWebsocketSubscription, Executors.newSingleThreadExecutor()) - .thenApply(this::loadExistingTasks).thenAccept(this::connectWebsocket).exceptionally(this::onError); + .thenApply(this::loadExistingResources).thenAccept(this::connectWebsocket).exceptionally(this::onError); } private Subscription retrieveWebsocketSubscription() { if (maxRetries >= 0) - return retry(() -> doRetrieveWebsocketSubscription()); + return retry(this::doRetrieveWebsocketSubscription); else - return retryForever(() -> doRetrieveWebsocketSubscription()); + return retryForever(this::doRetrieveWebsocketSubscription); } private Subscription retry(Supplier supplier) @@ -160,7 +160,7 @@ private Subscription doRetrieveWebsocketSubscription() Bundle bundle = clientProvider.getLocalWebserviceClient().searchWithStrictHandling(Subscription.class, subscriptionSearchParameter); - if (!BundleType.SEARCHSET.equals(bundle.getType())) + if (!Bundle.BundleType.SEARCHSET.equals(bundle.getType())) throw new RuntimeException("Could not retrieve searchset for subscription search query " + subscriptionSearchParameter + ", but got " + bundle.getType()); if (bundle.getTotal() != 1) @@ -177,14 +177,15 @@ private Subscription doRetrieveWebsocketSubscription() return subscription; } - private Subscription loadExistingTasks(Subscription subscription) + private Subscription loadExistingResources(Subscription subscription) { - logger.debug("Downloading existing Task resources"); + logger.debug("Downloading existing resources"); - FhirWebserviceClient webserviceClient = clientProvider.getLocalWebserviceClient(); - ExistingTaskLoader existingTaskLoader = new ExistingTaskLoader(lastEventTimeIo, taskHandler, webserviceClient); - Map> subscriptionCriteria = parse(subscription.getCriteria(), "Task"); - existingTaskLoader.readExistingTasks(subscriptionCriteria); + FhirWebserviceClient client = clientProvider.getLocalWebserviceClient(); + ExistingResourceLoader existingResourceLoader = subscriptionHandlerFactory + .createExistingResourceLoader(client); + Map> subscriptionCriteria = parse(subscription.getCriteria(), resourcePath); + existingResourceLoader.readExistingResources(subscriptionCriteria); return subscription; } @@ -199,7 +200,7 @@ private void connectWebsocket(Subscription subscription) EventType eventType = toEventType(subscription.getChannel().getPayload()); if (EventType.PING.equals(eventType)) { - Map> subscriptionCriteria = parse(subscription.getCriteria(), "Task"); + Map> subscriptionCriteria = parse(subscription.getCriteria(), resourcePath); setPingEventHandler(client, subscription.getIdElement().getIdPart(), subscriptionCriteria); } else @@ -248,19 +249,21 @@ public void onContextClosedEvent(ContextClosedEvent event) clientProvider.disconnectAll(); } - protected void setPingEventHandler(WebsocketClient client, String subscriptionIdPart, + private void setPingEventHandler(WebsocketClient client, String subscriptionIdPart, Map> searchCriteriaQueryParameters) { FhirWebserviceClient webserviceClient = clientProvider.getLocalWebserviceClient(); - PingEventHandler handler = new PingEventHandler( - new ExistingTaskLoader(lastEventTimeIo, taskHandler, webserviceClient)); - client.setPingHandler(ping -> handler.onPing(ping, subscriptionIdPart, searchCriteriaQueryParameters)); + ExistingResourceLoader existingResourceLoader = subscriptionHandlerFactory + .createExistingResourceLoader(webserviceClient); + PingEventResourceHandler pingHandler = subscriptionHandlerFactory + .createPingEventResourceHandler(existingResourceLoader); + client.setPingHandler(ping -> pingHandler.onPing(ping, subscriptionIdPart, searchCriteriaQueryParameters)); } - protected void setResourceEventHandler(WebsocketClient client, EventType eventType) + private void setResourceEventHandler(WebsocketClient client, EventType eventType) { - ResourceEventHandler handler = new ResourceEventHandler(lastEventTimeIo, taskHandler); - client.setDomainResourceHandler(handler::onResource, createParserFactory(eventType, fhirContext)); + EventResourceHandler eventHandler = subscriptionHandlerFactory.createEventResourceHandler(); + client.setDomainResourceHandler(eventHandler::onResource, createParserFactory(eventType, fhirContext)); } private Supplier createParserFactory(EventType eventType, FhirContext fhirContext) diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/LastEventTimeIo.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/LastEventTimeIo.java deleted file mode 100755 index b88a1da1f..000000000 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/LastEventTimeIo.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.highmed.dsf.fhir.websocket; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.Date; -import java.util.Objects; -import java.util.Optional; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.InitializingBean; - -public class LastEventTimeIo implements InitializingBean -{ - private static final Logger logger = LoggerFactory.getLogger(LastEventTimeIo.class); - private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ISO_LOCAL_DATE_TIME; - - private final Path lastEventTimeFile; - - public LastEventTimeIo(Path lastEventTimeFile) - { - this.lastEventTimeFile = lastEventTimeFile; - } - - @Override - public void afterPropertiesSet() throws Exception - { - Objects.requireNonNull(lastEventTimeFile, "lastEventTimeFile"); - - if (Files.exists(lastEventTimeFile) && !Files.isWritable(lastEventTimeFile)) - throw new IOException("Last event time file at " + lastEventTimeFile.toString() + " not writable"); - else if (!Files.isWritable(lastEventTimeFile.getParent())) - throw new IOException("Last event time file at " + lastEventTimeFile.toString() - + " not existing and parent not writable"); - } - - public Optional readLastEventTime() - { - try - { - Optional value = Optional - .of(LocalDateTime.parse(Files.readString(lastEventTimeFile), DATE_TIME_FORMAT)); - - logger.debug("Read {} from {}", value.get(), lastEventTimeFile); - return value; - } - catch (DateTimeParseException e) - { - logger.warn("Error while reading last event time file: {} {}", e.getClass().getName(), e.getMessage()); - return Optional.empty(); - } - catch (IOException e) - { - logger.warn("Error while reading last event time file: {} {}", e.getClass().getName(), e.getMessage()); - return Optional.empty(); - } - } - - public LocalDateTime writeLastEventTime(LocalDateTime localDateTime) - { - try - { - String value = localDateTime.format(DATE_TIME_FORMAT); - logger.debug("Writing {} to {}", value, lastEventTimeFile); - - Files.writeString(lastEventTimeFile, value, StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING); - } - catch (IOException e) - { - logger.warn("Error while writing last event time file: {} {}", e.getClass().getName(), e.getMessage()); - } - - return localDateTime; - } - - public void writeLastEventTime(Date authoredOn) - { - LocalDateTime localDateTime = LocalDateTime.ofInstant(authoredOn.toInstant(), ZoneId.systemDefault()); - writeLastEventTime(localDateTime); - } -} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/ResourceEventHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/ResourceEventHandler.java deleted file mode 100755 index cdc033f01..000000000 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/ResourceEventHandler.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.highmed.dsf.fhir.websocket; - -import org.highmed.dsf.fhir.task.TaskHandler; -import org.hl7.fhir.r4.model.DomainResource; -import org.hl7.fhir.r4.model.Task; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ca.uhn.fhir.model.api.annotation.ResourceDef; - -public class ResourceEventHandler -{ - private static final Logger logger = LoggerFactory.getLogger(ResourceEventHandler.class); - - private final TaskHandler taskHandler; - private final LastEventTimeIo lastEventTimeIo; - - public ResourceEventHandler(LastEventTimeIo lastEventTimeIo, TaskHandler taskHandler) - { - this.lastEventTimeIo = lastEventTimeIo; - this.taskHandler = taskHandler; - } - - public void onResource(DomainResource resource) - { - logger.trace("Resource of type {} received", resource.getClass().getAnnotation(ResourceDef.class).name()); - - if (resource instanceof Task) - { - Task task = (Task) resource; - taskHandler.onTask(task); - lastEventTimeIo.writeLastEventTime(task.getAuthoredOn()); - } - else - logger.warn("Ignoring resource of type {}"); - } -} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/ResourceHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/ResourceHandler.java new file mode 100644 index 000000000..6c0080b8f --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/fhir/websocket/ResourceHandler.java @@ -0,0 +1,8 @@ +package org.highmed.dsf.fhir.websocket; + +import org.hl7.fhir.r4.model.Resource; + +public interface ResourceHandler +{ + void onResource(R resource); +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/db/camunda/postgres_engine_7.17_to_7.18.sql b/dsf-bpe/dsf-bpe-server/src/main/resources/db/camunda/postgres_engine_7.17_to_7.18.sql new file mode 100644 index 000000000..4fa868407 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/db/camunda/postgres_engine_7.17_to_7.18.sql @@ -0,0 +1,34 @@ +-- +-- Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH +-- under one or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information regarding copyright +-- ownership. Camunda licenses this file to you under the Apache License, +-- Version 2.0; 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. +-- + +insert into ACT_GE_SCHEMA_LOG +values ('700', CURRENT_TIMESTAMP, '7.18.0'); + +-- https://jira.camunda.com/browse/CAM-14303 -- +ALTER TABLE ACT_RU_TASK + ADD COLUMN LAST_UPDATED_ timestamp; +create index ACT_IDX_TASK_LAST_UPDATED on ACT_RU_TASK(LAST_UPDATED_); + +-- https://jira.camunda.com/browse/CAM-14721 +ALTER TABLE ACT_RU_BATCH + ADD COLUMN START_TIME_ timestamp; + +-- https://jira.camunda.com/browse/CAM-14722 +ALTER TABLE ACT_RU_BATCH + ADD COLUMN EXEC_START_TIME_ timestamp; +ALTER TABLE ACT_HI_BATCH + ADD COLUMN EXEC_START_TIME_ timestamp; \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.camunda_engine.changelog-0.8.0.xml b/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.camunda_engine.changelog-0.8.0.xml new file mode 100644 index 000000000..0c728645e --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.camunda_engine.changelog-0.8.0.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.changelog.xml b/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.changelog.xml index ef2bf1fcd..93e510bab 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.changelog.xml +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.changelog.xml @@ -21,5 +21,9 @@ + + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.last_event.changelog-0.8.0.xml b/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.last_event.changelog-0.8.0.xml new file mode 100644 index 000000000..4147992f0 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/db/db.last_event.changelog-0.8.0.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + ALTER TABLE last_events OWNER TO ${db.liquibase_user}; + GRANT ALL ON TABLE process_states TO ${db.liquibase_user}; + GRANT SELECT, INSERT, UPDATE ON TABLE last_events TO ${db.server_users_group}; + + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/dao/AbstractDaoTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/dao/AbstractDaoTest.java new file mode 100644 index 000000000..99f5d9678 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/dao/AbstractDaoTest.java @@ -0,0 +1,47 @@ +package org.highmed.dsf.bpe.dao; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; + +import de.rwh.utils.test.LiquibaseTemplateTestClassRule; +import de.rwh.utils.test.LiquibaseTemplateTestRule; + +public class AbstractDaoTest extends AbstractDbTest +{ + public static final String DAO_DB_TEMPLATE_NAME = "dao_template"; + + protected static final BasicDataSource adminDataSource = createAdminBasicDataSource(); + protected static final BasicDataSource liquibaseDataSource = createLiquibaseDataSource(); + protected static final BasicDataSource defaultDataSource = createDefaultDataSource(); + protected static final BasicDataSource camundaDataSource = createCamundaDataSource(); + + @ClassRule + public static final LiquibaseTemplateTestClassRule liquibaseRule = new LiquibaseTemplateTestClassRule( + adminDataSource, LiquibaseTemplateTestClassRule.DEFAULT_TEST_DB_NAME, DAO_DB_TEMPLATE_NAME, + liquibaseDataSource, CHANGE_LOG_FILE, CHANGE_LOG_PARAMETERS, true); + + @BeforeClass + public static void beforeClass() throws Exception + { + defaultDataSource.start(); + liquibaseDataSource.start(); + adminDataSource.start(); + camundaDataSource.start(); + } + + @AfterClass + public static void afterClass() throws Exception + { + defaultDataSource.close(); + liquibaseDataSource.close(); + adminDataSource.close(); + camundaDataSource.close(); + } + + @Rule + public final LiquibaseTemplateTestRule templateRule = new LiquibaseTemplateTestRule(adminDataSource, + LiquibaseTemplateTestClassRule.DEFAULT_TEST_DB_NAME, DAO_DB_TEMPLATE_NAME); +} diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/dao/AbstractDbTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/dao/AbstractDbTest.java new file mode 100644 index 000000000..759d0a4f6 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/dao/AbstractDbTest.java @@ -0,0 +1,92 @@ +package org.highmed.dsf.bpe.dao; + +import java.util.Map; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.postgresql.Driver; +import org.slf4j.bridge.SLF4JBridgeHandler; + +public abstract class AbstractDbTest +{ + static + { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + } + + protected static final String CHANGE_LOG_FILE = "db/db.changelog.xml"; + + protected static final String DATABASE_USERS_GROUP = "server_users_group"; + protected static final String DATABASE_USER = "server_user"; + protected static final String DATABASE_USER_PASSWORD = "server_user_password"; + + protected static final String DATABASE_CAMUNDA_USERS_GROUP = "camunda_users_group"; + protected static final String DATABASE_CAMUNDA_USER = "camunda_user"; + protected static final String DATABASE_CAMUNDA_USER_PASSWORD = "camunda_user_password"; + + protected static final String DATABASE_URL = "jdbc:postgresql://localhost:54321/db"; + + protected static final Map CHANGE_LOG_PARAMETERS = Map.of("db.liquibase_user", "postgres", + "db.server_users_group", DATABASE_USERS_GROUP, "db.server_user", DATABASE_USER, "db.server_user_password", + DATABASE_USER_PASSWORD, "db.camunda_users_group", DATABASE_CAMUNDA_USERS_GROUP, "db.camunda_user", + DATABASE_CAMUNDA_USER, "db.camunda_user_password", DATABASE_CAMUNDA_USER_PASSWORD); + + public static BasicDataSource createDefaultDataSource() + { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName(Driver.class.getName()); + dataSource.setUrl(DATABASE_URL); + dataSource.setUsername(DATABASE_USER); + dataSource.setPassword(DATABASE_USER_PASSWORD); + dataSource.setDefaultReadOnly(true); + + dataSource.setTestOnBorrow(true); + dataSource.setValidationQuery("SELECT 1"); + + return dataSource; + } + + public static BasicDataSource createLiquibaseDataSource() + { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName(Driver.class.getName()); + dataSource.setUrl(DATABASE_URL); + dataSource.setUsername("postgres"); + dataSource.setPassword("password"); + dataSource.setDefaultReadOnly(true); + + dataSource.setTestOnBorrow(true); + dataSource.setValidationQuery("SELECT 1"); + + return dataSource; + } + + public static BasicDataSource createAdminBasicDataSource() + { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName(Driver.class.getName()); + dataSource.setUrl("jdbc:postgresql://localhost:54321/postgres"); + dataSource.setUsername("postgres"); + dataSource.setPassword("password"); + + dataSource.setTestOnBorrow(true); + dataSource.setValidationQuery("SELECT 1"); + + return dataSource; + } + + public static BasicDataSource createCamundaDataSource() + { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName(Driver.class.getName()); + dataSource.setUrl(DATABASE_URL); + dataSource.setUsername(DATABASE_CAMUNDA_USER); + dataSource.setPassword(DATABASE_CAMUNDA_USER_PASSWORD); + dataSource.setDefaultReadOnly(true); + + dataSource.setTestOnBorrow(true); + dataSource.setValidationQuery("SELECT 1"); + + return dataSource; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/dao/LastEventTimeDaoTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/dao/LastEventTimeDaoTest.java new file mode 100644 index 000000000..b2f5e7449 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/dao/LastEventTimeDaoTest.java @@ -0,0 +1,133 @@ +package org.highmed.dsf.bpe.dao; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.Optional; + +import org.junit.Test; + +public class LastEventTimeDaoTest extends AbstractDaoTest +{ + private LastEventTimeDao dao = new LastEventTimeDaoJdbc(defaultDataSource, "test_type"); + + @Test + public void testReadEmpty() throws Exception + { + Optional lastEventTime = dao.readLastEventTime(); + assertNotNull(lastEventTime); + assertTrue(lastEventTime.isEmpty()); + } + + @Test(expected = NullPointerException.class) + public void testWriteLocalDateTimeNull() throws Exception + { + dao.writeLastEventTime((LocalDateTime) null); + } + + @Test(expected = NullPointerException.class) + public void testWriteDateNull() throws Exception + { + dao.writeLastEventTime((Date) null); + } + + @Test + public void testWriteLocalDateTime() throws Exception + { + LocalDateTime write = LocalDateTime.now(); + LocalDateTime written = dao.writeLastEventTime(write); + + assertNotNull(written); + assertNotSame(write, written); + assertEquals(write.truncatedTo(ChronoUnit.MILLIS), written); + } + + @Test + public void testWriteLocalDateTime2() throws Exception + { + LocalDateTime write1 = LocalDateTime.now(); + LocalDateTime written1 = dao.writeLastEventTime(write1); + + assertNotNull(written1); + assertNotSame(write1, written1); + assertEquals(write1.truncatedTo(ChronoUnit.MILLIS), written1); + + LocalDateTime write2 = LocalDateTime.now().plusMinutes(1); + LocalDateTime written2 = dao.writeLastEventTime(write2); + + assertNotNull(written2); + assertNotSame(write2, written2); + assertEquals(write2.truncatedTo(ChronoUnit.MILLIS), written2); + } + + @Test + public void testReadWriteReadLocalDateTime() throws Exception + { + Optional readLastEvent1 = dao.readLastEventTime(); + assertNotNull(readLastEvent1); + assertTrue(readLastEvent1.isEmpty()); + + LocalDateTime lastEvent = dao.writeLastEventTime(LocalDateTime.now()); + + Optional readLastEvent2 = dao.readLastEventTime(); + assertNotNull(readLastEvent2); + assertTrue(readLastEvent2.isPresent()); + + assertEquals(lastEvent, readLastEvent2.get()); + } + + @Test + public void testReadWriteReadLocalDateTime2() throws Exception + { + Optional readLastEvent0 = dao.readLastEventTime(); + assertNotNull(readLastEvent0); + assertTrue(readLastEvent0.isEmpty()); + + LocalDateTime lastEvent = dao.writeLastEventTime(LocalDateTime.now()); + + Optional readLastEvent1 = dao.readLastEventTime(); + assertNotNull(readLastEvent1); + assertTrue(readLastEvent1.isPresent()); + + assertEquals(lastEvent, readLastEvent1.get()); + + LocalDateTime lastEvent2 = dao.writeLastEventTime(LocalDateTime.now().plusMinutes(1)); + + Optional readLastEvent2 = dao.readLastEventTime(); + assertNotNull(readLastEvent2); + assertTrue(readLastEvent2.isPresent()); + + assertEquals(lastEvent2, readLastEvent2.get()); + } + + @Test + public void testWriteDate() throws Exception + { + Date write = new Date(); + Date written = dao.writeLastEventTime(write); + + assertEquals(write, written); + } + + @Test + public void testReadWriteReadDate() throws Exception + { + Optional readLastEvent1 = dao.readLastEventTime(); + assertNotNull(readLastEvent1); + assertTrue(readLastEvent1.isEmpty()); + + Date lastEvent = dao.writeLastEventTime(new Date()); + + Optional readLastEvent2 = dao.readLastEventTime(); + assertNotNull(readLastEvent2); + assertTrue(readLastEvent2.isPresent()); + + assertEquals(LocalDateTime.ofInstant(lastEvent.toInstant(), ZoneId.systemDefault()), readLastEvent2.get()); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/service/LoggingMailServiceTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/service/LoggingMailServiceTest.java new file mode 100644 index 000000000..67a871e03 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/service/LoggingMailServiceTest.java @@ -0,0 +1,12 @@ +package org.highmed.dsf.bpe.service; + +import org.junit.Test; + +public class LoggingMailServiceTest +{ + @Test + public void testSend() throws Exception + { + new LoggingMailService().send("subject test", "message test"); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/service/SmtpMailServiceTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/service/SmtpMailServiceTest.java new file mode 100644 index 000000000..89b944ed9 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/bpe/service/SmtpMailServiceTest.java @@ -0,0 +1,68 @@ +package org.highmed.dsf.bpe.service; + +import java.nio.file.Paths; +import java.security.KeyStore; +import java.util.Arrays; + +import org.junit.Ignore; +import org.junit.Test; + +import de.rwh.utils.crypto.io.CertificateReader; + +@Ignore +public class SmtpMailServiceTest +{ + @Test + public void testSend() throws Exception + { + new SmtpMailService("from@localhost", Arrays.asList("to@localhost"), "localhost", 1025).send("test subject", + "test message"); + } + + @Test + public void testSendTo() throws Exception + { + new SmtpMailService("from@localhost", Arrays.asList("to@localhost"), "localhost", 1025).send("test subject", + "test message", "to-test@localhost"); + } + + @Test + public void testSendReplyAndCc() throws Exception + { + new SmtpMailService("from@localhost", Arrays.asList("to1@localhost", "to2@localhost"), + Arrays.asList("cc1@localhost", "cc2@localhost"), + Arrays.asList("replyTo1@localhost", "replyTo2@localhost"), false, "localhost", 1025, null, null, null, + null, null, null, null, false, 0, SmtpMailService.DEFAULT_DEBUG_LOG_LOCATION) + .send("test subject", "test message"); + } + + @Test + public void testSendSigned() throws Exception + { + char[] signStorePassword = "password".toCharArray(); + KeyStore signStore = CertificateReader.fromPkcs12(Paths.get("cert.p12"), signStorePassword); + + new SmtpMailService("from@localhost", Arrays.asList("to@localhost"), null, null, false, "localhost", 1025, null, + null, null, null, null, signStore, signStorePassword, false, 0, + SmtpMailService.DEFAULT_DEBUG_LOG_LOCATION).send("test subject", "test message"); + } + + @Test + public void testSendViaSmtps() throws Exception + { + KeyStore trustStore = CertificateReader.allFromCer(Paths.get("cert.pem")); + new SmtpMailService("from@localhost", Arrays.asList("to@localhost"), null, null, true, "localhost", 465, null, + null, trustStore, null, null, null, null, false, 0, SmtpMailService.DEFAULT_DEBUG_LOG_LOCATION) + .send("test subject", "test message"); + + } + + @Test + public void testSendViaGmail() throws Exception + { + new SmtpMailService("foo@gmail.com", Arrays.asList("foo@gmail.com"), null, null, true, "smtp.gmail.com", 465, + "foo", "password".toCharArray(), null, null, null, null, null, false, 0, + SmtpMailService.DEFAULT_DEBUG_LOG_LOCATION).send("test subject", "test message"); + + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseTest.java new file mode 100644 index 000000000..c6dc07d87 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/fhir/questionnaire/QuestionnaireResponseTest.java @@ -0,0 +1,253 @@ +package org.highmed.dsf.fhir.questionnaire; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.TimeType; +import org.hl7.fhir.r4.model.Type; +import org.hl7.fhir.r4.model.UriType; +import org.junit.Test; + +public class QuestionnaireResponseTest +{ + @Test + public void testFlattenItemsToLeaves() + { + QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse(); + + QuestionnaireResponse.QuestionnaireResponseItemComponent node1 = questionnaireResponse.addItem(); + node1.setLinkId("Item 1.1 LinkId").addAnswer().setValue(new StringType("Item 1.1 Value")); + + QuestionnaireResponse.QuestionnaireResponseItemComponent node2 = questionnaireResponse.addItem(); + node2.addItem().setLinkId("Item 2.1 LinkId").addAnswer().setValue(new StringType("Item 2.1 Value")); + node2.addItem().setLinkId("Item 2.2 LinkId").addAnswer().setValue(new StringType("Item 2.2 Value")); + + QuestionnaireResponse.QuestionnaireResponseItemComponent node3 = questionnaireResponse.addItem(); + node3.addItem().addItem().setLinkId("Item 3.1 LinkId").addAnswer().setValue(new StringType("Item 3.1 Value")); + + QuestionnaireResponseHelper questionnaireResponseHelper = new QuestionnaireResponseHelperImpl(); + + List itemLeavesAsList = questionnaireResponseHelper + .getItemLeavesAsList(questionnaireResponse); + + assertEquals(4, itemLeavesAsList.size()); + } + + @Test + public void testQuestionTypeStringToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.STRING); + + Type type = qrh.transformQuestionTypeToAnswerType(question); + + assertTrue(type instanceof StringType); + } + + @Test + public void testQuestionTypeTextToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.TEXT); + + Type type = qrh.transformQuestionTypeToAnswerType(question); + + assertTrue(type instanceof StringType); + } + + @Test + public void testQuestionTypeBooleanToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.BOOLEAN); + + Type type = qrh.transformQuestionTypeToAnswerType(question); + + assertTrue(type instanceof BooleanType); + } + + @Test + public void testQuestionTypeDecimalToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.DECIMAL); + + Type type = qrh.transformQuestionTypeToAnswerType(question); + + assertTrue(type instanceof DecimalType); + } + + @Test + public void testQuestionTypeIntegerToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.INTEGER); + + Type type = qrh.transformQuestionTypeToAnswerType(question); + + assertTrue(type instanceof IntegerType); + } + + @Test + public void testQuestionTypeDateToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.DATE); + + Type type = qrh.transformQuestionTypeToAnswerType(question); + + assertTrue(type instanceof DateType); + } + + @Test + public void testQuestionTypeDateTimeToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.DATETIME); + + Type type = qrh.transformQuestionTypeToAnswerType(question); + + assertTrue(type instanceof DateTimeType); + } + + @Test + public void testQuestionTypeTimeToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.TIME); + + Type type = qrh.transformQuestionTypeToAnswerType(question); + + assertTrue(type instanceof TimeType); + } + + @Test + public void testQuestionTypeUrlToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.URL); + + Type type = qrh.transformQuestionTypeToAnswerType(question); + + assertTrue(type instanceof UriType); + } + + @Test + public void testQuestionTypeReferenceToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.REFERENCE); + + Type type = qrh.transformQuestionTypeToAnswerType(question); + + assertTrue(type instanceof Reference); + } + + @Test(expected = RuntimeException.class) + public void testQuestionTypeChoiceToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.CHOICE); + + qrh.transformQuestionTypeToAnswerType(question); + } + + @Test(expected = RuntimeException.class) + public void testQuestionTypeOpenChoiceToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.OPENCHOICE); + + qrh.transformQuestionTypeToAnswerType(question); + } + + @Test(expected = RuntimeException.class) + public void testQuestionTypeQuantityToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.QUANTITY); + + qrh.transformQuestionTypeToAnswerType(question); + } + + @Test(expected = RuntimeException.class) + public void testQuestionTypeAttachmentToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.ATTACHMENT); + + qrh.transformQuestionTypeToAnswerType(question); + } + + @Test(expected = RuntimeException.class) + public void testQuestionTypeGroupToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.GROUP); + + qrh.transformQuestionTypeToAnswerType(question); + } + + @Test(expected = RuntimeException.class) + public void testQuestionTypeDisplayToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.DISPLAY); + + qrh.transformQuestionTypeToAnswerType(question); + } + + @Test(expected = RuntimeException.class) + public void testQuestionTypeQuestionToAnswerType() + { + QuestionnaireResponseHelper qrh = new QuestionnaireResponseHelperImpl(); + + Questionnaire.QuestionnaireItemComponent question = new Questionnaire.QuestionnaireItemComponent() + .setType(Questionnaire.QuestionnaireItemType.QUESTION); + + qrh.transformQuestionTypeToAnswerType(question); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/fhir/websocket/LastEventTimeIoTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/fhir/websocket/LastEventTimeIoTest.java deleted file mode 100644 index 145cb471a..000000000 --- a/dsf-bpe/dsf-bpe-server/src/test/java/org/highmed/dsf/fhir/websocket/LastEventTimeIoTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.highmed.dsf.fhir.websocket; - -import static org.junit.Assert.*; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.util.Optional; -import java.util.UUID; - -import org.junit.Test; - -public class LastEventTimeIoTest -{ - @Test - public void testWriteRead() throws Exception - { - Path lastEventTimeFile = Paths.get("target", UUID.randomUUID().toString()); - try - { - LastEventTimeIo io = new LastEventTimeIo(lastEventTimeFile); - - LocalDateTime written = io.writeLastEventTime(LocalDateTime.now()); - assertNotNull(written); - - Optional read = io.readLastEventTime(); - assertTrue(read.isPresent()); - - assertEquals(written, read.get()); - } - finally - { - Files.deleteIfExists(lastEventTimeFile); - } - } - - @Test - public void testReadNotExistingFile() throws Exception - { - Path lastEventTimeFile = Paths.get("target", UUID.randomUUID().toString()); - LastEventTimeIo io = new LastEventTimeIo(lastEventTimeFile); - - assertFalse(io.readLastEventTime().isPresent()); - } - - @Test - public void testReadEmptyFile() throws Exception - { - Path lastEventTimeFile = Paths.get("target", UUID.randomUUID().toString()); - Files.createFile(lastEventTimeFile); - - LastEventTimeIo io = new LastEventTimeIo(lastEventTimeFile); - - assertFalse(io.readLastEventTime().isPresent()); - } -} diff --git a/dsf-bpe/dsf-bpe-webservice-client/pom.xml b/dsf-bpe/dsf-bpe-webservice-client/pom.xml index 3d1893a3c..03abb546c 100755 --- a/dsf-bpe/dsf-bpe-webservice-client/pom.xml +++ b/dsf-bpe/dsf-bpe-webservice-client/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-bpe-pom - 0.7.0 + 0.8.0 diff --git a/dsf-bpe/pom.xml b/dsf-bpe/pom.xml index f9335c981..33e260d2f 100755 --- a/dsf-bpe/pom.xml +++ b/dsf-bpe/pom.xml @@ -8,7 +8,7 @@ org.highmed.dsf dsf-pom - 0.7.0 + 0.8.0 @@ -22,7 +22,7 @@ ${project.basedir}/../.. - 7.17.0 + 7.18.0 diff --git a/dsf-consent/dsf-consent-client-stub/pom.xml b/dsf-consent/dsf-consent-client-stub/pom.xml index 503255081..3f565da81 100644 --- a/dsf-consent/dsf-consent-client-stub/pom.xml +++ b/dsf-consent/dsf-consent-client-stub/pom.xml @@ -9,7 +9,7 @@ dsf-consent-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-consent/dsf-consent-client/pom.xml b/dsf-consent/dsf-consent-client/pom.xml index 027058c9b..a8c6b2bdf 100644 --- a/dsf-consent/dsf-consent-client/pom.xml +++ b/dsf-consent/dsf-consent-client/pom.xml @@ -9,7 +9,7 @@ dsf-consent-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-consent/pom.xml b/dsf-consent/pom.xml index 8438de4aa..24d963486 100644 --- a/dsf-consent/pom.xml +++ b/dsf-consent/pom.xml @@ -10,7 +10,7 @@ dsf-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-docker-test-setup-3medic-ttp-docker/docker-compose.yml b/dsf-docker-test-setup-3medic-ttp-docker/docker-compose.yml index f3e435b6a..7680914cd 100644 --- a/dsf-docker-test-setup-3medic-ttp-docker/docker-compose.yml +++ b/dsf-docker-test-setup-3medic-ttp-docker/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: proxy: image: nginx:1.21 - restart: on-failure + restart: "no" ports: - 127.0.0.1:443:443 secrets: @@ -38,7 +38,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" healthcheck: test: [ "CMD-SHELL", "pg_isready -U postgres -d postgres" ] interval: 10s @@ -71,7 +71,7 @@ services: medic1-fhir: image: highmed/fhir - restart: on-failure + restart: "no" ports: - 127.0.0.1:5001:5001 healthcheck: @@ -128,7 +128,7 @@ services: medic2-fhir: image: highmed/fhir - restart: on-failure + restart: "no" ports: - 127.0.0.1:5002:5002 healthcheck: @@ -185,7 +185,7 @@ services: medic3-fhir: image: highmed/fhir - restart: on-failure + restart: "no" ports: - 127.0.0.1:5003:5003 healthcheck: @@ -242,7 +242,7 @@ services: ttp-fhir: image: highmed/fhir - restart: on-failure + restart: "no" ports: - 127.0.0.1:5004:5004 healthcheck: @@ -301,7 +301,7 @@ services: medic1-bpe: image: highmed/bpe - restart: on-failure + restart: "no" ports: - 127.0.0.1:5011:5011 healthcheck: @@ -329,9 +329,6 @@ services: - type: bind source: ./medic1/bpe/log target: /opt/bpe/log - - type: bind - source: ./medic1/bpe/last_event - target: /opt/bpe/last_event - type: bind source: ./medic1/bpe/psn target: /opt/bpe/psn @@ -352,6 +349,11 @@ services: ORG_HIGHMED_DSF_BPE_DB_USER_CAMUNDA_USERNAME: medic1_camunda_server_user ORG_HIGHMED_DSF_BPE_FHIR_SERVER_ORGANIZATION_IDENTIFIER_VALUE: Test_MeDIC_1 ORG_HIGHMED_DSF_BPE_FHIR_SERVER_BASE_URL: https://medic1-docker/fhir + ORG_HIGHMED_DSF_BPE_MAIL_HOST: mailhog + ORG_HIGHMED_DSF_BPE_MAIL_PORT: 1025 + ORG_HIGHMED_DSF_BPE_MAIL_FROMADDRESS: bpe@medic1-docker + ORG_HIGHMED_DSF_BPE_MAIL_TOADDRESSES: bpe@medic1-docker + #ORG_HIGHMED_DSF_BPE_MAIL_SENDTESTMAILONSTARTUP: 'false' # default no test mail on startup ORG_HIGHMED_DSF_BPE_PROCESS_EXCLUDED: highmedorg_computeFeasibility/0.6.0,highmedorg_computeDataSharing/0.6.0,highmedorg_requestUpdateResources/0.6.0,highmedorg_updateAllowList/0.6.0 # property org.highmed.dsf.bpe.allow.list.organization should only be set for testing, do not configure property in production, potential security risk ORG_HIGHMED_DSF_BPE_ALLOW_LIST_ORGANIZATION: Test_TTP @@ -365,7 +367,7 @@ services: medic2-bpe: image: highmed/bpe - restart: on-failure + restart: "no" ports: - 127.0.0.1:5012:5012 healthcheck: @@ -393,9 +395,6 @@ services: - type: bind source: ./medic2/bpe/log target: /opt/bpe/log - - type: bind - source: ./medic2/bpe/last_event - target: /opt/bpe/last_event - type: bind source: ./medic2/bpe/psn target: /opt/bpe/psn @@ -416,6 +415,11 @@ services: ORG_HIGHMED_DSF_BPE_DB_USER_CAMUNDA_USERNAME: medic2_camunda_server_user ORG_HIGHMED_DSF_BPE_FHIR_SERVER_ORGANIZATION_IDENTIFIER_VALUE: Test_MeDIC_2 ORG_HIGHMED_DSF_BPE_FHIR_SERVER_BASE_URL: https://medic2-docker/fhir + ORG_HIGHMED_DSF_BPE_MAIL_HOST: mailhog + ORG_HIGHMED_DSF_BPE_MAIL_PORT: 1025 + ORG_HIGHMED_DSF_BPE_MAIL_FROMADDRESS: bpe@medic2-docker + ORG_HIGHMED_DSF_BPE_MAIL_TOADDRESSES: bpe@medic2-docker + #ORG_HIGHMED_DSF_BPE_MAIL_SENDTESTMAILONSTARTUP: 'false' # default no test mail on startup ORG_HIGHMED_DSF_BPE_PROCESS_EXCLUDED: highmedorg_computeFeasibility/0.6.0, highmedorg_computeDataSharing/0.6.0, highmedorg_requestUpdateResources/0.6.0, highmedorg_updateAllowList/0.6.0 # property org.highmed.dsf.bpe.allow.list.organization should only be set for testing, do not configure property in production, potential security risk ORG_HIGHMED_DSF_BPE_ALLOW_LIST_ORGANIZATION: Test_TTP @@ -429,7 +433,7 @@ services: medic3-bpe: image: highmed/bpe - restart: on-failure + restart: "no" ports: - 127.0.0.1:5014:5014 healthcheck: @@ -457,9 +461,6 @@ services: - type: bind source: ./medic3/bpe/log target: /opt/bpe/log - - type: bind - source: ./medic3/bpe/last_event - target: /opt/bpe/last_event - type: bind source: ./medic3/bpe/psn target: /opt/bpe/psn @@ -480,6 +481,11 @@ services: ORG_HIGHMED_DSF_BPE_DB_USER_CAMUNDA_USERNAME: medic3_camunda_server_user ORG_HIGHMED_DSF_BPE_FHIR_SERVER_ORGANIZATION_IDENTIFIER_VALUE: Test_MeDIC_3 ORG_HIGHMED_DSF_BPE_FHIR_SERVER_BASE_URL: https://medic3-docker/fhir + ORG_HIGHMED_DSF_BPE_MAIL_HOST: mailhog + ORG_HIGHMED_DSF_BPE_MAIL_PORT: 1025 + ORG_HIGHMED_DSF_BPE_MAIL_FROMADDRESS: bpe@medic3-docker + ORG_HIGHMED_DSF_BPE_MAIL_TOADDRESSES: bpe@medic3-docker + #ORG_HIGHMED_DSF_BPE_MAIL_SENDTESTMAILONSTARTUP: 'false' # default no test mail on startup ORG_HIGHMED_DSF_BPE_PROCESS_EXCLUDED: > highmedorg_computeFeasibility/0.6.0, highmedorg_computeDataSharing/0.6.0, @@ -494,10 +500,11 @@ services: depends_on: - db - medic3-fhir + - mailhog ttp-bpe: image: highmed/bpe - restart: on-failure + restart: "no" ports: - 127.0.0.1:5013:5013 healthcheck: @@ -525,9 +532,6 @@ services: - type: bind source: ./ttp/bpe/log target: /opt/bpe/log - - type: bind - source: ./ttp/bpe/last_event - target: /opt/bpe/last_event - type: bind source: ./ttp/bpe/psn target: /opt/bpe/psn @@ -548,6 +552,12 @@ services: ORG_HIGHMED_DSF_BPE_DB_USER_CAMUNDA_USERNAME: ttp_camunda_server_user ORG_HIGHMED_DSF_BPE_FHIR_SERVER_ORGANIZATION_IDENTIFIER_VALUE: Test_TTP ORG_HIGHMED_DSF_BPE_FHIR_SERVER_BASE_URL: https://ttp-docker/fhir + ORG_HIGHMED_DSF_BPE_MAIL_HOST: mailhog + ORG_HIGHMED_DSF_BPE_MAIL_PORT: 1025 + ORG_HIGHMED_DSF_BPE_MAIL_FROMADDRESS: bpe@ttp-docker + ORG_HIGHMED_DSF_BPE_MAIL_TOADDRESSES: bpe@ttp-docker + ORG_HIGHMED_DSF_BPE_MAIL_SENDTESTMAILONSTARTUP: 'true' + ORG_HIGHMED_DSF_BPE_MAIL_SENDMAILONERRORLOGEVENT: 'true' ORG_HIGHMED_DSF_BPE_PROCESS_EXCLUDED: | highmedorg_executeUpdateResources/0.6.0 highmedorg_downloadAllowList/0.6.0 @@ -568,6 +578,15 @@ services: depends_on: - db - ttp-fhir + - mailhog + + mailhog: + image: mailhog/mailhog + restart: "no" + ports: + - 127.0.0.1:8025:8025 # web ui + networks: + internet: secrets: proxy_certificate_and_int_cas.pem: diff --git a/dsf-docker-test-setup-3medic-ttp-docker/medic1/bpe/last_event/README.md b/dsf-docker-test-setup-3medic-ttp-docker/medic1/bpe/last_event/README.md deleted file mode 100644 index 12fefbfaa..000000000 --- a/dsf-docker-test-setup-3medic-ttp-docker/medic1/bpe/last_event/README.md +++ /dev/null @@ -1 +0,0 @@ -empty directory for last-event time.file \ No newline at end of file diff --git a/dsf-docker-test-setup-3medic-ttp-docker/medic2/bpe/last_event/README.md b/dsf-docker-test-setup-3medic-ttp-docker/medic2/bpe/last_event/README.md deleted file mode 100644 index 12fefbfaa..000000000 --- a/dsf-docker-test-setup-3medic-ttp-docker/medic2/bpe/last_event/README.md +++ /dev/null @@ -1 +0,0 @@ -empty directory for last-event time.file \ No newline at end of file diff --git a/dsf-docker-test-setup-3medic-ttp-docker/medic3/bpe/last_event/README.md b/dsf-docker-test-setup-3medic-ttp-docker/medic3/bpe/last_event/README.md deleted file mode 100644 index 12fefbfaa..000000000 --- a/dsf-docker-test-setup-3medic-ttp-docker/medic3/bpe/last_event/README.md +++ /dev/null @@ -1 +0,0 @@ -empty directory for last-event time.file \ No newline at end of file diff --git a/dsf-docker-test-setup-3medic-ttp-docker/ttp/bpe/last_event/README.md b/dsf-docker-test-setup-3medic-ttp-docker/ttp/bpe/last_event/README.md deleted file mode 100644 index 12fefbfaa..000000000 --- a/dsf-docker-test-setup-3medic-ttp-docker/ttp/bpe/last_event/README.md +++ /dev/null @@ -1 +0,0 @@ -empty directory for last-event time.file \ No newline at end of file diff --git a/dsf-docker-test-setup-3medic-ttp/medic1/bpe/.rsync-filter b/dsf-docker-test-setup-3medic-ttp/medic1/bpe/.rsync-filter index 03008242c..4415547d5 100644 --- a/dsf-docker-test-setup-3medic-ttp/medic1/bpe/.rsync-filter +++ b/dsf-docker-test-setup-3medic-ttp/medic1/bpe/.rsync-filter @@ -1,5 +1,4 @@ - .rsync-filter -- last_event/README.md - log/README.md - plugin/README.md - process/README.md diff --git a/dsf-docker-test-setup-3medic-ttp/medic1/bpe/docker-compose.yml b/dsf-docker-test-setup-3medic-ttp/medic1/bpe/docker-compose.yml index d73c0d423..1c1a71865 100755 --- a/dsf-docker-test-setup-3medic-ttp/medic1/bpe/docker-compose.yml +++ b/dsf-docker-test-setup-3medic-ttp/medic1/bpe/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: app: image: registry:5000/highmed/bpe - restart: on-failure + restart: "no" healthcheck: test: ["CMD", "java", "-cp", "dsf_bpe.jar", "org.highmed.dsf.bpe.StatusClient"] interval: 10s @@ -29,9 +29,6 @@ services: - type: bind source: ./log target: /opt/bpe/log - - type: bind - source: ./last_event - target: /opt/bpe/last_event - type: bind source: ./psn target: /opt/bpe/psn @@ -69,7 +66,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" healthcheck: test: ["CMD-SHELL", "pg_isready -U liquibase_user -d bpe"] interval: 10s diff --git a/dsf-docker-test-setup-3medic-ttp/medic1/bpe/last_event/README.md b/dsf-docker-test-setup-3medic-ttp/medic1/bpe/last_event/README.md deleted file mode 100644 index 12fefbfaa..000000000 --- a/dsf-docker-test-setup-3medic-ttp/medic1/bpe/last_event/README.md +++ /dev/null @@ -1 +0,0 @@ -empty directory for last-event time.file \ No newline at end of file diff --git a/dsf-docker-test-setup-3medic-ttp/medic1/fhir/docker-compose.yml b/dsf-docker-test-setup-3medic-ttp/medic1/fhir/docker-compose.yml index 45f271fd1..b388d421e 100755 --- a/dsf-docker-test-setup-3medic-ttp/medic1/fhir/docker-compose.yml +++ b/dsf-docker-test-setup-3medic-ttp/medic1/fhir/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: proxy: image: registry:5000/highmed/fhir_proxy - restart: on-failure + restart: "no" ports: - 80:80 - 443:443 @@ -29,7 +29,7 @@ services: app: image: registry:5000/highmed/fhir - restart: on-failure + restart: "no" healthcheck: test: ["CMD", "java", "-cp", "dsf_fhir.jar", "org.highmed.dsf.fhir.StatusClient"] interval: 10s @@ -84,7 +84,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" healthcheck: test: ["CMD-SHELL", "pg_isready -U liquibase_user -d fhir"] interval: 10s diff --git a/dsf-docker-test-setup-3medic-ttp/medic2/bpe/.rsync-filter b/dsf-docker-test-setup-3medic-ttp/medic2/bpe/.rsync-filter index 03008242c..4415547d5 100644 --- a/dsf-docker-test-setup-3medic-ttp/medic2/bpe/.rsync-filter +++ b/dsf-docker-test-setup-3medic-ttp/medic2/bpe/.rsync-filter @@ -1,5 +1,4 @@ - .rsync-filter -- last_event/README.md - log/README.md - plugin/README.md - process/README.md diff --git a/dsf-docker-test-setup-3medic-ttp/medic2/bpe/docker-compose.yml b/dsf-docker-test-setup-3medic-ttp/medic2/bpe/docker-compose.yml index 7aab61d28..e8f1bb807 100755 --- a/dsf-docker-test-setup-3medic-ttp/medic2/bpe/docker-compose.yml +++ b/dsf-docker-test-setup-3medic-ttp/medic2/bpe/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: app: image: registry:5000/highmed/bpe - restart: on-failure + restart: "no" healthcheck: test: ["CMD", "java", "-cp", "dsf_bpe.jar", "org.highmed.dsf.bpe.StatusClient"] interval: 10s @@ -29,9 +29,6 @@ services: - type: bind source: ./log target: /opt/bpe/log - - type: bind - source: ./last_event - target: /opt/bpe/last_event - type: bind source: ./psn target: /opt/bpe/psn @@ -69,7 +66,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" healthcheck: test: ["CMD-SHELL", "pg_isready -U liquibase_user -d bpe"] interval: 10s diff --git a/dsf-docker-test-setup-3medic-ttp/medic2/bpe/last_event/README.md b/dsf-docker-test-setup-3medic-ttp/medic2/bpe/last_event/README.md deleted file mode 100644 index 12fefbfaa..000000000 --- a/dsf-docker-test-setup-3medic-ttp/medic2/bpe/last_event/README.md +++ /dev/null @@ -1 +0,0 @@ -empty directory for last-event time.file \ No newline at end of file diff --git a/dsf-docker-test-setup-3medic-ttp/medic2/fhir/docker-compose.yml b/dsf-docker-test-setup-3medic-ttp/medic2/fhir/docker-compose.yml index c9c69592e..bd14ed432 100755 --- a/dsf-docker-test-setup-3medic-ttp/medic2/fhir/docker-compose.yml +++ b/dsf-docker-test-setup-3medic-ttp/medic2/fhir/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: proxy: image: registry:5000/highmed/fhir_proxy - restart: on-failure + restart: "no" ports: - 80:80 - 443:443 @@ -29,7 +29,7 @@ services: app: image: registry:5000/highmed/fhir - restart: on-failure + restart: "no" healthcheck: test: ["CMD", "java", "-cp", "dsf_fhir.jar", "org.highmed.dsf.fhir.StatusClient"] interval: 10s @@ -84,7 +84,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" healthcheck: test: ["CMD-SHELL", "pg_isready -U liquibase_user -d fhir"] interval: 10s diff --git a/dsf-docker-test-setup-3medic-ttp/medic3/bpe/.rsync-filter b/dsf-docker-test-setup-3medic-ttp/medic3/bpe/.rsync-filter index 03008242c..4415547d5 100644 --- a/dsf-docker-test-setup-3medic-ttp/medic3/bpe/.rsync-filter +++ b/dsf-docker-test-setup-3medic-ttp/medic3/bpe/.rsync-filter @@ -1,5 +1,4 @@ - .rsync-filter -- last_event/README.md - log/README.md - plugin/README.md - process/README.md diff --git a/dsf-docker-test-setup-3medic-ttp/medic3/bpe/docker-compose.yml b/dsf-docker-test-setup-3medic-ttp/medic3/bpe/docker-compose.yml index 393c9cb8c..d9bbb345b 100755 --- a/dsf-docker-test-setup-3medic-ttp/medic3/bpe/docker-compose.yml +++ b/dsf-docker-test-setup-3medic-ttp/medic3/bpe/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: app: image: registry:5000/highmed/bpe - restart: on-failure + restart: "no" healthcheck: test: ["CMD", "java", "-cp", "dsf_bpe.jar", "org.highmed.dsf.bpe.StatusClient"] interval: 10s @@ -29,9 +29,6 @@ services: - type: bind source: ./log target: /opt/bpe/log - - type: bind - source: ./last_event - target: /opt/bpe/last_event - type: bind source: ./psn target: /opt/bpe/psn @@ -69,7 +66,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" healthcheck: test: ["CMD-SHELL", "pg_isready -U liquibase_user -d bpe"] interval: 10s diff --git a/dsf-docker-test-setup-3medic-ttp/medic3/bpe/last_event/README.md b/dsf-docker-test-setup-3medic-ttp/medic3/bpe/last_event/README.md deleted file mode 100644 index 12fefbfaa..000000000 --- a/dsf-docker-test-setup-3medic-ttp/medic3/bpe/last_event/README.md +++ /dev/null @@ -1 +0,0 @@ -empty directory for last-event time.file \ No newline at end of file diff --git a/dsf-docker-test-setup-3medic-ttp/medic3/fhir/docker-compose.yml b/dsf-docker-test-setup-3medic-ttp/medic3/fhir/docker-compose.yml index 2b3fe15db..16ad69c9e 100755 --- a/dsf-docker-test-setup-3medic-ttp/medic3/fhir/docker-compose.yml +++ b/dsf-docker-test-setup-3medic-ttp/medic3/fhir/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: proxy: image: registry:5000/highmed/fhir_proxy - restart: on-failure + restart: "no" ports: - 80:80 - 443:443 @@ -29,7 +29,7 @@ services: app: image: registry:5000/highmed/fhir - restart: on-failure + restart: "no" healthcheck: test: ["CMD", "java", "-cp", "dsf_fhir.jar", "org.highmed.dsf.fhir.StatusClient"] interval: 10s @@ -84,7 +84,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" healthcheck: test: ["CMD-SHELL", "pg_isready -U liquibase_user -d fhir"] interval: 10s diff --git a/dsf-docker-test-setup-3medic-ttp/ttp/bpe/.rsync-filter b/dsf-docker-test-setup-3medic-ttp/ttp/bpe/.rsync-filter index 03008242c..4415547d5 100644 --- a/dsf-docker-test-setup-3medic-ttp/ttp/bpe/.rsync-filter +++ b/dsf-docker-test-setup-3medic-ttp/ttp/bpe/.rsync-filter @@ -1,5 +1,4 @@ - .rsync-filter -- last_event/README.md - log/README.md - plugin/README.md - process/README.md diff --git a/dsf-docker-test-setup-3medic-ttp/ttp/bpe/docker-compose.yml b/dsf-docker-test-setup-3medic-ttp/ttp/bpe/docker-compose.yml index 698877479..994f5da78 100755 --- a/dsf-docker-test-setup-3medic-ttp/ttp/bpe/docker-compose.yml +++ b/dsf-docker-test-setup-3medic-ttp/ttp/bpe/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: app: image: registry:5000/highmed/bpe - restart: on-failure + restart: "no" healthcheck: test: ["CMD", "java", "-cp", "dsf_bpe.jar", "org.highmed.dsf.bpe.StatusClient"] interval: 10s @@ -29,9 +29,6 @@ services: - type: bind source: ./log target: /opt/bpe/log - - type: bind - source: ./last_event - target: /opt/bpe/last_event - type: bind source: ./psn target: /opt/bpe/psn @@ -75,7 +72,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" healthcheck: test: ["CMD-SHELL", "pg_isready -U liquibase_user -d bpe"] interval: 10s diff --git a/dsf-docker-test-setup-3medic-ttp/ttp/bpe/last_event/README.md b/dsf-docker-test-setup-3medic-ttp/ttp/bpe/last_event/README.md deleted file mode 100644 index 12fefbfaa..000000000 --- a/dsf-docker-test-setup-3medic-ttp/ttp/bpe/last_event/README.md +++ /dev/null @@ -1 +0,0 @@ -empty directory for last-event time.file \ No newline at end of file diff --git a/dsf-docker-test-setup-3medic-ttp/ttp/fhir/docker-compose.yml b/dsf-docker-test-setup-3medic-ttp/ttp/fhir/docker-compose.yml index 3dc7db9b5..1544d826e 100755 --- a/dsf-docker-test-setup-3medic-ttp/ttp/fhir/docker-compose.yml +++ b/dsf-docker-test-setup-3medic-ttp/ttp/fhir/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: proxy: image: registry:5000/highmed/fhir_proxy - restart: on-failure + restart: "no" ports: - 80:80 - 443:443 @@ -29,7 +29,7 @@ services: app: image: registry:5000/highmed/fhir - restart: on-failure + restart: "no" healthcheck: test: ["CMD", "java", "-cp", "dsf_fhir.jar", "org.highmed.dsf.fhir.StatusClient"] interval: 10s @@ -86,7 +86,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" healthcheck: test: ["CMD-SHELL", "pg_isready -U liquibase_user -d fhir"] interval: 10s diff --git a/dsf-docker-test-setup/bpe/docker-compose.yml b/dsf-docker-test-setup/bpe/docker-compose.yml index b5443f843..caa2fdf2e 100755 --- a/dsf-docker-test-setup/bpe/docker-compose.yml +++ b/dsf-docker-test-setup/bpe/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: app: image: highmed/bpe - restart: on-failure + restart: "no" # ports: # - 127.0.0.1:5002:5002 healthcheck: @@ -30,9 +30,6 @@ services: - type: bind source: ./log target: /opt/bpe/log - - type: bind - source: ./last_event - target: /opt/bpe/last_event - type: bind source: ./psn target: /opt/bpe/psn @@ -59,7 +56,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" # ports: # - 127.0.0.1:5432:5432 healthcheck: diff --git a/dsf-docker-test-setup/bpe/last_event/README.md b/dsf-docker-test-setup/bpe/last_event/README.md deleted file mode 100644 index db72590c7..000000000 --- a/dsf-docker-test-setup/bpe/last_event/README.md +++ /dev/null @@ -1 +0,0 @@ -Empty folder for event.time file \ No newline at end of file diff --git a/dsf-docker-test-setup/fhir/docker-compose.yml b/dsf-docker-test-setup/fhir/docker-compose.yml index d9c4b9599..ea33e50cb 100755 --- a/dsf-docker-test-setup/fhir/docker-compose.yml +++ b/dsf-docker-test-setup/fhir/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: proxy: image: highmed/fhir_proxy - restart: on-failure + restart: "no" ports: - 127.0.0.1:80:80 - 127.0.0.1:443:443 @@ -29,7 +29,7 @@ services: app: image: highmed/fhir - restart: on-failure + restart: "no" # ports: # - 127.0.0.1:5001:5001 healthcheck: @@ -81,7 +81,7 @@ services: db: image: postgres:13 - restart: on-failure + restart: "no" # ports: # - 127.0.0.1:5432:5432 healthcheck: diff --git a/dsf-fhir/dsf-fhir-auth/pom.xml b/dsf-fhir/dsf-fhir-auth/pom.xml index c93e33d7b..79c4296cc 100644 --- a/dsf-fhir/dsf-fhir-auth/pom.xml +++ b/dsf-fhir/dsf-fhir-auth/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-fhir-pom - 0.7.0 + 0.8.0 diff --git a/dsf-fhir/dsf-fhir-auth/src/main/java/org/highmed/dsf/fhir/authorization/process/Role.java b/dsf-fhir/dsf-fhir-auth/src/main/java/org/highmed/dsf/fhir/authorization/process/Role.java index 1fa0481f6..6aa04fb2f 100644 --- a/dsf-fhir/dsf-fhir-auth/src/main/java/org/highmed/dsf/fhir/authorization/process/Role.java +++ b/dsf-fhir/dsf-fhir-auth/src/main/java/org/highmed/dsf/fhir/authorization/process/Role.java @@ -118,7 +118,7 @@ private Coding toCoding() .setSystem(ProcessAuthorizationHelper.ORGANIZATION_IDENTIFIER_SYSTEM).setValue(consortiumIdentifier); Extension consortiumExt = new Extension( ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_CONSORTIUM_ROLE_CONSORTIUM) - .setValue(consortium); + .setValue(consortium); Coding role = new Coding(roleSystem, roleCode, null); Extension roleExt = new Extension( diff --git a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml index 58597c8a6..aaccbb44a 100755 --- a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml +++ b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-fhir-pom - 0.7.0 + 0.8.0 diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/HtmlFhirAdapter.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/HtmlFhirAdapter.java index 9ba31befa..bcfa6d5f4 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/HtmlFhirAdapter.java +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/HtmlFhirAdapter.java @@ -3,6 +3,8 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.StringReader; +import java.io.StringWriter; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.net.MalformedURLException; @@ -20,12 +22,21 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.MessageBodyWriter; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; import org.hl7.fhir.r4.model.BaseResource; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Resource; +import com.google.common.base.Objects; + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.annotation.ResourceDef; import ca.uhn.fhir.parser.IParser; @@ -70,8 +81,7 @@ public static interface ServerBaseProvider private static final Pattern JSON_ID_UUID_AND_VERSION_PATTERN = Pattern .compile("\"id\": \"(" + UUID + ")\",\\n([ ]*)\"meta\": \\{\\n([ ]*)\"versionId\": \"([0-9]+)\","); - private static final Pattern CLOSABLE_XML_TAGS = Pattern - .compile("(m?)[\\t ]*<[a-zA-Z0-9]+( value=\".*\"){0,1}(>)"); + private final TransformerFactory transformerFactory = TransformerFactory.newInstance(); private final FhirContext fhirContext; private final ServerBaseProvider serverBaseProvider; @@ -87,6 +97,11 @@ protected HtmlFhirAdapter(FhirContext fhirContext, ServerBaseProvider serverBase this.resourceType = resourceType; } + protected FhirContext getFhirContext() + { + return fhirContext; + } + /* Parsers are not guaranteed to be thread safe */ private IParser getParser(Supplier parser) { @@ -120,11 +135,14 @@ public void writeTo(T t, Class type, Type genericType, Annotation[] annotatio out.write("\n"); out.write("\n"); out.write("\n"); + out.write("\n"); out.write("\n"); out.write("\n"); + out.write("\n"); out.write("DSF" + (uriInfo.getPath() == null || uriInfo.getPath().isEmpty() ? "" : ": ") + uriInfo.getPath() + "\n\n"); - out.write("\n"); + out.write("\n"); out.write("

\n"); out.write("\n"); out.write("Show Help\n"); @@ -181,6 +199,10 @@ public void writeTo(T t, Class type, Type genericType, Annotation[] annotatio out.write("\n"); out.write("\n"); out.write("
\n"); + + if (isHtmlEnabled()) + out.write("\n"); + out.write("\n"); out.write("\n"); out.write("
\n"); @@ -188,6 +210,9 @@ public void writeTo(T t, Class type, Type genericType, Annotation[] annotatio writeXml(t, out); writeJson(t, out); + if (isHtmlEnabled()) + writeHtml(t, out); + out.write(""); out.flush(); } @@ -229,11 +254,7 @@ private URI toURI(String str) private Optional getResourceUrl(T t) throws MalformedURLException { - if (t instanceof Bundle) - return ((Bundle) t).getLink().stream().filter(c -> "self".equals(c.getRelation())).findFirst() - .map(c -> c.getUrl()); - else if (t instanceof Resource && t.getIdElement().getResourceType() != null - && t.getIdElement().getIdPart() != null) + if (t instanceof Resource && t.getIdElement().hasIdPart()) { if (!uriInfo.getPath().contains("_history")) return Optional.of(String.format("%s/%s/%s", serverBaseProvider.getServerBase(), @@ -243,6 +264,9 @@ else if (t instanceof Resource && t.getIdElement().getResourceType() != null t.getIdElement().getResourceType(), t.getIdElement().getIdPart(), t.getIdElement().getVersionIdPart())); } + else if (t instanceof Bundle && !t.getIdElement().hasIdPart()) + return ((Bundle) t).getLink().stream().filter(c -> "self".equals(c.getRelation())).findFirst() + .map(c -> c.getUrl()); else return Optional.empty(); } @@ -254,6 +278,8 @@ private void writeXml(T t, OutputStreamWriter out) throws IOException out.write("
");
 		String content = parser.encodeResourceToString(t);
 
+		content = content.replace("&", "&amp;").replace("'", "&apos;").replace(">", "&gt;")
+				.replace("<", "&lt;").replace(""", "&quot;");
 		content = simplifyXml(content);
 		content = content.replace("<", "<").replace(">", ">");
 
@@ -268,7 +294,10 @@ private void writeXml(T t, OutputStreamWriter out) throws IOException
 		});
 
 		Matcher urlMatcher = URL_PATTERN.matcher(content);
-		content = urlMatcher.replaceAll(result -> "" + result.group() + "");
+		content = urlMatcher.replaceAll(result -> "" + result.group() + "");
 
 		Matcher referenceUuidMatcher = XML_REFERENCE_UUID_PATTERN.matcher(content);
 		content = referenceUuidMatcher.replaceAll(result -> "<reference value=\"\n");
 	}
 
+	private Transformer newTransformer() throws TransformerConfigurationException
+	{
+		Transformer transformer = transformerFactory.newTransformer();
+		transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+		transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+		transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "3");
+		return transformer;
+	}
+
 	private String simplifyXml(String xml)
 	{
-		Matcher matcher = CLOSABLE_XML_TAGS.matcher(xml);
-		return matcher.replaceAll(r -> r.group().replace(r.group(3), "/>"));
+		try
+		{
+			Transformer transformer = newTransformer();
+			StringWriter writer = new StringWriter();
+			transformer.transform(new StreamSource(new StringReader(xml)), new StreamResult(writer));
+			return writer.toString();
+		}
+		catch (TransformerException e)
+		{
+			throw new RuntimeException(e);
+		}
 	}
 
 	private void writeJson(T t, OutputStreamWriter out) throws IOException
@@ -312,22 +360,60 @@ private void writeJson(T t, OutputStreamWriter out) throws IOException
 		out.write("
\n"); } + private void writeHtml(T t, OutputStreamWriter out) throws IOException + { + out.write("
\n"); + doWriteHtml(t, out); + out.write("
\n"); + } + + /** + * Override this method to return true if the HTML tab should be shown. This implies overriding + * {@link #doWriteHtml(BaseResource, OutputStreamWriter)} as well. + * + * @return true if the html tab should be shown, false otherwise (default + * false) + */ + protected boolean isHtmlEnabled() + { + return false; + } + + /** + * Use this method to write output to the html tab. This implies overriding {@link #isHtmlEnabled()} as well. + * + * @param t + * the resource, not null + * @param out + * the outputStreamWriter, not null + * @throws IOException + */ + protected void doWriteHtml(T t, OutputStreamWriter out) throws IOException + { + } + private Optional getResourceName(T t, String uuid) { if (t instanceof Bundle) - return ((Bundle) t).getEntry().stream().filter(c -> - { - if (c.hasResource()) - return uuid.equals(c.getResource().getIdElement().getIdPart()); - else - return uuid.equals(new IdType(c.getResponse().getLocation()).getIdPart()); - }).map(c -> - { - if (c.hasResource()) - return c.getResource().getClass().getAnnotation(ResourceDef.class).name(); - else - return new IdType(c.getResponse().getLocation()).getResourceType(); - }).findFirst(); + { + // if persistent Bundle resource + if (Objects.equal(uuid, t.getIdElement().getIdPart())) + return Optional.of(t.getClass().getAnnotation(ResourceDef.class).name()); + else + return ((Bundle) t).getEntry().stream().filter(c -> + { + if (c.hasResource()) + return uuid.equals(c.getResource().getIdElement().getIdPart()); + else + return uuid.equals(new IdType(c.getResponse().getLocation()).getIdPart()); + }).map(c -> + { + if (c.hasResource()) + return c.getResource().getClass().getAnnotation(ResourceDef.class).name(); + else + return new IdType(c.getResponse().getLocation()).getResourceType(); + }).findFirst(); + } else if (t instanceof Resource) return Optional.of(t.getClass().getAnnotation(ResourceDef.class).name()); else diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireHtmlFhirAdapter.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireHtmlFhirAdapter.java new file mode 100644 index 000000000..d5b2bffac --- /dev/null +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireHtmlFhirAdapter.java @@ -0,0 +1,16 @@ +package org.highmed.dsf.fhir.adapter; + +import javax.ws.rs.ext.Provider; + +import org.hl7.fhir.r4.model.Questionnaire; + +import ca.uhn.fhir.context.FhirContext; + +@Provider +public class QuestionnaireHtmlFhirAdapter extends HtmlFhirAdapter +{ + public QuestionnaireHtmlFhirAdapter(FhirContext fhirContext, ServerBaseProvider serverBaseProvider) + { + super(fhirContext, serverBaseProvider, Questionnaire.class); + } +} \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireJsonFhirAdapter.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireJsonFhirAdapter.java new file mode 100644 index 000000000..c530b8e68 --- /dev/null +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireJsonFhirAdapter.java @@ -0,0 +1,16 @@ +package org.highmed.dsf.fhir.adapter; + +import javax.ws.rs.ext.Provider; + +import org.hl7.fhir.r4.model.Questionnaire; + +import ca.uhn.fhir.context.FhirContext; + +@Provider +public class QuestionnaireJsonFhirAdapter extends JsonFhirAdapter +{ + public QuestionnaireJsonFhirAdapter(FhirContext fhirContext) + { + super(fhirContext, Questionnaire.class); + } +} diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireResponseHtmlFhirAdapter.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireResponseHtmlFhirAdapter.java new file mode 100644 index 000000000..c87c6fff1 --- /dev/null +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireResponseHtmlFhirAdapter.java @@ -0,0 +1,209 @@ +package org.highmed.dsf.fhir.adapter; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.ws.rs.ext.Provider; + +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.TimeType; +import org.hl7.fhir.r4.model.Type; +import org.hl7.fhir.r4.model.UriType; + +import ca.uhn.fhir.context.FhirContext; + +@Provider +public class QuestionnaireResponseHtmlFhirAdapter extends HtmlFhirAdapter +{ + private static final String CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY = "business-key"; + private static final String CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID = "user-task-id"; + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + private static final SimpleDateFormat DATE_TIME_DISPLAY_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss"); + + public QuestionnaireResponseHtmlFhirAdapter(FhirContext fhirContext, ServerBaseProvider serverBaseProvider) + { + super(fhirContext, serverBaseProvider, QuestionnaireResponse.class); + } + + @Override + protected boolean isHtmlEnabled() + { + return true; + } + + @Override + protected void doWriteHtml(QuestionnaireResponse questionnaireResponse, OutputStreamWriter out) throws IOException + { + boolean isCompleted = QuestionnaireResponse.QuestionnaireResponseStatus.COMPLETED + .equals(questionnaireResponse.getStatus()); + out.write("
"); + out.write("
\n"); + out.write("
\n"); + + out.write("
"); + out.write("\n"); + out.write("Info\n"); + out.write("\"/>\n"); + out.write("\n"); + out.write("
\n"); + + String urlVersion = questionnaireResponse.getQuestionnaire(); + String[] urlVersionSplit = urlVersion.split("\\|"); + String href = "/fhir/Questionnaire?url=" + urlVersionSplit[0] + "&version=" + urlVersionSplit[1]; + + out.write("
"); + out.write("

\n"); + out.write("This QuestionnaireResponse answers the Questionnaire:
" + + urlVersion + ""); + out.write("

\n"); + out.write("
    \n"); + out.write("
  • State: " + questionnaireResponse.getStatus().getDisplay() + "
  • \n"); + out.write("
  • Process instance-id: " + getProcessInstanceId(questionnaireResponse) + "
  • \n"); + + String lastUpdated = DATE_TIME_DISPLAY_FORMAT.format(questionnaireResponse.getMeta().getLastUpdated()); + if (isCompleted) + { + out.write("
  • Completion date: " + lastUpdated + "
  • \n"); + } + else + { + out.write("
  • Creation date: " + lastUpdated + "
  • \n"); + } + + out.write("
\n"); + out.write("
\n"); + out.write("
\n"); + + out.write("
\n"); + + for (QuestionnaireResponse.QuestionnaireResponseItemComponent item : questionnaireResponse.getItem()) + { + writeRow(item, isCompleted, out); + } + + out.write("
\n"); + out.write("\n"); + out.write("
\n"); + out.write("
\n"); + out.write("
\n"); + } + + private String getProcessInstanceId(QuestionnaireResponse questionnaireResponse) + { + return questionnaireResponse.getItem().stream() + .filter(i -> CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY.equals(i.getLinkId())) + .flatMap(i -> i.getAnswer().stream()).map(a -> ((StringType) a.getValue()).getValue()).findFirst() + .orElse("unknown"); + } + + private void writeRow(QuestionnaireResponse.QuestionnaireResponseItemComponent item, boolean isCompleted, + OutputStreamWriter out) throws IOException + { + String linkId = item.getLinkId(); + String style = display(linkId) ? "" : "style=\"display:none;\""; + + out.write("
\n"); + out.write("\n"); + writeFormInput(item.getAnswerFirstRep(), linkId, isCompleted, out); + out.write("
    \n"); + out.write("
\n"); + out.write("
\n"); + } + + private boolean display(String linkId) + { + return !(CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY.equals(linkId) + || CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID.equals(linkId)); + } + + private void writeFormInput(QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent answerPlaceholder, + String linkId, boolean isCompleted, OutputStreamWriter out) throws IOException + { + Type type = answerPlaceholder.getValue(); + + if (type instanceof StringType) + { + String value = ((StringType) type).getValue(); + out.write("\n"); + } + else if (type instanceof IntegerType) + { + String value = String.valueOf(((IntegerType) type).getValue()); + out.write("\n"); + } + else if (type instanceof DecimalType) + { + String value = String.valueOf(((DecimalType) type).getValue()); + out.write("\n"); + } + else if (type instanceof BooleanType) + { + boolean valueIsTrue = ((BooleanType) type).getValue(); + + out.write("
\n"); + out.write("\n"); + out.write("\n"); + out.write("
\n"); + } + else if (type instanceof DateType) + { + Date value = ((DateType) type).getValue(); + String date = DATE_FORMAT.format(value); + + out.write("\n"); + } + else if (type instanceof TimeType) + { + String value = ((TimeType) type).getValue(); + out.write("\n"); + } + else if (type instanceof DateTimeType) + { + Date value = ((DateTimeType) type).getValue(); + String dateTime = DATE_TIME_FORMAT.format(value); + + out.write("\n"); + } + else if (type instanceof UriType) + { + String value = ((UriType) type).getValue(); + out.write("\n"); + } + else if (type instanceof Reference) + { + String value = ((Reference) type).getReference(); + out.write("\n"); + } + else + { + throw new RuntimeException("Answer type '" + ((type != null) ? type.getClass().getName() : "null") + + "' in QuestionnaireResponse.item is not supported"); + } + } +} diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireResponseJsonFhirAdapter.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireResponseJsonFhirAdapter.java new file mode 100644 index 000000000..434284d95 --- /dev/null +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireResponseJsonFhirAdapter.java @@ -0,0 +1,16 @@ +package org.highmed.dsf.fhir.adapter; + +import javax.ws.rs.ext.Provider; + +import org.hl7.fhir.r4.model.QuestionnaireResponse; + +import ca.uhn.fhir.context.FhirContext; + +@Provider +public class QuestionnaireResponseJsonFhirAdapter extends JsonFhirAdapter +{ + public QuestionnaireResponseJsonFhirAdapter(FhirContext fhirContext) + { + super(fhirContext, QuestionnaireResponse.class); + } +} diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireResponseXmlFhirAdapter.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireResponseXmlFhirAdapter.java new file mode 100644 index 000000000..db5b0ec6e --- /dev/null +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireResponseXmlFhirAdapter.java @@ -0,0 +1,16 @@ +package org.highmed.dsf.fhir.adapter; + +import javax.ws.rs.ext.Provider; + +import org.hl7.fhir.r4.model.QuestionnaireResponse; + +import ca.uhn.fhir.context.FhirContext; + +@Provider +public class QuestionnaireResponseXmlFhirAdapter extends XmlFhirAdapter +{ + public QuestionnaireResponseXmlFhirAdapter(FhirContext fhirContext) + { + super(fhirContext, QuestionnaireResponse.class); + } +} diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireXmlFhirAdapter.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireXmlFhirAdapter.java new file mode 100644 index 000000000..7d78c8783 --- /dev/null +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/adapter/QuestionnaireXmlFhirAdapter.java @@ -0,0 +1,16 @@ +package org.highmed.dsf.fhir.adapter; + +import javax.ws.rs.ext.Provider; + +import org.hl7.fhir.r4.model.Questionnaire; + +import ca.uhn.fhir.context.FhirContext; + +@Provider +public class QuestionnaireXmlFhirAdapter extends XmlFhirAdapter +{ + public QuestionnaireXmlFhirAdapter(FhirContext fhirContext) + { + super(fhirContext, Questionnaire.class); + } +} diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/service/ReferenceExtractor.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/service/ReferenceExtractor.java index 323f6bf29..876a6e365 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/service/ReferenceExtractor.java +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/service/ReferenceExtractor.java @@ -21,6 +21,8 @@ import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.ResearchStudy; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StructureDefinition; @@ -73,6 +75,10 @@ public interface ReferenceExtractor Stream getReferences(Provenance resource); + Stream getReferences(Questionnaire resource); + + Stream getReferences(QuestionnaireResponse resource); + Stream getReferences(ResearchStudy resource); Stream getReferences(StructureDefinition resource); diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/service/ReferenceExtractorImpl.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/service/ReferenceExtractorImpl.java index 114e0897d..ef11427bf 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/service/ReferenceExtractorImpl.java +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/org/highmed/dsf/fhir/service/ReferenceExtractorImpl.java @@ -10,6 +10,7 @@ import org.hl7.fhir.r4.model.Attachment; import org.hl7.fhir.r4.model.BackboneElement; import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.CarePlan; import org.hl7.fhir.r4.model.CareTeam; import org.hl7.fhir.r4.model.ClaimResponse; import org.hl7.fhir.r4.model.CodeSystem; @@ -38,6 +39,7 @@ import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupPopulationComponent; import org.hl7.fhir.r4.model.Medication; import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.ObservationDefinition; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Organization; @@ -49,14 +51,18 @@ import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.Practitioner.PractitionerQualificationComponent; import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Procedure; import org.hl7.fhir.r4.model.Provenance; import org.hl7.fhir.r4.model.Provenance.ProvenanceAgentComponent; import org.hl7.fhir.r4.model.Provenance.ProvenanceEntityComponent; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.RelatedArtifact; import org.hl7.fhir.r4.model.RelatedPerson; import org.hl7.fhir.r4.model.ResearchStudy; import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ServiceRequest; import org.hl7.fhir.r4.model.SpecimenDefinition; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.Subscription; @@ -365,6 +371,10 @@ else if (resource instanceof PractitionerRole) return getReferences((PractitionerRole) resource); else if (resource instanceof Provenance) return getReferences((Provenance) resource); + else if (resource instanceof Questionnaire) + return getReferences((Questionnaire) resource); + else if (resource instanceof QuestionnaireResponse) + return getReferences((QuestionnaireResponse) resource); else if (resource instanceof ResearchStudy) return getReferences((ResearchStudy) resource); else if (resource instanceof StructureDefinition) @@ -769,6 +779,69 @@ public Stream getReferences(Provenance resource) return concat(targets, location, agentsWho, agentsOnBehalfOf, entitiesWhat, extensionReferences); } + @Override + public Stream getReferences(Questionnaire resource) + { + if (resource == null) + return Stream.empty(); + + var enableWhen = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, + Questionnaire.QuestionnaireItemComponent::hasEnableWhen, + Questionnaire.QuestionnaireItemComponent::getEnableWhen, + Questionnaire.QuestionnaireItemEnableWhenComponent::hasAnswerReference, + Questionnaire.QuestionnaireItemEnableWhenComponent::getAnswerReference, + "Questionnaire.item.enableWhen.answerReference"); + + var answerOption = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, + Questionnaire.QuestionnaireItemComponent::hasAnswerOption, + Questionnaire.QuestionnaireItemComponent::getAnswerOption, + Questionnaire.QuestionnaireItemAnswerOptionComponent::hasValueReference, + Questionnaire.QuestionnaireItemAnswerOptionComponent::getValueReference, + "Questionnaire.item.answerOption.valueReference"); + + var initial = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, + Questionnaire.QuestionnaireItemComponent::hasInitial, + Questionnaire.QuestionnaireItemComponent::getInitial, + Questionnaire.QuestionnaireItemInitialComponent::hasValueReference, + Questionnaire.QuestionnaireItemInitialComponent::getValueReference, + "Questionnaire.item.initial.valueReference"); + + var extensionReferences = getExtensionReferences(resource); + + return concat(enableWhen, answerOption, initial, extensionReferences); + } + + @Override + public Stream getReferences(QuestionnaireResponse resource) + { + if (resource == null) + return Stream.empty(); + + var author = getReference(resource, QuestionnaireResponse::hasAuthor, QuestionnaireResponse::getAuthor, + "QuestionnaireResponse.author", Device.class, Organization.class, Patient.class, Practitioner.class, + PractitionerRole.class, RelatedPerson.class); + + var basedOn = getReferences(resource, QuestionnaireResponse::hasBasedOn, QuestionnaireResponse::getBasedOn, + "QuestionnaireResponse.basedOn", CarePlan.class, ServiceRequest.class); + + var encounter = getReference(resource, QuestionnaireResponse::hasEncounter, QuestionnaireResponse::getEncounter, + "QuestionnaireResponse.encounter", Encounter.class); + + var partOf = getReferences(resource, QuestionnaireResponse::hasPartOf, QuestionnaireResponse::getPartOf, + "QuestionnaireResponse.partOf", Observation.class, Procedure.class); + + var source = getReference(resource, QuestionnaireResponse::hasSource, QuestionnaireResponse::getSource, + "QuestionnaireResponse.source", Patient.class, Practitioner.class, PractitionerRole.class, + RelatedPerson.class); + + var subject = getReference(resource, QuestionnaireResponse::hasSubject, QuestionnaireResponse::getSubject, + "QuestionnaireResponse.subject"); + + var extensionReferences = getExtensionReferences(resource); + + return concat(author, basedOn, encounter, partOf, source, subject, extensionReferences); + } + @Override public Stream getReferences(ResearchStudy resource) { diff --git a/dsf-fhir/dsf-fhir-server-jetty/pom.xml b/dsf-fhir/dsf-fhir-server-jetty/pom.xml index 55f8ff78d..49d5c86a5 100755 --- a/dsf-fhir/dsf-fhir-server-jetty/pom.xml +++ b/dsf-fhir/dsf-fhir-server-jetty/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-fhir-pom - 0.7.0 + 0.8.0 @@ -51,11 +51,6 @@ javax-websocket-server-impl - - javax.mail - mail - - org.slf4j jul-to-slf4j diff --git a/dsf-fhir/dsf-fhir-server/pom.xml b/dsf-fhir/dsf-fhir-server/pom.xml index 15329c722..a3f937b7f 100755 --- a/dsf-fhir/dsf-fhir-server/pom.xml +++ b/dsf-fhir/dsf-fhir-server/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-fhir-pom - 0.7.0 + 0.8.0 @@ -134,6 +134,11 @@ org.postgresql postgresql + + + com.sun.mail + jakarta.mail + org.highmed.dsf @@ -231,7 +236,6 @@ io.fabric8 docker-maven-plugin - 0.34.1 true diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/AbstractMetaTagAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/AbstractMetaTagAuthorizationRule.java index 95e18fe5a..6c31e0d7e 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/AbstractMetaTagAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/AbstractMetaTagAuthorizationRule.java @@ -56,7 +56,7 @@ public final Optional reasonCreateAllowed(Connection connection, User us { if (isLocalUser(user)) { - Optional errors = newResourceOk(connection, user, newResource); + Optional errors = newResourceOkForCreate(connection, user, newResource); if (errors.isEmpty()) { if (!resourceExists(connection, newResource)) @@ -86,7 +86,7 @@ public final Optional reasonCreateAllowed(Connection connection, User us protected abstract boolean resourceExists(Connection connection, R newResource); - protected abstract Optional newResourceOk(Connection connection, User user, R newResource); + protected abstract Optional newResourceOkForCreate(Connection connection, User user, R newResource); @Override public final Optional reasonReadAllowed(Connection connection, User user, R existingResource) @@ -123,12 +123,14 @@ public final Optional reasonReadAllowed(Connection connection, User user } } + protected abstract Optional newResourceOkForUpdate(Connection connection, User user, R newResource); + @Override public final Optional reasonUpdateAllowed(Connection connection, User user, R oldResource, R newResource) { if (isLocalUser(user)) { - Optional errors = newResourceOk(connection, user, newResource); + Optional errors = newResourceOkForUpdate(connection, user, newResource); if (errors.isEmpty()) { if (modificationsOk(connection, oldResource, newResource)) @@ -145,7 +147,7 @@ public final Optional reasonUpdateAllowed(Connection connection, User us } else { - logger.warn("Update of {} unauthorized, ", resourceTypeName, errors.get()); + logger.warn("Update of {} unauthorized, {}", resourceTypeName, errors.get()); return Optional.empty(); } } @@ -158,7 +160,7 @@ public final Optional reasonUpdateAllowed(Connection connection, User us /** * No need to check if the new resource is valid, will be checked by - * {@link #newResourceOk(Connection, User, Resource)} + * {@link #newResourceOkForUpdate(Connection, User, Resource)} * * @param connection * not null diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ActivityDefinitionAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ActivityDefinitionAuthorizationRule.java index a6b0602ea..fbf507cd0 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ActivityDefinitionAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ActivityDefinitionAuthorizationRule.java @@ -48,7 +48,19 @@ public void afterPropertiesSet() throws Exception Objects.requireNonNull(processAuthorizationHelper, "processAuthorizationHelper"); } - protected Optional newResourceOk(Connection connection, User user, ActivityDefinition newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, ActivityDefinition newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, ActivityDefinition newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, ActivityDefinition newResource) { List errors = new ArrayList(); @@ -93,6 +105,7 @@ protected Optional newResourceOk(Connection connection, User user, Activ return Optional.of(errors.stream().collect(Collectors.joining(", "))); } + @Override protected boolean resourceExists(Connection connection, ActivityDefinition newResource) { try @@ -108,6 +121,7 @@ protected boolean resourceExists(Connection connection, ActivityDefinition newRe } } + @Override protected boolean modificationsOk(Connection connection, ActivityDefinition oldResource, ActivityDefinition newResource) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/BinaryAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/BinaryAuthorizationRule.java index 9d59d435b..59db3f1f6 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/BinaryAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/BinaryAuthorizationRule.java @@ -35,7 +35,19 @@ public BinaryAuthorizationRule(DaoProvider daoProvider, String serverBase, Refer .collect(Collectors.toMap(AuthorizationRule::getResourceType, Function.identity())); } - protected Optional newResourceOk(Connection connection, User user, Binary newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Binary newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Binary newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Binary newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/BundleAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/BundleAuthorizationRule.java index 2629db9ff..70c47e47b 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/BundleAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/BundleAuthorizationRule.java @@ -25,7 +25,19 @@ public BundleAuthorizationRule(DaoProvider daoProvider, String serverBase, Refer parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, Bundle newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Bundle newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Bundle newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Bundle newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/CodeSystemAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/CodeSystemAuthorizationRule.java index 37b8e5da6..b1df67b73 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/CodeSystemAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/CodeSystemAuthorizationRule.java @@ -32,7 +32,19 @@ public CodeSystemAuthorizationRule(DaoProvider daoProvider, String serverBase, R parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, CodeSystem newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, CodeSystem newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, CodeSystem newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, CodeSystem newResource) { List errors = new ArrayList(); @@ -69,6 +81,7 @@ protected Optional newResourceOk(Connection connection, User user, CodeS return Optional.of(errors.stream().collect(Collectors.joining(", "))); } + @Override protected boolean resourceExists(Connection connection, CodeSystem newResource) { try diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/DocumentReferenceAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/DocumentReferenceAuthorizationRule.java index bc9240662..7730cf26a 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/DocumentReferenceAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/DocumentReferenceAuthorizationRule.java @@ -27,7 +27,18 @@ public DocumentReferenceAuthorizationRule(DaoProvider daoProvider, String server } @Override - protected Optional newResourceOk(Connection connection, User user, DocumentReference newResource) + protected Optional newResourceOkForCreate(Connection connection, User user, DocumentReference newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, DocumentReference newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, DocumentReference newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/EndpointAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/EndpointAuthorizationRule.java index e4cebf3cc..6a44812db 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/EndpointAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/EndpointAuthorizationRule.java @@ -39,7 +39,19 @@ public EndpointAuthorizationRule(DaoProvider daoProvider, String serverBase, Ref parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, Endpoint newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Endpoint newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Endpoint newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Endpoint newResource) { List errors = new ArrayList(); @@ -81,6 +93,7 @@ protected Optional newResourceOk(Connection connection, User user, Endpo return Optional.of(errors.stream().collect(Collectors.joining(", "))); } + @Override protected boolean resourceExists(Connection connection, Endpoint newResource) { String identifierValue = newResource.getIdentifier().stream() diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/GroupAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/GroupAuthorizationRule.java index 553544eae..8cb3a6746 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/GroupAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/GroupAuthorizationRule.java @@ -26,7 +26,18 @@ public GroupAuthorizationRule(DaoProvider daoProvider, String serverBase, Refere } @Override - protected Optional newResourceOk(Connection connection, User user, Group newResource) + protected Optional newResourceOkForCreate(Connection connection, User user, Group newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Group newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Group newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/HealthcareServiceAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/HealthcareServiceAuthorizationRule.java index 4cd294549..0452dbb57 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/HealthcareServiceAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/HealthcareServiceAuthorizationRule.java @@ -26,7 +26,19 @@ public HealthcareServiceAuthorizationRule(DaoProvider daoProvider, String server readAccessHelper, parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, HealthcareService newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, HealthcareService newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, HealthcareService newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, HealthcareService newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/LibraryAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/LibraryAuthorizationRule.java index 132dc8280..dc80cd705 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/LibraryAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/LibraryAuthorizationRule.java @@ -25,7 +25,19 @@ public LibraryAuthorizationRule(DaoProvider daoProvider, String serverBase, Refe parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, Library newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Library newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Library newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Library newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/LocationAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/LocationAuthorizationRule.java index 7e35708e6..7686759e2 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/LocationAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/LocationAuthorizationRule.java @@ -25,7 +25,19 @@ public LocationAuthorizationRule(DaoProvider daoProvider, String serverBase, Ref parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, Location newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Location newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Location newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Location newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/MeasureAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/MeasureAuthorizationRule.java index c77450d38..339c5160b 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/MeasureAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/MeasureAuthorizationRule.java @@ -25,7 +25,19 @@ public MeasureAuthorizationRule(DaoProvider daoProvider, String serverBase, Refe parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, Measure newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Measure newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Measure newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Measure newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/MeasureReportAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/MeasureReportAuthorizationRule.java index 4b26f4fe1..3310d1317 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/MeasureReportAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/MeasureReportAuthorizationRule.java @@ -25,7 +25,19 @@ public MeasureReportAuthorizationRule(DaoProvider daoProvider, String serverBase parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, MeasureReport newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, MeasureReport newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, MeasureReport newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, MeasureReport newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/NamingSystemAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/NamingSystemAuthorizationRule.java index 63230d215..7bf893148 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/NamingSystemAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/NamingSystemAuthorizationRule.java @@ -35,7 +35,19 @@ public NamingSystemAuthorizationRule(DaoProvider daoProvider, String serverBase, parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, NamingSystem newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, NamingSystem newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, NamingSystem newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, NamingSystem newResource) { List errors = new ArrayList(); @@ -84,6 +96,7 @@ private boolean hasOnlyUniqueUriUniqueIds(NamingSystem newResource) return uriCount == distinctUriCount; } + @Override protected boolean resourceExists(Connection connection, NamingSystem newResource) { try diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRule.java index 262453283..b9f78ca38 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/OrganizationAffiliationAuthorizationRule.java @@ -35,7 +35,21 @@ public OrganizationAffiliationAuthorizationRule(DaoProvider daoProvider, String readAccessHelper, parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, OrganizationAffiliation newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, + OrganizationAffiliation newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, + OrganizationAffiliation newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, OrganizationAffiliation newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/OrganizationAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/OrganizationAuthorizationRule.java index 866cb6d70..9f7828bfc 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/OrganizationAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/OrganizationAuthorizationRule.java @@ -42,7 +42,19 @@ public OrganizationAuthorizationRule(DaoProvider daoProvider, String serverBase, parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, Organization newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Organization newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Organization newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Organization newResource) { List errors = new ArrayList(); @@ -86,6 +98,7 @@ protected Optional newResourceOk(Connection connection, User user, Organ return Optional.of(errors.stream().collect(Collectors.joining(", "))); } + @Override protected boolean resourceExists(Connection connection, Organization newResource) { Identifier organizationIdentifier = newResource.getIdentifier().stream() diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PatientAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PatientAuthorizationRule.java index f5e88d725..1bb86a868 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PatientAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PatientAuthorizationRule.java @@ -25,7 +25,19 @@ public PatientAuthorizationRule(DaoProvider daoProvider, String serverBase, Refe parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, Patient newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Patient newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Patient newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Patient newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PractitionerAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PractitionerAuthorizationRule.java index dcb3362a3..8791f9354 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PractitionerAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PractitionerAuthorizationRule.java @@ -25,7 +25,19 @@ public PractitionerAuthorizationRule(DaoProvider daoProvider, String serverBase, parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, Practitioner newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Practitioner newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Practitioner newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Practitioner newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PractitionerRoleAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PractitionerRoleAuthorizationRule.java index 101e6cc60..ea0bc4e9b 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PractitionerRoleAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/PractitionerRoleAuthorizationRule.java @@ -26,7 +26,19 @@ public PractitionerRoleAuthorizationRule(DaoProvider daoProvider, String serverB readAccessHelper, parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, PractitionerRole newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, PractitionerRole newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, PractitionerRole newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, PractitionerRole newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ProvenanceAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ProvenanceAuthorizationRule.java index a8411f2d0..6c29e1c12 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ProvenanceAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ProvenanceAuthorizationRule.java @@ -25,7 +25,19 @@ public ProvenanceAuthorizationRule(DaoProvider daoProvider, String serverBase, R parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, Provenance newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Provenance newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Provenance newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Provenance newResource) { List errors = new ArrayList(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/QuestionnaireAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/QuestionnaireAuthorizationRule.java new file mode 100644 index 000000000..cec6f4ffa --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/QuestionnaireAuthorizationRule.java @@ -0,0 +1,68 @@ +package org.highmed.dsf.fhir.authorization; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.highmed.dsf.fhir.authentication.OrganizationProvider; +import org.highmed.dsf.fhir.authentication.User; +import org.highmed.dsf.fhir.authorization.read.ReadAccessHelper; +import org.highmed.dsf.fhir.dao.QuestionnaireDao; +import org.highmed.dsf.fhir.dao.provider.DaoProvider; +import org.highmed.dsf.fhir.help.ParameterConverter; +import org.highmed.dsf.fhir.service.ReferenceResolver; +import org.hl7.fhir.r4.model.Questionnaire; + +public class QuestionnaireAuthorizationRule extends AbstractMetaTagAuthorizationRule +{ + public QuestionnaireAuthorizationRule(DaoProvider daoProvider, String serverBase, + ReferenceResolver referenceResolver, OrganizationProvider organizationProvider, + ReadAccessHelper readAccessHelper, ParameterConverter parameterConverter) + { + super(Questionnaire.class, daoProvider, serverBase, referenceResolver, organizationProvider, readAccessHelper, + parameterConverter); + } + + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Questionnaire newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Questionnaire newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Questionnaire newResource) + { + List errors = new ArrayList(); + + if (!hasValidReadAccessTag(connection, newResource)) + { + errors.add("Questionnaire is missing valid read access tag"); + } + + if (errors.isEmpty()) + return Optional.empty(); + else + return Optional.of(errors.stream().collect(Collectors.joining(", "))); + } + + @Override + protected boolean resourceExists(Connection connection, Questionnaire newResource) + { + // no unique criteria for Questionnaire + return false; + } + + @Override + protected boolean modificationsOk(Connection connection, Questionnaire oldResource, Questionnaire newResource) + { + // no unique criteria for Questionnaire + return true; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRule.java new file mode 100644 index 000000000..3fd1afcc0 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/QuestionnaireResponseAuthorizationRule.java @@ -0,0 +1,118 @@ +package org.highmed.dsf.fhir.authorization; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.highmed.dsf.fhir.authentication.OrganizationProvider; +import org.highmed.dsf.fhir.authentication.User; +import org.highmed.dsf.fhir.authorization.read.ReadAccessHelper; +import org.highmed.dsf.fhir.dao.QuestionnaireResponseDao; +import org.highmed.dsf.fhir.dao.provider.DaoProvider; +import org.highmed.dsf.fhir.help.ParameterConverter; +import org.highmed.dsf.fhir.service.ReferenceResolver; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class QuestionnaireResponseAuthorizationRule + extends AbstractMetaTagAuthorizationRule +{ + private static final Logger logger = LoggerFactory.getLogger(QuestionnaireResponseAuthorizationRule.class); + + public QuestionnaireResponseAuthorizationRule(DaoProvider daoProvider, String serverBase, + ReferenceResolver referenceResolver, OrganizationProvider organizationProvider, + ReadAccessHelper readAccessHelper, ParameterConverter parameterConverter) + { + super(QuestionnaireResponse.class, daoProvider, serverBase, referenceResolver, organizationProvider, + readAccessHelper, parameterConverter); + } + + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, + QuestionnaireResponse newResource) + { + List errors = new ArrayList(); + + if (!hasValidReadAccessTag(connection, newResource)) + { + errors.add("QuestionnaireResponse is missing valid read access tag"); + } + + if (newResource.hasStatus()) + { + if (!QuestionnaireResponseStatus.INPROGRESS.equals(newResource.getStatus())) + { + errors.add("QuestionnaireResponse.status not in-progress and version 1"); + } + } + else + { + errors.add("QuestionnaireResponse.status missing"); + } + + if (errors.isEmpty()) + return Optional.empty(); + else + return Optional.of(errors.stream().collect(Collectors.joining(", "))); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, + QuestionnaireResponse newResource) + { + List errors = new ArrayList(); + + if (!hasValidReadAccessTag(connection, newResource)) + { + errors.add("QuestionnaireResponse is missing valid read access tag"); + } + + if (newResource.hasStatus()) + { + if (!EnumSet.of(QuestionnaireResponseStatus.COMPLETED, QuestionnaireResponseStatus.STOPPED) + .contains(newResource.getStatus())) + { + errors.add("QuestionnaireResponse.status not (completed or stopped) and version 2"); + } + } + else + { + errors.add("QuestionnaireResponse.status missing"); + } + + if (errors.isEmpty()) + return Optional.empty(); + else + return Optional.of(errors.stream().collect(Collectors.joining(", "))); + } + + @Override + protected boolean resourceExists(Connection connection, QuestionnaireResponse newResource) + { + // no unique criteria for QuestionnaireResponse + return false; + } + + @Override + protected boolean modificationsOk(Connection connection, QuestionnaireResponse oldResource, + QuestionnaireResponse newResource) + { + boolean statusModificationOk = QuestionnaireResponseStatus.INPROGRESS.equals(oldResource.getStatus()) + && (QuestionnaireResponseStatus.COMPLETED.equals(newResource.getStatus()) + || QuestionnaireResponseStatus.STOPPED.equals(newResource.getStatus())); + + if (!statusModificationOk) + logger.warn( + "Modifications only allowed if status changes from '{}' to '{}', current status of old resource is '{}' and of new resource is '{}'", + QuestionnaireResponseStatus.INPROGRESS, + QuestionnaireResponseStatus.COMPLETED + "|" + QuestionnaireResponseStatus.STOPPED, + oldResource.getStatus(), newResource.getStatus()); + + return statusModificationOk; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ResearchStudyAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ResearchStudyAuthorizationRule.java index 158a1a525..799670552 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ResearchStudyAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ResearchStudyAuthorizationRule.java @@ -52,7 +52,19 @@ public ResearchStudyAuthorizationRule(DaoProvider daoProvider, String serverBase parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, ResearchStudy newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, ResearchStudy newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, ResearchStudy newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, ResearchStudy newResource) { List errors = new ArrayList(); @@ -222,6 +234,7 @@ private boolean practitionerRoleExists(Connection connection, User user, IdType } } + @Override protected boolean resourceExists(Connection connection, ResearchStudy newResource) { if (newResource.getMeta().hasProfile(HIGHMED_RESEARCH_STUDY)) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/StructureDefinitionAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/StructureDefinitionAuthorizationRule.java index 2864170f6..ca6f9386d 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/StructureDefinitionAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/StructureDefinitionAuthorizationRule.java @@ -33,7 +33,19 @@ public StructureDefinitionAuthorizationRule(DaoProvider daoProvider, String serv readAccessHelper, parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, StructureDefinition newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, StructureDefinition newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, StructureDefinition newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, StructureDefinition newResource) { List errors = new ArrayList(); @@ -70,6 +82,7 @@ protected Optional newResourceOk(Connection connection, User user, Struc return Optional.of(errors.stream().collect(Collectors.joining(", "))); } + @Override protected boolean resourceExists(Connection connection, StructureDefinition newResource) { try diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/SubscriptionAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/SubscriptionAuthorizationRule.java index 3b1eba4f2..15b2c137d 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/SubscriptionAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/SubscriptionAuthorizationRule.java @@ -41,7 +41,19 @@ public SubscriptionAuthorizationRule(DaoProvider daoProvider, String serverBase, parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, Subscription newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, Subscription newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, Subscription newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, Subscription newResource) { List errors = new ArrayList(); @@ -111,6 +123,7 @@ protected Optional newResourceOk(Connection connection, User user, Subsc return Optional.of(errors.stream().collect(Collectors.joining(", "))); } + @Override protected boolean resourceExists(Connection connection, Subscription newResource) { Map> queryParameters = Map.of("criteria", diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ValueSetAuthorizationRule.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ValueSetAuthorizationRule.java index 39019f0b3..50175eb19 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ValueSetAuthorizationRule.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/authorization/ValueSetAuthorizationRule.java @@ -32,7 +32,19 @@ public ValueSetAuthorizationRule(DaoProvider daoProvider, String serverBase, Ref parameterConverter); } - protected Optional newResourceOk(Connection connection, User user, ValueSet newResource) + @Override + protected Optional newResourceOkForCreate(Connection connection, User user, ValueSet newResource) + { + return newResourceOk(connection, user, newResource); + } + + @Override + protected Optional newResourceOkForUpdate(Connection connection, User user, ValueSet newResource) + { + return newResourceOk(connection, user, newResource); + } + + private Optional newResourceOk(Connection connection, User user, ValueSet newResource) { List errors = new ArrayList(); @@ -69,6 +81,7 @@ protected Optional newResourceOk(Connection connection, User user, Value return Optional.of(errors.stream().collect(Collectors.joining(", "))); } + @Override protected boolean resourceExists(Connection connection, ValueSet newResource) { try diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/QuestionnaireDao.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/QuestionnaireDao.java new file mode 100755 index 000000000..8f12d14c2 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/QuestionnaireDao.java @@ -0,0 +1,7 @@ +package org.highmed.dsf.fhir.dao; + +import org.hl7.fhir.r4.model.Questionnaire; + +public interface QuestionnaireDao extends ResourceDao, ReadByUrlDao +{ +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/QuestionnaireResponseDao.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/QuestionnaireResponseDao.java new file mode 100755 index 000000000..c1fdd24a6 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/QuestionnaireResponseDao.java @@ -0,0 +1,7 @@ +package org.highmed.dsf.fhir.dao; + +import org.hl7.fhir.r4.model.QuestionnaireResponse; + +public interface QuestionnaireResponseDao extends ResourceDao +{ +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/jdbc/QuestionnaireDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/jdbc/QuestionnaireDaoJdbc.java new file mode 100755 index 000000000..84afa4c3a --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/jdbc/QuestionnaireDaoJdbc.java @@ -0,0 +1,67 @@ +package org.highmed.dsf.fhir.dao.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Optional; + +import javax.sql.DataSource; + +import org.highmed.dsf.fhir.dao.QuestionnaireDao; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireDate; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireIdentifier; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireStatus; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireUrl; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireVersion; +import org.highmed.dsf.fhir.search.parameters.user.QuestionnaireUserFilter; +import org.hl7.fhir.r4.model.Questionnaire; + +import ca.uhn.fhir.context.FhirContext; + +public class QuestionnaireDaoJdbc extends AbstractResourceDaoJdbc implements QuestionnaireDao +{ + private final ReadByUrlDaoJdbc readByUrl; + + public QuestionnaireDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, FhirContext fhirContext) + { + super(dataSource, permanentDeleteDataSource, fhirContext, Questionnaire.class, "questionnaires", + "questionnaire", "questionnaire_id", QuestionnaireUserFilter::new, + with(QuestionnaireDate::new, QuestionnaireIdentifier::new, QuestionnaireStatus::new, + QuestionnaireUrl::new, QuestionnaireVersion::new), + with()); + + readByUrl = new ReadByUrlDaoJdbc<>(this::getDataSource, this::getResource, getResourceTable(), + getResourceColumn()); + } + + @Override + protected Questionnaire copy(Questionnaire resource) + { + return resource.copy(); + } + + @Override + public Optional readByUrlAndVersion(String urlAndVersion) throws SQLException + { + return readByUrl.readByUrlAndVersion(urlAndVersion); + } + + @Override + public Optional readByUrlAndVersionWithTransaction(Connection connection, String urlAndVersion) + throws SQLException + { + return readByUrl.readByUrlAndVersionWithTransaction(connection, urlAndVersion); + } + + @Override + public Optional readByUrlAndVersion(String url, String version) throws SQLException + { + return readByUrl.readByUrlAndVersion(url, version); + } + + @Override + public Optional readByUrlAndVersionWithTransaction(Connection connection, String url, String version) + throws SQLException + { + return readByUrl.readByUrlAndVersionWithTransaction(connection, url, version); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/jdbc/QuestionnaireResponseDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/jdbc/QuestionnaireResponseDaoJdbc.java new file mode 100755 index 000000000..a5098faee --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/jdbc/QuestionnaireResponseDaoJdbc.java @@ -0,0 +1,36 @@ +package org.highmed.dsf.fhir.dao.jdbc; + +import javax.sql.DataSource; + +import org.highmed.dsf.fhir.dao.QuestionnaireResponseDao; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireResponseAuthored; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireResponseIdentifier; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireResponseQuestionnaire; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireResponseStatus; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireResponseSubject; +import org.highmed.dsf.fhir.search.parameters.user.QuestionnaireResponseUserFilter; +import org.hl7.fhir.r4.model.QuestionnaireResponse; + +import ca.uhn.fhir.context.FhirContext; + +public class QuestionnaireResponseDaoJdbc extends AbstractResourceDaoJdbc + implements QuestionnaireResponseDao +{ + public QuestionnaireResponseDaoJdbc(DataSource dataSource, DataSource permanentDeleteDataSource, + FhirContext fhirContext) + { + super(dataSource, permanentDeleteDataSource, fhirContext, QuestionnaireResponse.class, + "questionnaire_responses", "questionnaire_response", "questionnaire_response_id", + QuestionnaireResponseUserFilter::new, + with(QuestionnaireResponseAuthored::new, QuestionnaireResponseIdentifier::new, + QuestionnaireResponseQuestionnaire::new, QuestionnaireResponseStatus::new, + QuestionnaireResponseSubject::new), + with()); + } + + @Override + protected QuestionnaireResponse copy(QuestionnaireResponse resource) + { + return resource.copy(); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/provider/DaoProvider.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/provider/DaoProvider.java index ae846e6dc..cf1a8daf9 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/provider/DaoProvider.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/provider/DaoProvider.java @@ -23,6 +23,8 @@ import org.highmed.dsf.fhir.dao.PractitionerDao; import org.highmed.dsf.fhir.dao.PractitionerRoleDao; import org.highmed.dsf.fhir.dao.ProvenanceDao; +import org.highmed.dsf.fhir.dao.QuestionnaireDao; +import org.highmed.dsf.fhir.dao.QuestionnaireResponseDao; import org.highmed.dsf.fhir.dao.ReadAccessDao; import org.highmed.dsf.fhir.dao.ResearchStudyDao; import org.highmed.dsf.fhir.dao.ResourceDao; @@ -76,6 +78,10 @@ public interface DaoProvider ProvenanceDao getProvenanceDao(); + QuestionnaireDao getQuestionnaireDao(); + + QuestionnaireResponseDao getQuestionnaireResponseDao(); + ResearchStudyDao getResearchStudyDao(); StructureDefinitionDao getStructureDefinitionDao(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/provider/DaoProviderImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/provider/DaoProviderImpl.java index ef846ed4a..31c1ea3a3 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/provider/DaoProviderImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/dao/provider/DaoProviderImpl.java @@ -28,6 +28,8 @@ import org.highmed.dsf.fhir.dao.PractitionerDao; import org.highmed.dsf.fhir.dao.PractitionerRoleDao; import org.highmed.dsf.fhir.dao.ProvenanceDao; +import org.highmed.dsf.fhir.dao.QuestionnaireDao; +import org.highmed.dsf.fhir.dao.QuestionnaireResponseDao; import org.highmed.dsf.fhir.dao.ReadAccessDao; import org.highmed.dsf.fhir.dao.ResearchStudyDao; import org.highmed.dsf.fhir.dao.ResourceDao; @@ -54,6 +56,8 @@ import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.ResearchStudy; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StructureDefinition; @@ -86,6 +90,8 @@ public class DaoProviderImpl implements DaoProvider, InitializingBean private final PractitionerDao practitionerDao; private final PractitionerRoleDao practitionerRoleDao; private final ProvenanceDao provenanceDao; + private final QuestionnaireDao questionnaireDao; + private final QuestionnaireResponseDao questionnaireResponseDao; private final ResearchStudyDao researchStudyDao; private final StructureDefinitionDao structureDefinitionDao; private final StructureDefinitionDao structureDefinitionSnapshotDao; @@ -105,6 +111,7 @@ public DaoProviderImpl(DataSource dataSource, ActivityDefinitionDao activityDefi NamingSystemDao namingSystemDao, OrganizationDao organizationDao, OrganizationAffiliationDao organizationAffiliationDao, PatientDao patientDao, PractitionerDao practitionerDao, PractitionerRoleDao practitionerRoleDao, ProvenanceDao provenanceDao, + QuestionnaireDao questionnaireDao, QuestionnaireResponseDao questionnaireResponseDao, ResearchStudyDao researchStudyDao, StructureDefinitionDao structureDefinitionDao, StructureDefinitionDao structureDefinitionSnapshotDao, SubscriptionDao subscriptionDao, TaskDao taskDao, ValueSetDao valueSetDao, ReadAccessDao readAccessDao) @@ -129,6 +136,8 @@ public DaoProviderImpl(DataSource dataSource, ActivityDefinitionDao activityDefi this.practitionerDao = practitionerDao; this.practitionerRoleDao = practitionerRoleDao; this.provenanceDao = provenanceDao; + this.questionnaireDao = questionnaireDao; + this.questionnaireResponseDao = questionnaireResponseDao; this.researchStudyDao = researchStudyDao; this.structureDefinitionDao = structureDefinitionDao; this.structureDefinitionSnapshotDao = structureDefinitionSnapshotDao; @@ -157,6 +166,8 @@ public DaoProviderImpl(DataSource dataSource, ActivityDefinitionDao activityDefi daosByResourceClass.put(Practitioner.class, practitionerDao); daosByResourceClass.put(PractitionerRole.class, practitionerRoleDao); daosByResourceClass.put(Provenance.class, provenanceDao); + daosByResourceClass.put(Questionnaire.class, questionnaireDao); + daosByResourceClass.put(QuestionnaireResponse.class, questionnaireResponseDao); daosByResourceClass.put(ResearchStudy.class, researchStudyDao); daosByResourceClass.put(StructureDefinition.class, structureDefinitionDao); daosByResourceClass.put(Subscription.class, subscriptionDao); @@ -188,6 +199,8 @@ public void afterPropertiesSet() throws Exception Objects.requireNonNull(practitionerDao, "practitionerDao"); Objects.requireNonNull(practitionerRoleDao, "practitionerRoleDao"); Objects.requireNonNull(provenanceDao, "provenanceDao"); + Objects.requireNonNull(questionnaireDao, "questionnaireDao"); + Objects.requireNonNull(questionnaireResponseDao, "questionnaireResponseDao"); Objects.requireNonNull(researchStudyDao, "researchStudyDao"); Objects.requireNonNull(structureDefinitionDao, "structureDefinitionDao"); Objects.requireNonNull(structureDefinitionSnapshotDao, "structureDefinitionSnapshotDao"); @@ -332,6 +345,18 @@ public ProvenanceDao getProvenanceDao() return provenanceDao; } + @Override + public QuestionnaireDao getQuestionnaireDao() + { + return questionnaireDao; + } + + @Override + public QuestionnaireResponseDao getQuestionnaireResponseDao() + { + return questionnaireResponseDao; + } + @Override public ResearchStudyDao getResearchStudyDao() { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireDate.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireDate.java new file mode 100644 index 000000000..5215e661c --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireDate.java @@ -0,0 +1,17 @@ +package org.highmed.dsf.fhir.search.parameters; + +import org.highmed.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractDateTimeParameter; +import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import org.hl7.fhir.r4.model.Questionnaire; + +@SearchParameterDefinition(name = QuestionnaireDate.PARAMETER_NAME, definition = "http://hl7.org/fhir/SearchParameter/Questionnaire-date", type = SearchParamType.DATE, documentation = "The questionnaire publication date") +public class QuestionnaireDate extends AbstractDateTimeParameter +{ + public static final String PARAMETER_NAME = "date"; + + public QuestionnaireDate() + { + super(PARAMETER_NAME, "questionnaire->>'date'"); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireIdentifier.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireIdentifier.java new file mode 100644 index 000000000..da52f87c0 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireIdentifier.java @@ -0,0 +1,32 @@ +package org.highmed.dsf.fhir.search.parameters; + +import org.highmed.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractIdentifierParameter; +import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.Resource; + +@SearchParameterDefinition(name = AbstractIdentifierParameter.PARAMETER_NAME, definition = "http://hl7.org/fhir/SearchParameter/Questionnaire-identifier", type = SearchParamType.TOKEN, documentation = "External identifier for the questionnaire") +public class QuestionnaireIdentifier extends AbstractIdentifierParameter +{ + public static final String RESOURCE_COLUMN = "questionnaire"; + + public QuestionnaireIdentifier() + { + super(RESOURCE_COLUMN); + } + + @Override + public boolean matches(Resource resource) + { + if (!isDefined()) + throw notDefined(); + + if (!(resource instanceof Questionnaire)) + return false; + + Questionnaire q = (Questionnaire) resource; + + return identifierMatches(q.getIdentifier()); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseAuthored.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseAuthored.java new file mode 100644 index 000000000..f13ba9b0f --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseAuthored.java @@ -0,0 +1,17 @@ +package org.highmed.dsf.fhir.search.parameters; + +import org.highmed.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractDateTimeParameter; +import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import org.hl7.fhir.r4.model.QuestionnaireResponse; + +@SearchParameterDefinition(name = QuestionnaireResponseAuthored.PARAMETER_NAME, definition = "http://hl7.org/fhir/SearchParameter/QuestionnaireRespone-authored", type = SearchParamType.DATE, documentation = "When the questionnaire response was last changed") +public class QuestionnaireResponseAuthored extends AbstractDateTimeParameter +{ + public static final String PARAMETER_NAME = "authored"; + + public QuestionnaireResponseAuthored() + { + super(PARAMETER_NAME, "questionnaire_response->>'authored'"); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseIdentifier.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseIdentifier.java new file mode 100644 index 000000000..24af36f50 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseIdentifier.java @@ -0,0 +1,100 @@ +package org.highmed.dsf.fhir.search.parameters; + +import java.sql.Array; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.highmed.dsf.fhir.function.BiFunctionWithSqlException; +import org.highmed.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractIdentifierParameter; +import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; + +@SearchParameterDefinition(name = AbstractIdentifierParameter.PARAMETER_NAME, definition = "http://hl7.org/fhir/SearchParameter/QuestionnaireResponse-identifier", type = SearchParamType.TOKEN, documentation = "The unique identifier for the questionnaire response") +public class QuestionnaireResponseIdentifier extends AbstractIdentifierParameter +{ + public static final String RESOURCE_COLUMN = "questionnaire_response"; + + public QuestionnaireResponseIdentifier() + { + super(RESOURCE_COLUMN); + } + + @Override + public String getFilterQuery() + { + switch (valueAndType.type) + { + case CODE: + case CODE_AND_SYSTEM: + case SYSTEM: + return "questionnaire_response->'identifier' " + (valueAndType.negated ? "<>" : "=") + " ?::jsonb"; + case CODE_AND_NO_SYSTEM_PROPERTY: + if (valueAndType.negated) + return "questionnaire_response->'identifier'->>'value' <> ? OR (questionnaire_response->'identifier' ?? 'system')"; + else + return "questionnaire_response->'identifier'->>'value' = ? AND NOT (questionnaire_response->'identifier' ?? 'system')"; + default: + return ""; + } + } + + @Override + public int getSqlParameterCount() + { + return 1; + } + + @Override + public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, + BiFunctionWithSqlException arrayCreator) throws SQLException + { + switch (valueAndType.type) + { + case CODE: + statement.setString(parameterIndex, "{\"value\": \"" + valueAndType.codeValue + "\"}"); + return; + case CODE_AND_SYSTEM: + statement.setString(parameterIndex, "{\"value\": \"" + valueAndType.codeValue + "\", \"system\": \"" + + valueAndType.systemValue + "\"}"); + return; + case CODE_AND_NO_SYSTEM_PROPERTY: + statement.setString(parameterIndex, valueAndType.codeValue); + return; + case SYSTEM: + statement.setString(parameterIndex, "{\"system\": \"" + valueAndType.systemValue + "\"}"); + return; + } + } + + private boolean identifierMatches(Identifier identifier) + { + if (valueAndType.negated) + return !AbstractIdentifierParameter.identifierMatches(valueAndType, identifier); + else + return AbstractIdentifierParameter.identifierMatches(valueAndType, identifier); + } + + @Override + protected String getSortSql(String sortDirectionWithSpacePrefix) + { + return "(questionnaire_response->'identifier'->>'system')::text || (questionnaire_response->'identifier'->>'value')::text" + + sortDirectionWithSpacePrefix; + } + + @Override + public boolean matches(Resource resource) + { + if (!isDefined()) + throw notDefined(); + + if (!(resource instanceof QuestionnaireResponse)) + return false; + + QuestionnaireResponse qr = (QuestionnaireResponse) resource; + + return identifierMatches(qr.getIdentifier()); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseQuestionnaire.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseQuestionnaire.java new file mode 100755 index 000000000..9375a28ae --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseQuestionnaire.java @@ -0,0 +1,105 @@ +package org.highmed.dsf.fhir.search.parameters; + +import java.sql.Array; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.highmed.dsf.fhir.dao.provider.DaoProvider; +import org.highmed.dsf.fhir.function.BiFunctionWithSqlException; +import org.highmed.dsf.fhir.search.IncludeParameterDefinition; +import org.highmed.dsf.fhir.search.IncludeParts; +import org.highmed.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractCanonicalReferenceParameter; +import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; + +@IncludeParameterDefinition(resourceType = QuestionnaireResponse.class, parameterName = QuestionnaireResponseQuestionnaire.PARAMETER_NAME, targetResourceTypes = Questionnaire.class) +@SearchParameterDefinition(name = QuestionnaireResponseQuestionnaire.PARAMETER_NAME, definition = "http://hl7.org/fhir/SearchParameter/QuestionnaireResponse-questionnaire", type = SearchParamType.REFERENCE, documentation = "The questionnaire the answers are provided for") +public class QuestionnaireResponseQuestionnaire extends AbstractCanonicalReferenceParameter +{ + private static final String RESOURCE_TYPE_NAME = "QuestionnaireResponse"; + public static final String PARAMETER_NAME = "questionnaire"; + private static final String TARGET_RESOURCE_TYPE_NAME = "Questionnaire"; + + public QuestionnaireResponseQuestionnaire() + { + super(QuestionnaireResponse.class, RESOURCE_TYPE_NAME, PARAMETER_NAME, TARGET_RESOURCE_TYPE_NAME); + } + + @Override + public boolean isDefined() + { + return super.isDefined() && ReferenceSearchType.URL.equals(valueAndType.type); + } + + @Override + public String getFilterQuery() + { + if (ReferenceSearchType.URL.equals(valueAndType.type)) + return "(questionnaire_response->>'questionnaire' LIKE (? || '%'))"; + + return ""; + } + + @Override + public int getSqlParameterCount() + { + return 1; + } + + @Override + public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, + BiFunctionWithSqlException arrayCreator) throws SQLException + { + if (ReferenceSearchType.URL.equals(valueAndType.type)) + { + if (subqueryParameterIndex == 1) + statement.setString(parameterIndex, valueAndType.url); + } + } + + @Override + protected void doResolveReferencesForMatching(QuestionnaireResponse resource, DaoProvider daoProvider) + throws SQLException + { + // Nothing to do for questionnaires + } + + @Override + public boolean matches(Resource resource) + { + if (!isDefined()) + throw notDefined(); + + if (!(resource instanceof QuestionnaireResponse)) + return false; + + QuestionnaireResponse qr = (QuestionnaireResponse) resource; + + return qr.getQuestionnaire().equals(valueAndType.url); + } + + @Override + protected String getSortSql(String sortDirectionWithSpacePrefix) + { + return "(SELECT string_agg(canonical, ' ') FROM questionnaire_response->'questionnaire' AS canonical)"; + } + + @Override + protected String getIncludeSql(IncludeParts includeParts) + { + if (includeParts.matches(RESOURCE_TYPE_NAME, PARAMETER_NAME, TARGET_RESOURCE_TYPE_NAME)) + return "(SELECT json_agg(questionnaire) FROM current_questionnaires WHERE (questionnaire->>'url' = split_part((questionnaire_response->>'questionnaire'), '|', 1) AND questionnaire->>'version' = split_part((questionnaire_response->>'questionnaire'), '|', 2)) OR (questionnaire->>'url' = split_part((questionnaire_response->>'questionnaire'), '|', 1) AND split_part((questionnaire_response->>'questionnaire'), '|', 2) = 'null') OR (questionnaire->>'url' = questionnaire_response->>'questionnaire' AND (questionnaire->'version') is null)) AS questionnaire"; + else + return null; + } + + @Override + protected void modifyIncludeResource(IncludeParts includeParts, Resource resource, Connection connection) + { + // Nothing to do for questionnaires + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseStatus.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseStatus.java new file mode 100755 index 000000000..b745728b8 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseStatus.java @@ -0,0 +1,112 @@ +package org.highmed.dsf.fhir.search.parameters; + +import java.sql.Array; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.ws.rs.core.UriBuilder; + +import org.highmed.dsf.fhir.function.BiFunctionWithSqlException; +import org.highmed.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; +import org.highmed.dsf.fhir.search.SearchQueryParameterError; +import org.highmed.dsf.fhir.search.SearchQueryParameterError.SearchQueryParameterErrorType; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractTokenParameter; +import org.highmed.dsf.fhir.search.parameters.basic.TokenSearchType; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; + +@SearchParameterDefinition(name = QuestionnaireResponseStatus.PARAMETER_NAME, definition = "http://hl7.org/fhir/SearchParameter/QuestionnaireResponse-status", type = SearchParamType.TOKEN, documentation = "The status of the questionnaire response") +public class QuestionnaireResponseStatus extends AbstractTokenParameter +{ + public static final String PARAMETER_NAME = "status"; + + private QuestionnaireResponse.QuestionnaireResponseStatus status; + + public QuestionnaireResponseStatus() + { + super(PARAMETER_NAME); + } + + @Override + protected void configureSearchParameter(Map> queryParameters) + { + super.configureSearchParameter(queryParameters); + + if (valueAndType != null && valueAndType.type == TokenSearchType.CODE) + status = toStatus(valueAndType.codeValue, queryParameters.get(parameterName)); + } + + private QuestionnaireResponse.QuestionnaireResponseStatus toStatus(String status, List parameterValues) + { + if (status == null || status.isBlank()) + return null; + + try + { + return QuestionnaireResponse.QuestionnaireResponseStatus.fromCode(status); + } + catch (FHIRException e) + { + addError(new SearchQueryParameterError(SearchQueryParameterErrorType.UNPARSABLE_VALUE, parameterName, + parameterValues, e)); + return null; + } + } + + @Override + public boolean isDefined() + { + return super.isDefined() && status != null; + } + + @Override + public String getFilterQuery() + { + return "questionnaire_response->>'status' " + (valueAndType.negated ? "<>" : "=") + " ?"; + } + + @Override + public int getSqlParameterCount() + { + return 1; + } + + @Override + public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, + BiFunctionWithSqlException arrayCreator) throws SQLException + { + statement.setString(parameterIndex, status.toCode()); + } + + @Override + public void modifyBundleUri(UriBuilder bundleUri) + { + bundleUri.replaceQueryParam(PARAMETER_NAME + (valueAndType.negated ? ":not" : ""), status.toCode()); + } + + @Override + public boolean matches(Resource resource) + { + if (!isDefined()) + throw notDefined(); + + if (!(resource instanceof QuestionnaireResponse)) + return false; + + if (valueAndType.negated) + return !Objects.equals(((QuestionnaireResponse) resource).getStatus(), status); + else + return Objects.equals(((QuestionnaireResponse) resource).getStatus(), status); + } + + @Override + protected String getSortSql(String sortDirectionWithSpacePrefix) + { + return "questionnaire_response->>'status'" + sortDirectionWithSpacePrefix; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java new file mode 100755 index 000000000..6445c5734 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java @@ -0,0 +1,255 @@ +package org.highmed.dsf.fhir.search.parameters; + +import java.sql.Array; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.UUID; + +import org.highmed.dsf.fhir.dao.ResourceDao; +import org.highmed.dsf.fhir.dao.exception.ResourceDeletedException; +import org.highmed.dsf.fhir.dao.provider.DaoProvider; +import org.highmed.dsf.fhir.function.BiFunctionWithSqlException; +import org.highmed.dsf.fhir.search.IncludeParameterDefinition; +import org.highmed.dsf.fhir.search.IncludeParts; +import org.highmed.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractIdentifierParameter; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractReferenceParameter; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; + +@IncludeParameterDefinition(resourceType = QuestionnaireResponse.class, parameterName = QuestionnaireResponseSubject.PARAMETER_NAME, targetResourceTypes = { + Organization.class, Practitioner.class, PractitionerRole.class }) +@SearchParameterDefinition(name = QuestionnaireResponseSubject.PARAMETER_NAME, definition = "http://hl7.org/fhir/SearchParameter/QuestionnaireResponse-subject", type = SearchParamType.REFERENCE, documentation = "The subject of the questionnaire response") +public class QuestionnaireResponseSubject extends AbstractReferenceParameter +{ + private static final String RESOURCE_TYPE_NAME = "QuestionnaireResponse"; + public static final String PARAMETER_NAME = "subject"; + + // TODO if needed, modify for Reference(Any), see also doResolveReferencesForMatching, matches, getIncludeSql + private static final String[] TARGET_RESOURCE_TYPE_NAMES = { "Organization", "Practitioner", "PractitionerRole" }; + + private static final String IDENTIFIERS_SUBQUERY = "(SELECT practitioner->'identifier' FROM current_practitioners " + + "WHERE concat('Practitioner/', practitioner->>'id') = questionnaire_response->'subject'->>'reference' " + + "UNION SELECT organization->'identifier' FROM current_organizations " + + "WHERE concat('Organization/', organization->>'id') = questionnaire_response->'subject'->>'reference' " + + "UNION SELECT practitioner_role->'identifier' FROM current_practitioner_roles " + + "WHERE concat('PractitionerRole/', practitioner_role->>'id') = questionnaire_response->'subject'->>'reference')"; + + public QuestionnaireResponseSubject() + { + super(QuestionnaireResponse.class, RESOURCE_TYPE_NAME, PARAMETER_NAME, TARGET_RESOURCE_TYPE_NAMES); + } + + @Override + public String getFilterQuery() + { + switch (valueAndType.type) + { + case ID: + // testing all TargetResourceTypeName/ID combinations + return "questionnaire_response->'subject'->>'reference' = ANY (?)"; + case RESOURCE_NAME_AND_ID: + case URL: + case TYPE_AND_ID: + case TYPE_AND_RESOURCE_NAME_AND_ID: + return "questionnaire_response->'subject'->>'reference' = ?"; + case IDENTIFIER: + { + switch (valueAndType.identifier.type) + { + case CODE: + case CODE_AND_SYSTEM: + case SYSTEM: + return IDENTIFIERS_SUBQUERY + " @> ?::jsonb"; + case CODE_AND_NO_SYSTEM_PROPERTY: + return "(SELECT count(*) FROM jsonb_array_elements(" + IDENTIFIERS_SUBQUERY + + ") identifier WHERE identifier->>'value' = ? AND NOT (identifier ?? 'system')) > 0"; + } + } + } + + return ""; + } + + @Override + public int getSqlParameterCount() + { + return 1; + } + + @Override + public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement, + BiFunctionWithSqlException arrayCreator) throws SQLException + { + switch (valueAndType.type) + { + case ID: + Array array = arrayCreator.apply("TEXT", + Arrays.stream(TARGET_RESOURCE_TYPE_NAMES).map(n -> n + "/" + valueAndType.id).toArray()); + statement.setArray(parameterIndex, array); + break; + case RESOURCE_NAME_AND_ID: + case TYPE_AND_ID: + case TYPE_AND_RESOURCE_NAME_AND_ID: + statement.setString(parameterIndex, valueAndType.resourceName + "/" + valueAndType.id); + break; + case URL: + statement.setString(parameterIndex, valueAndType.url); + break; + case IDENTIFIER: + { + switch (valueAndType.identifier.type) + { + case CODE: + statement.setString(parameterIndex, + "[{\"value\": \"" + valueAndType.identifier.codeValue + "\"}]"); + break; + case CODE_AND_SYSTEM: + statement.setString(parameterIndex, "[{\"value\": \"" + valueAndType.identifier.codeValue + + "\", \"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); + break; + case CODE_AND_NO_SYSTEM_PROPERTY: + statement.setString(parameterIndex, valueAndType.identifier.codeValue); + break; + case SYSTEM: + statement.setString(parameterIndex, + "[{\"system\": \"" + valueAndType.identifier.systemValue + "\"}]"); + break; + } + } + } + } + + @Override + protected void doResolveReferencesForMatching(QuestionnaireResponse resource, DaoProvider daoProvider) + throws SQLException + { + Reference reference = resource.getSubject(); + IIdType idType = reference.getReferenceElement(); + + if (idType.hasResourceType()) + { + if ("Organization".equals(idType.getResourceType())) + setResource(reference, idType, daoProvider.getOrganizationDao()); + else if ("Practitioner".equals(idType.getResourceType())) + setResource(reference, idType, daoProvider.getPractitionerDao()); + else if ("PractitionerRole".equals(idType.getResourceType())) + setResource(reference, idType, daoProvider.getPractitionerRoleDao()); + } + } + + private void setResource(Reference reference, IIdType idType, ResourceDao dao) throws SQLException + { + try + { + if (idType.hasVersionIdPart()) + dao.readVersion(UUID.fromString(idType.getIdPart()), idType.getVersionIdPartAsLong()) + .ifPresent(reference::setResource); + else + dao.read(UUID.fromString(idType.getIdPart())).ifPresent(reference::setResource); + } + catch (ResourceDeletedException e) + { + // ignore while matching, will result in a non match if this would have been the matching resource + } + } + + @Override + public boolean matches(Resource resource) + { + if (!isDefined()) + throw notDefined(); + + if (!(resource instanceof Endpoint)) + return false; + + QuestionnaireResponse qr = (QuestionnaireResponse) resource; + + if (ReferenceSearchType.IDENTIFIER.equals(valueAndType.type)) + { + if (qr.getSubject().getResource() instanceof Organization) + { + Organization o = (Organization) qr.getSubject().getResource(); + return o.getIdentifier().stream() + .anyMatch(i -> AbstractIdentifierParameter.identifierMatches(valueAndType.identifier, i)); + } + else if (qr.getSubject().getResource() instanceof Practitioner) + { + Practitioner p = (Practitioner) qr.getSubject().getResource(); + return p.getIdentifier().stream() + .anyMatch(i -> AbstractIdentifierParameter.identifierMatches(valueAndType.identifier, i)); + } + else if (qr.getSubject().getResource() instanceof PractitionerRole) + { + PractitionerRole p = (PractitionerRole) qr.getSubject().getResource(); + return p.getIdentifier().stream() + .anyMatch(i -> AbstractIdentifierParameter.identifierMatches(valueAndType.identifier, i)); + } + else + return false; + } + else + { + String ref = qr.getSubject().getReference(); + switch (valueAndType.type) + { + case ID: + return ref.equals("Organization" + "/" + valueAndType.id) + || ref.equals("Practitioner" + "/" + valueAndType.id) + || ref.equals("PractitionerRole" + "/" + valueAndType.id); + case RESOURCE_NAME_AND_ID: + return ref.equals(valueAndType.resourceName + "/" + valueAndType.id); + case URL: + return ref.equals(valueAndType.url); + default: + return false; + } + } + } + + @Override + protected String getSortSql(String sortDirectionWithSpacePrefix) + { + return "questionnaire_response->'subject'->>'reference'"; + } + + @Override + protected String getIncludeSql(IncludeParts includeParts) + { + if (RESOURCE_TYPE_NAME.equals(includeParts.getSourceResourceTypeName()) + && PARAMETER_NAME.equals(includeParts.getSearchParameterName()) + && Arrays.stream(TARGET_RESOURCE_TYPE_NAMES) + .anyMatch(n -> n.equals(includeParts.getTargetResourceTypeName()))) + switch (includeParts.getTargetResourceTypeName()) + { + case "Organization": + return "(SELECT jsonb_build_array(organization) FROM current_organizations" + + " WHERE concat('Organization/', organization->>'id') = questionnaire_response->'subject'->>'reference') AS organizations"; + case "Practitioner": + return "(SELECT jsonb_build_array(practitioner) FROM current_practitioners" + + " WHERE concat('Practitioner/', practitioner->>'id') = questionnaire_response->'subject'->>'reference') AS practitioners"; + case "PractitionerRole": + return "(SELECT jsonb_build_array(practitioner_role) FROM current_practitioner_roles" + + " WHERE concat('PractitionerRole/', practitioner_role->>'id') = questionnaire_response->'subject'->>'reference') AS practitioner_roles"; + default: + return null; + } + else + return null; + } + + @Override + protected void modifyIncludeResource(IncludeParts includeParts, Resource resource, Connection connection) + { + // Nothing to do for organizations, practitioners or practitioner-roles + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireStatus.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireStatus.java new file mode 100644 index 000000000..e3b4e40ac --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireStatus.java @@ -0,0 +1,15 @@ +package org.highmed.dsf.fhir.search.parameters; + +import org.highmed.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractStatusParameter; +import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import org.hl7.fhir.r4.model.Questionnaire; + +@SearchParameterDefinition(name = QuestionnaireStatus.PARAMETER_NAME, definition = "http://hl7.org/fhir/SearchParameter/Questionnaire-status", type = SearchParamType.TOKEN, documentation = "The current status of the questionnaire") +public class QuestionnaireStatus extends AbstractStatusParameter +{ + public QuestionnaireStatus() + { + super("questionnaire", Questionnaire.class); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireUrl.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireUrl.java new file mode 100644 index 000000000..dc70f36e8 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireUrl.java @@ -0,0 +1,24 @@ +package org.highmed.dsf.fhir.search.parameters; + +import org.highmed.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractUrlAndVersionParameter; +import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.Resource; + +@SearchParameterDefinition(name = QuestionnaireUrl.PARAMETER_NAME, definition = "http://hl7.org/fhir/SearchParameter/Questionnaire-url", type = SearchParamType.URI, documentation = "The uri that identifies the questionnaire") +public class QuestionnaireUrl extends AbstractUrlAndVersionParameter +{ + public static final String RESOURCE_COLUMN = "questionnaire"; + + public QuestionnaireUrl() + { + super(RESOURCE_COLUMN); + } + + @Override + protected boolean instanceOf(Resource resource) + { + return resource instanceof Questionnaire; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireVersion.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireVersion.java new file mode 100644 index 000000000..7bbfee4c7 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/QuestionnaireVersion.java @@ -0,0 +1,29 @@ +package org.highmed.dsf.fhir.search.parameters; + +import org.highmed.dsf.fhir.search.SearchQueryParameter.SearchParameterDefinition; +import org.highmed.dsf.fhir.search.parameters.basic.AbstractVersionParameter; +import org.hl7.fhir.r4.model.Enumerations.SearchParamType; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.Resource; + +@SearchParameterDefinition(name = QuestionnaireVersion.PARAMETER_NAME, definition = "http://hl7.org/fhir/SearchParameter/Questionnaire-version", type = SearchParamType.TOKEN, documentation = "The business version of the questionnaire") +public class QuestionnaireVersion extends AbstractVersionParameter +{ + public static final String RESOURCE_COLUMN = "questionnaire"; + + public QuestionnaireVersion() + { + this(RESOURCE_COLUMN); + } + + public QuestionnaireVersion(String resourceColumn) + { + super(resourceColumn); + } + + @Override + protected boolean instanceOf(Resource resource) + { + return resource instanceof Questionnaire; + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/user/QuestionnaireResponseUserFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/user/QuestionnaireResponseUserFilter.java new file mode 100644 index 000000000..f21f32a16 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/user/QuestionnaireResponseUserFilter.java @@ -0,0 +1,19 @@ +package org.highmed.dsf.fhir.search.parameters.user; + +import org.highmed.dsf.fhir.authentication.User; + +public class QuestionnaireResponseUserFilter extends AbstractMetaTagAuthorizationRoleUserFilter +{ + private static final String RESOURCE_TABLE = "current_questionnaire_responses"; + private static final String RESOURCE_ID_COLUMN = "questionnaire_response_id"; + + public QuestionnaireResponseUserFilter(User user) + { + super(user, RESOURCE_TABLE, RESOURCE_ID_COLUMN); + } + + public QuestionnaireResponseUserFilter(User user, String resourceTable, String resourceIdColumn) + { + super(user, resourceTable, resourceIdColumn); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/user/QuestionnaireUserFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/user/QuestionnaireUserFilter.java new file mode 100644 index 000000000..fa7096b24 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/search/parameters/user/QuestionnaireUserFilter.java @@ -0,0 +1,19 @@ +package org.highmed.dsf.fhir.search.parameters.user; + +import org.highmed.dsf.fhir.authentication.User; + +public class QuestionnaireUserFilter extends AbstractMetaTagAuthorizationRoleUserFilter +{ + private static final String RESOURCE_TABLE = "current_questionnaires"; + private static final String RESOURCE_ID_COLUMN = "questionnaire_id"; + + public QuestionnaireUserFilter(User user) + { + super(user, RESOURCE_TABLE, RESOURCE_ID_COLUMN); + } + + public QuestionnaireUserFilter(User user, String resourceTable, String resourceIdColumn) + { + super(user, resourceTable, resourceIdColumn); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/AuthorizationConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/AuthorizationConfig.java index e8f19095f..e95cdcea0 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/AuthorizationConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/AuthorizationConfig.java @@ -24,6 +24,8 @@ import org.highmed.dsf.fhir.authorization.PractitionerAuthorizationRule; import org.highmed.dsf.fhir.authorization.PractitionerRoleAuthorizationRule; import org.highmed.dsf.fhir.authorization.ProvenanceAuthorizationRule; +import org.highmed.dsf.fhir.authorization.QuestionnaireAuthorizationRule; +import org.highmed.dsf.fhir.authorization.QuestionnaireResponseAuthorizationRule; import org.highmed.dsf.fhir.authorization.ResearchStudyAuthorizationRule; import org.highmed.dsf.fhir.authorization.RootAuthorizationRule; import org.highmed.dsf.fhir.authorization.StructureDefinitionAuthorizationRule; @@ -55,6 +57,8 @@ import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.ResearchStudy; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StructureDefinition; @@ -122,6 +126,7 @@ public AuthorizationRule binaryAuthorizationRule() measureAuthorizationRule(), measureReportAuthorizationRule(), namingSystemAuthorizationRule(), organizationAuthorizationRule(), organizationAffiliationAuthorizationRule(), patientAuthorizationRule(), practitionerAuthorizationRule(), practitionerRoleAuthorizationRule(), provenanceAuthorizationRule(), + questionnaireAuthorizationRule(), questionnaireResponseAuthorizationRule(), researchStudyAuthorizationRule(), structureDefinitionAuthorizationRule(), subscriptionAuthorizationRule(), valueSetAuthorizationRule()); } @@ -262,6 +267,22 @@ public AuthorizationRule provenanceAuthorizationRule() helperConfig.parameterConverter()); } + @Bean + public AuthorizationRule questionnaireAuthorizationRule() + { + return new QuestionnaireAuthorizationRule(daoConfig.daoProvider(), propertiesConfig.getServerBaseUrl(), + referenceConfig.referenceResolver(), organizationProvider(), readAccessHelper(), + helperConfig.parameterConverter()); + } + + @Bean + public AuthorizationRule questionnaireResponseAuthorizationRule() + { + return new QuestionnaireResponseAuthorizationRule(daoConfig.daoProvider(), propertiesConfig.getServerBaseUrl(), + referenceConfig.referenceResolver(), organizationProvider(), readAccessHelper(), + helperConfig.parameterConverter()); + } + @Bean public AuthorizationRule researchStudyAuthorizationRule() { @@ -311,7 +332,8 @@ public AuthorizationRuleProvider authorizationRuleProvider() libraryAuthorizationRule(), locationAuthorizationRule(), measureAuthorizationRule(), measureReportAuthorizationRule(), namingSystemAuthorizationRule(), organizationAuthorizationRule(), organizationAffiliationAuthorizationRule(), patientAuthorizationRule(), practitionerAuthorizationRule(), - practitionerRoleAuthorizationRule(), provenanceAuthorizationRule(), researchStudyAuthorizationRule(), + practitionerRoleAuthorizationRule(), provenanceAuthorizationRule(), questionnaireAuthorizationRule(), + questionnaireResponseAuthorizationRule(), researchStudyAuthorizationRule(), structureDefinitionAuthorizationRule(), subscriptionAuthorizationRule(), taskAuthorizationRule(), valueSetAuthorizationRule()); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/DaoConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/DaoConfig.java index 4c0bbb76e..353017072 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/DaoConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/DaoConfig.java @@ -21,6 +21,8 @@ import org.highmed.dsf.fhir.dao.PractitionerDao; import org.highmed.dsf.fhir.dao.PractitionerRoleDao; import org.highmed.dsf.fhir.dao.ProvenanceDao; +import org.highmed.dsf.fhir.dao.QuestionnaireDao; +import org.highmed.dsf.fhir.dao.QuestionnaireResponseDao; import org.highmed.dsf.fhir.dao.ReadAccessDao; import org.highmed.dsf.fhir.dao.ResearchStudyDao; import org.highmed.dsf.fhir.dao.StructureDefinitionDao; @@ -47,6 +49,8 @@ import org.highmed.dsf.fhir.dao.jdbc.PractitionerDaoJdbc; import org.highmed.dsf.fhir.dao.jdbc.PractitionerRoleDaoJdbc; import org.highmed.dsf.fhir.dao.jdbc.ProvenanceDaoJdbc; +import org.highmed.dsf.fhir.dao.jdbc.QuestionnaireDaoJdbc; +import org.highmed.dsf.fhir.dao.jdbc.QuestionnaireResponseDaoJdbc; import org.highmed.dsf.fhir.dao.jdbc.ReadAccessDaoJdbc; import org.highmed.dsf.fhir.dao.jdbc.ResearchStudyDaoJdbc; import org.highmed.dsf.fhir.dao.jdbc.StructureDefinitionDaoJdbc; @@ -219,6 +223,18 @@ public ProvenanceDao provenanceDao() return new ProvenanceDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); } + @Bean + public QuestionnaireDao questionnaireDao() + { + return new QuestionnaireDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + } + + @Bean + public QuestionnaireResponseDao questionnaireResponseDao() + { + return new QuestionnaireResponseDaoJdbc(dataSource(), permanentDeleteDataSource(), fhirConfig.fhirContext()); + } + @Bean public ResearchStudyDao researchStudyDao() { @@ -262,9 +278,9 @@ public DaoProvider daoProvider() return new DaoProviderImpl(dataSource(), activityDefinitionDao(), binaryDao(), bundleDao(), codeSystemDao(), documentReferenceDao(), endpointDao(), groupDao(), healthcareServiceDao(), libraryDao(), locationDao(), measureDao(), measureReportDao(), namingSystemDao(), organizationDao(), organizationAffiliationDao(), - patientDao(), practitionerDao(), practitionerRoleDao(), provenanceDao(), researchStudyDao(), - structureDefinitionDao(), structureDefinitionSnapshotDao(), subscriptionDao(), taskDao(), valueSetDao(), - readAccessDao()); + patientDao(), practitionerDao(), practitionerRoleDao(), provenanceDao(), questionnaireDao(), + questionnaireResponseDao(), researchStudyDao(), structureDefinitionDao(), + structureDefinitionSnapshotDao(), subscriptionDao(), taskDao(), valueSetDao(), readAccessDao()); } @Bean diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/EventConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/EventConfig.java index 167685209..73aa4d048 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/EventConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/EventConfig.java @@ -60,6 +60,8 @@ public MatcherFactory matcherFactory() put(daosByResourceName, daoConfig.practitionerDao()); put(daosByResourceName, daoConfig.practitionerRoleDao()); put(daosByResourceName, daoConfig.provenanceDao()); + put(daosByResourceName, daoConfig.questionnaireDao()); + put(daosByResourceName, daoConfig.questionnaireResponseDao()); put(daosByResourceName, daoConfig.researchStudyDao()); put(daosByResourceName, daoConfig.structureDefinitionDao()); put(daosByResourceName, daoConfig.subscriptionDao()); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/WebserviceConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/WebserviceConfig.java index 17967e95f..c3c274045 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/WebserviceConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/spring/config/WebserviceConfig.java @@ -22,6 +22,8 @@ import org.highmed.dsf.fhir.webservice.impl.PractitionerRoleServiceImpl; import org.highmed.dsf.fhir.webservice.impl.PractitionerServiceImpl; import org.highmed.dsf.fhir.webservice.impl.ProvenanceServiceImpl; +import org.highmed.dsf.fhir.webservice.impl.QuestionnaireResponseServiceImpl; +import org.highmed.dsf.fhir.webservice.impl.QuestionnaireServiceImpl; import org.highmed.dsf.fhir.webservice.impl.ResearchStudyServiceImpl; import org.highmed.dsf.fhir.webservice.impl.RootServiceImpl; import org.highmed.dsf.fhir.webservice.impl.StaticResourcesServiceImpl; @@ -50,6 +52,8 @@ import org.highmed.dsf.fhir.webservice.jaxrs.PractitionerRoleServiceJaxrs; import org.highmed.dsf.fhir.webservice.jaxrs.PractitionerServiceJaxrs; import org.highmed.dsf.fhir.webservice.jaxrs.ProvenanceServiceJaxrs; +import org.highmed.dsf.fhir.webservice.jaxrs.QuestionnaireResponseServiceJaxrs; +import org.highmed.dsf.fhir.webservice.jaxrs.QuestionnaireServiceJaxrs; import org.highmed.dsf.fhir.webservice.jaxrs.ResearchStudyServiceJaxrs; import org.highmed.dsf.fhir.webservice.jaxrs.RootServiceJaxrs; import org.highmed.dsf.fhir.webservice.jaxrs.StaticResourcesServiceJaxrs; @@ -78,6 +82,8 @@ import org.highmed.dsf.fhir.webservice.secure.PractitionerRoleServiceSecure; import org.highmed.dsf.fhir.webservice.secure.PractitionerServiceSecure; import org.highmed.dsf.fhir.webservice.secure.ProvenanceServiceSecure; +import org.highmed.dsf.fhir.webservice.secure.QuestionnaireResponseServiceSecure; +import org.highmed.dsf.fhir.webservice.secure.QuestionnaireServiceSecure; import org.highmed.dsf.fhir.webservice.secure.ResearchStudyServiceSecure; import org.highmed.dsf.fhir.webservice.secure.RootServiceSecure; import org.highmed.dsf.fhir.webservice.secure.StaticResourcesServiceSecure; @@ -106,6 +112,8 @@ import org.highmed.dsf.fhir.webservice.specification.PractitionerRoleService; import org.highmed.dsf.fhir.webservice.specification.PractitionerService; import org.highmed.dsf.fhir.webservice.specification.ProvenanceService; +import org.highmed.dsf.fhir.webservice.specification.QuestionnaireResponseService; +import org.highmed.dsf.fhir.webservice.specification.QuestionnaireService; import org.highmed.dsf.fhir.webservice.specification.ResearchStudyService; import org.highmed.dsf.fhir.webservice.specification.RootService; import org.highmed.dsf.fhir.webservice.specification.StaticResourcesService; @@ -662,6 +670,59 @@ private ProvenanceServiceImpl provenanceServiceImpl() historyConfig.historyService()); } + @Bean + public QuestionnaireService questionnaireService() + { + return new QuestionnaireServiceJaxrs(questionnaireServiceSecure()); + } + + private QuestionnaireServiceSecure questionnaireServiceSecure() + { + return new QuestionnaireServiceSecure(questionnaireServiceImpl(), propertiesConfig.getServerBaseUrl(), + helperConfig.responseGenerator(), referenceConfig.referenceResolver(), + referenceConfig.referenceCleaner(), referenceConfig.referenceExtractor(), daoConfig.questionnaireDao(), + helperConfig.exceptionHandler(), helperConfig.parameterConverter(), + authorizationConfig.questionnaireAuthorizationRule(), validationConfig.resourceValidator()); + } + + private QuestionnaireServiceImpl questionnaireServiceImpl() + { + return new QuestionnaireServiceImpl(QuestionnaireServiceJaxrs.PATH, propertiesConfig.getServerBaseUrl(), + propertiesConfig.getDefaultPageCount(), daoConfig.questionnaireDao(), + validationConfig.resourceValidator(), eventConfig.eventManager(), helperConfig.exceptionHandler(), + eventConfig.eventGenerator(), helperConfig.responseGenerator(), helperConfig.parameterConverter(), + referenceConfig.referenceExtractor(), referenceConfig.referenceResolver(), + referenceConfig.referenceCleaner(), authorizationConfig.authorizationRuleProvider(), + historyConfig.historyService()); + } + + @Bean + public QuestionnaireResponseService questionnaireResponseService() + { + return new QuestionnaireResponseServiceJaxrs(questionnaireResponseServiceSecure()); + } + + private QuestionnaireResponseServiceSecure questionnaireResponseServiceSecure() + { + return new QuestionnaireResponseServiceSecure(questionnaireResponseServiceImpl(), + propertiesConfig.getServerBaseUrl(), helperConfig.responseGenerator(), + referenceConfig.referenceResolver(), referenceConfig.referenceCleaner(), + referenceConfig.referenceExtractor(), daoConfig.questionnaireResponseDao(), + helperConfig.exceptionHandler(), helperConfig.parameterConverter(), + authorizationConfig.questionnaireResponseAuthorizationRule(), validationConfig.resourceValidator()); + } + + private QuestionnaireResponseServiceImpl questionnaireResponseServiceImpl() + { + return new QuestionnaireResponseServiceImpl(QuestionnaireResponseServiceJaxrs.PATH, + propertiesConfig.getServerBaseUrl(), propertiesConfig.getDefaultPageCount(), + daoConfig.questionnaireResponseDao(), validationConfig.resourceValidator(), eventConfig.eventManager(), + helperConfig.exceptionHandler(), eventConfig.eventGenerator(), helperConfig.responseGenerator(), + helperConfig.parameterConverter(), referenceConfig.referenceExtractor(), + referenceConfig.referenceResolver(), referenceConfig.referenceCleaner(), + authorizationConfig.authorizationRuleProvider(), historyConfig.historyService()); + } + @Bean public ResearchStudyService researchStudyService() { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/impl/ConformanceServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/impl/ConformanceServiceImpl.java index e2267c3cb..51e5a2a0f 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/impl/ConformanceServiceImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/impl/ConformanceServiceImpl.java @@ -74,6 +74,14 @@ import org.highmed.dsf.fhir.search.parameters.PractitionerRoleIdentifier; import org.highmed.dsf.fhir.search.parameters.PractitionerRoleOrganization; import org.highmed.dsf.fhir.search.parameters.PractitionerRolePractitioner; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireDate; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireIdentifier; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireResponseAuthored; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireResponseIdentifier; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireResponseStatus; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireStatus; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireUrl; +import org.highmed.dsf.fhir.search.parameters.QuestionnaireVersion; import org.highmed.dsf.fhir.search.parameters.ResearchStudyEnrollment; import org.highmed.dsf.fhir.search.parameters.ResearchStudyIdentifier; import org.highmed.dsf.fhir.search.parameters.ResearchStudyPrincipalInvestigator; @@ -152,6 +160,8 @@ import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.ResearchStudy; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; @@ -297,8 +307,8 @@ private CapabilityStatement createCapabilityStatement() DocumentReference.class, Endpoint.class, Group.class, HealthcareService.class, Library.class, Location.class, Measure.class, MeasureReport.class, NamingSystem.class, Organization.class, OrganizationAffiliation.class, Patient.class, PractitionerRole.class, Practitioner.class, - Provenance.class, ResearchStudy.class, StructureDefinition.class, Subscription.class, Task.class, - ValueSet.class); + Provenance.class, Questionnaire.class, QuestionnaireResponse.class, ResearchStudy.class, + StructureDefinition.class, Subscription.class, Task.class, ValueSet.class); var searchParameters = new HashMap, List>>>(); var revIncludeParameters = new HashMap, List>>(); @@ -360,6 +370,12 @@ private CapabilityStatement createCapabilityStatement() Arrays.asList(PractitionerRoleActive.class, PractitionerRoleIdentifier.class, PractitionerRoleOrganization.class, PractitionerRolePractitioner.class)); + searchParameters.put(Questionnaire.class, Arrays.asList(QuestionnaireDate.class, QuestionnaireIdentifier.class, + QuestionnaireStatus.class, QuestionnaireUrl.class, QuestionnaireVersion.class)); + + searchParameters.put(QuestionnaireResponse.class, Arrays.asList(QuestionnaireResponseAuthored.class, + QuestionnaireResponseIdentifier.class, QuestionnaireResponseStatus.class)); + searchParameters.put(ResearchStudy.class, Arrays.asList(ResearchStudyIdentifier.class, ResearchStudyEnrollment.class, ResearchStudyPrincipalInvestigator.class)); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/impl/QuestionnaireResponseServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/impl/QuestionnaireResponseServiceImpl.java new file mode 100755 index 000000000..d31691dc3 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/impl/QuestionnaireResponseServiceImpl.java @@ -0,0 +1,33 @@ +package org.highmed.dsf.fhir.webservice.impl; + +import org.highmed.dsf.fhir.authorization.AuthorizationRuleProvider; +import org.highmed.dsf.fhir.dao.QuestionnaireResponseDao; +import org.highmed.dsf.fhir.event.EventGenerator; +import org.highmed.dsf.fhir.event.EventHandler; +import org.highmed.dsf.fhir.help.ExceptionHandler; +import org.highmed.dsf.fhir.help.ParameterConverter; +import org.highmed.dsf.fhir.help.ResponseGenerator; +import org.highmed.dsf.fhir.history.HistoryService; +import org.highmed.dsf.fhir.service.ReferenceCleaner; +import org.highmed.dsf.fhir.service.ReferenceExtractor; +import org.highmed.dsf.fhir.service.ReferenceResolver; +import org.highmed.dsf.fhir.validation.ResourceValidator; +import org.highmed.dsf.fhir.webservice.specification.QuestionnaireResponseService; +import org.hl7.fhir.r4.model.QuestionnaireResponse; + +public class QuestionnaireResponseServiceImpl + extends AbstractResourceServiceImpl + implements QuestionnaireResponseService +{ + public QuestionnaireResponseServiceImpl(String path, String serverBase, int defaultPageCount, + QuestionnaireResponseDao dao, ResourceValidator validator, EventHandler eventHandler, + ExceptionHandler exceptionHandler, EventGenerator eventGenerator, ResponseGenerator responseGenerator, + ParameterConverter parameterConverter, ReferenceExtractor referenceExtractor, + ReferenceResolver referenceResolver, ReferenceCleaner referenceCleaner, + AuthorizationRuleProvider authorizationRuleProvider, HistoryService historyService) + { + super(path, QuestionnaireResponse.class, serverBase, defaultPageCount, dao, validator, eventHandler, + exceptionHandler, eventGenerator, responseGenerator, parameterConverter, referenceExtractor, + referenceResolver, referenceCleaner, authorizationRuleProvider, historyService); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/impl/QuestionnaireServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/impl/QuestionnaireServiceImpl.java new file mode 100755 index 000000000..72e7a4a90 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/impl/QuestionnaireServiceImpl.java @@ -0,0 +1,32 @@ +package org.highmed.dsf.fhir.webservice.impl; + +import org.highmed.dsf.fhir.authorization.AuthorizationRuleProvider; +import org.highmed.dsf.fhir.dao.QuestionnaireDao; +import org.highmed.dsf.fhir.event.EventGenerator; +import org.highmed.dsf.fhir.event.EventHandler; +import org.highmed.dsf.fhir.help.ExceptionHandler; +import org.highmed.dsf.fhir.help.ParameterConverter; +import org.highmed.dsf.fhir.help.ResponseGenerator; +import org.highmed.dsf.fhir.history.HistoryService; +import org.highmed.dsf.fhir.service.ReferenceCleaner; +import org.highmed.dsf.fhir.service.ReferenceExtractor; +import org.highmed.dsf.fhir.service.ReferenceResolver; +import org.highmed.dsf.fhir.validation.ResourceValidator; +import org.highmed.dsf.fhir.webservice.specification.QuestionnaireService; +import org.hl7.fhir.r4.model.Questionnaire; + +public class QuestionnaireServiceImpl extends AbstractResourceServiceImpl + implements QuestionnaireService +{ + public QuestionnaireServiceImpl(String path, String serverBase, int defaultPageCount, + QuestionnaireDao questionnaireDao, ResourceValidator validator, EventHandler eventHandler, + ExceptionHandler exceptionHandler, EventGenerator eventGenerator, ResponseGenerator responseGenerator, + ParameterConverter parameterConverter, ReferenceExtractor referenceExtractor, + ReferenceResolver referenceResolver, ReferenceCleaner referenceCleaner, + AuthorizationRuleProvider authorizationRuleProvider, HistoryService historyService) + { + super(path, Questionnaire.class, serverBase, defaultPageCount, questionnaireDao, validator, eventHandler, + exceptionHandler, eventGenerator, responseGenerator, parameterConverter, referenceExtractor, + referenceResolver, referenceCleaner, authorizationRuleProvider, historyService); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/jaxrs/QuestionnaireResponseServiceJaxrs.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/jaxrs/QuestionnaireResponseServiceJaxrs.java new file mode 100755 index 000000000..7dc98dd76 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/jaxrs/QuestionnaireResponseServiceJaxrs.java @@ -0,0 +1,19 @@ +package org.highmed.dsf.fhir.webservice.jaxrs; + +import javax.ws.rs.Path; + +import org.highmed.dsf.fhir.webservice.specification.QuestionnaireResponseService; +import org.hl7.fhir.r4.model.QuestionnaireResponse; + +@Path(QuestionnaireResponseServiceJaxrs.PATH) +public class QuestionnaireResponseServiceJaxrs + extends AbstractResourceServiceJaxrs + implements QuestionnaireResponseService +{ + public static final String PATH = "QuestionnaireResponse"; + + public QuestionnaireResponseServiceJaxrs(QuestionnaireResponseService delegate) + { + super(delegate); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/jaxrs/QuestionnaireServiceJaxrs.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/jaxrs/QuestionnaireServiceJaxrs.java new file mode 100755 index 000000000..81b1bb4b6 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/jaxrs/QuestionnaireServiceJaxrs.java @@ -0,0 +1,18 @@ +package org.highmed.dsf.fhir.webservice.jaxrs; + +import javax.ws.rs.Path; + +import org.highmed.dsf.fhir.webservice.specification.QuestionnaireService; +import org.hl7.fhir.r4.model.Questionnaire; + +@Path(QuestionnaireServiceJaxrs.PATH) +public class QuestionnaireServiceJaxrs extends AbstractResourceServiceJaxrs + implements QuestionnaireService +{ + public static final String PATH = "Questionnaire"; + + public QuestionnaireServiceJaxrs(QuestionnaireService delegate) + { + super(delegate); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/secure/QuestionnaireResponseServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/secure/QuestionnaireResponseServiceSecure.java new file mode 100755 index 000000000..c3310a87f --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/secure/QuestionnaireResponseServiceSecure.java @@ -0,0 +1,29 @@ +package org.highmed.dsf.fhir.webservice.secure; + +import org.highmed.dsf.fhir.authorization.AuthorizationRule; +import org.highmed.dsf.fhir.dao.QuestionnaireResponseDao; +import org.highmed.dsf.fhir.help.ExceptionHandler; +import org.highmed.dsf.fhir.help.ParameterConverter; +import org.highmed.dsf.fhir.help.ResponseGenerator; +import org.highmed.dsf.fhir.service.ReferenceCleaner; +import org.highmed.dsf.fhir.service.ReferenceExtractor; +import org.highmed.dsf.fhir.service.ReferenceResolver; +import org.highmed.dsf.fhir.validation.ResourceValidator; +import org.highmed.dsf.fhir.webservice.specification.QuestionnaireResponseService; +import org.hl7.fhir.r4.model.QuestionnaireResponse; + +public class QuestionnaireResponseServiceSecure extends + AbstractResourceServiceSecure + implements QuestionnaireResponseService +{ + public QuestionnaireResponseServiceSecure(QuestionnaireResponseService delegate, String serverBase, + ResponseGenerator responseGenerator, ReferenceResolver referenceResolver, ReferenceCleaner referenceCleaner, + ReferenceExtractor referenceExtractor, QuestionnaireResponseDao QuestionnaireResponseDao, + ExceptionHandler exceptionHandler, ParameterConverter parameterConverter, + AuthorizationRule authorizationRule, ResourceValidator resourceValidator) + { + super(delegate, serverBase, responseGenerator, referenceResolver, referenceCleaner, referenceExtractor, + QuestionnaireResponse.class, QuestionnaireResponseDao, exceptionHandler, parameterConverter, + authorizationRule, resourceValidator); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/secure/QuestionnaireServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/secure/QuestionnaireServiceSecure.java new file mode 100755 index 000000000..de96fccf9 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/secure/QuestionnaireServiceSecure.java @@ -0,0 +1,29 @@ +package org.highmed.dsf.fhir.webservice.secure; + +import org.highmed.dsf.fhir.authorization.AuthorizationRule; +import org.highmed.dsf.fhir.dao.QuestionnaireDao; +import org.highmed.dsf.fhir.help.ExceptionHandler; +import org.highmed.dsf.fhir.help.ParameterConverter; +import org.highmed.dsf.fhir.help.ResponseGenerator; +import org.highmed.dsf.fhir.service.ReferenceCleaner; +import org.highmed.dsf.fhir.service.ReferenceExtractor; +import org.highmed.dsf.fhir.service.ReferenceResolver; +import org.highmed.dsf.fhir.validation.ResourceValidator; +import org.highmed.dsf.fhir.webservice.specification.QuestionnaireService; +import org.hl7.fhir.r4.model.Questionnaire; + +public class QuestionnaireServiceSecure + extends AbstractResourceServiceSecure + implements QuestionnaireService +{ + public QuestionnaireServiceSecure(QuestionnaireService delegate, String serverBase, + ResponseGenerator responseGenerator, ReferenceResolver referenceResolver, ReferenceCleaner referenceCleaner, + ReferenceExtractor referenceExtractor, QuestionnaireDao questionnaireDao, ExceptionHandler exceptionHandler, + ParameterConverter parameterConverter, AuthorizationRule authorizationRule, + ResourceValidator resourceValidator) + { + super(delegate, serverBase, responseGenerator, referenceResolver, referenceCleaner, referenceExtractor, + Questionnaire.class, questionnaireDao, exceptionHandler, parameterConverter, authorizationRule, + resourceValidator); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/specification/QuestionnaireResponseService.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/specification/QuestionnaireResponseService.java new file mode 100755 index 000000000..266a2b0ad --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/specification/QuestionnaireResponseService.java @@ -0,0 +1,7 @@ +package org.highmed.dsf.fhir.webservice.specification; + +import org.hl7.fhir.r4.model.QuestionnaireResponse; + +public interface QuestionnaireResponseService extends BasicResourceService +{ +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/specification/QuestionnaireService.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/specification/QuestionnaireService.java new file mode 100755 index 000000000..22f2c706c --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/specification/QuestionnaireService.java @@ -0,0 +1,7 @@ +package org.highmed.dsf.fhir.webservice.specification; + +import org.hl7.fhir.r4.model.Questionnaire; + +public interface QuestionnaireService extends BasicResourceService +{ +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.changelog.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.changelog.xml index 2d2021fd6..5a3e26361 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.changelog.xml +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.changelog.xml @@ -61,4 +61,8 @@ + + + + diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.questionnaire_responses.changelog-0.8.0.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.questionnaire_responses.changelog-0.8.0.xml new file mode 100644 index 000000000..561158604 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.questionnaire_responses.changelog-0.8.0.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + ALTER TABLE questionnaire_responses OWNER TO ${db.liquibase_user}; + GRANT ALL ON TABLE questionnaire_responses TO ${db.liquibase_user}; + GRANT SELECT, INSERT, UPDATE ON TABLE questionnaire_responses TO ${db.server_users_group}; + GRANT SELECT, DELETE ON TABLE questionnaire_responses TO ${db.server_permanent_delete_users_group}; + CREATE INDEX questionnaire_response_id_index ON questionnaire_responses USING btree (questionnaire_response_id); + CREATE INDEX questionnaire_response_index ON questionnaire_responses USING gin (questionnaire_response); + CREATE INDEX questionnaire_response_id_version_index ON questionnaire_responses USING btree (questionnaire_response_id, version); + + + + SELECT questionnaire_response_id, version, questionnaire_response + FROM ( + SELECT DISTINCT ON (questionnaire_response_id) questionnaire_response_id, version, deleted, questionnaire_response + FROM questionnaire_responses ORDER BY questionnaire_response_id, version DESC + ) AS current_l + WHERE deleted IS NULL + + + + ALTER TABLE current_questionnaire_responses OWNER TO ${db.liquibase_user}; + GRANT ALL ON TABLE current_questionnaire_responses TO ${db.liquibase_user}; + GRANT SELECT ON TABLE current_questionnaire_responses TO ${db.server_users_group}; + + + diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.questionnaires.changelog-0.8.0.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.questionnaires.changelog-0.8.0.xml new file mode 100644 index 000000000..a99542866 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.questionnaires.changelog-0.8.0.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + ALTER TABLE questionnaires OWNER TO ${db.liquibase_user}; + GRANT ALL ON TABLE questionnaires TO ${db.liquibase_user}; + GRANT SELECT, INSERT, UPDATE ON TABLE questionnaires TO ${db.server_users_group}; + GRANT SELECT, DELETE ON TABLE questionnaires TO ${db.server_permanent_delete_users_group}; + CREATE INDEX questionnaire_id_index ON questionnaires USING btree (questionnaire_id); + CREATE INDEX questionnaire_index ON questionnaires USING gin (questionnaire); + CREATE INDEX questionnaire_id_version_index ON questionnaires USING btree (questionnaire_id, version); + + + + SELECT questionnaire_id, version, questionnaire + FROM ( + SELECT DISTINCT ON (questionnaire_id) questionnaire_id, version, deleted, questionnaire + FROM questionnaires ORDER BY questionnaire_id, version DESC + ) AS current_l + WHERE deleted IS NULL + + + + ALTER TABLE current_questionnaires OWNER TO ${db.liquibase_user}; + GRANT ALL ON TABLE current_questionnaires TO ${db.liquibase_user}; + GRANT SELECT ON TABLE current_questionnaires TO ${db.server_users_group}; + + + diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.read_access.changelog-0.8.0.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.read_access.changelog-0.8.0.xml new file mode 100644 index 000000000..ec03e21a6 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.read_access.changelog-0.8.0.xml @@ -0,0 +1,99 @@ + + + + + + + + + SELECT + id + , version + , type + , resource + FROM ( + SELECT activity_definition_id AS id, version, 'ActivityDefinition'::text AS type, activity_definition AS resource FROM current_activity_definitions + UNION + SELECT binary_id AS id, version, 'Binary'::text AS type, binary_json AS resource FROM current_binaries + UNION + SELECT bundle_id AS id, version, 'Bundle'::text AS type, bundle AS resource FROM current_bundles + UNION + SELECT code_system_id AS id, version, 'CodeSystem'::text AS type, code_system AS resource FROM current_code_systems + UNION + SELECT document_reference_id AS id, version, 'DocumentReference'::text AS type, document_reference AS resource FROM current_document_references + UNION + SELECT endpoint_id AS id, version, 'Endpoint'::text AS type, endpoint AS resource FROM current_endpoints + UNION + SELECT group_id AS id, version, 'Group'::text AS type, group_json AS resource FROM current_groups + UNION + SELECT healthcare_service_id AS id, version, 'HealthcareService'::text AS type, healthcare_service AS resource FROM current_healthcare_services + UNION + SELECT library_id AS id, version, 'Library'::text AS type, library AS resource FROM current_libraries + UNION + SELECT location_id AS id, version, 'Location'::text AS type, location AS resource FROM current_locations + UNION + SELECT measure_report_id AS id, version, 'MeasureReport'::text AS type, measure_report AS resource FROM current_measure_reports + UNION + SELECT measure_id AS id, version, 'Measure'::text AS type, measure AS resource FROM current_measures + UNION + SELECT naming_system_id AS id, version, 'NamingSystem'::text AS type, naming_system AS resource FROM current_naming_systems + UNION + SELECT organization_id AS id, version, 'Organization'::text AS type, organization AS resource FROM current_organizations + UNION + SELECT organization_affiliation_id AS id, version, 'OrganizationAffiliation'::text AS type, organization_affiliation AS resource FROM current_organization_affiliations + UNION + SELECT patient_id AS id, version, 'Patient'::text AS type, patient AS resource FROM current_patients + UNION + SELECT practitioner_role_id AS id, version, 'PractitionerRole'::text AS type, practitioner_role AS resource FROM current_practitioner_roles + UNION + SELECT practitioner_id AS id, version, 'Practitioner'::text AS type, practitioner AS resource FROM current_practitioners + UNION + SELECT provenance_id AS id, version, 'Provenance'::text AS type, provenance AS resource FROM current_provenances + UNION + SELECT questionnaire_id AS id, version, 'Questionnaire'::text AS type, questionnaire AS resource FROM current_questionnaires + UNION + SELECT questionnaire_response_id AS id, version, 'QuestionnaireResponse'::text AS type, questionnaire_response AS resource FROM current_questionnaire_responses + UNION + SELECT research_study_id AS id, version, 'ResearchStudy'::text AS type, research_study AS resource FROM current_research_studies + UNION + SELECT structure_definition_id AS id, version, 'StructureDefinition'::text AS type, structure_definition AS resource FROM current_structure_definitions + UNION + SELECT subscription_id AS id, version, 'Subscription'::text AS type, subscription AS resource FROM current_subscriptions + UNION + SELECT task_id AS id, version, 'Task'::text AS type, task AS resource FROM current_tasks + UNION + SELECT value_set_id AS id, version, 'ValueSet'::text AS type, value_set AS resource FROM current_value_sets + ) AS current_all + + + ALTER TABLE all_resources OWNER TO ${db.liquibase_user}; + GRANT ALL ON TABLE all_resources TO ${db.liquibase_user}; + GRANT SELECT ON TABLE all_resources TO ${db.server_users_group}; + + + + + + + + + + + + + + + + + + + + CREATE TRIGGER questionnaires_insert AFTER INSERT ON questionnaires FOR EACH ROW EXECUTE PROCEDURE on_questionnaires_insert(); + CREATE TRIGGER questionnaires_update AFTER UPDATE ON questionnaires FOR EACH ROW EXECUTE PROCEDURE on_questionnaires_update(); + CREATE TRIGGER questionnaire_responses_insert AFTER INSERT ON questionnaire_responses FOR EACH ROW EXECUTE PROCEDURE on_questionnaire_responses_insert(); + CREATE TRIGGER questionnaire_responses_update AFTER UPDATE ON questionnaire_responses FOR EACH ROW EXECUTE PROCEDURE on_questionnaire_responses_update(); + + + \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaire_responses_insert.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaire_responses_insert.sql new file mode 100644 index 000000000..ad795ce67 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaire_responses_insert.sql @@ -0,0 +1,6 @@ +CREATE OR REPLACE FUNCTION on_questionnaire_responses_insert() RETURNS TRIGGER AS $$ +BEGIN + PERFORM on_resources_insert(NEW.questionnaire_response_id, NEW.version, NEW.questionnaire_response); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaire_responses_update.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaire_responses_update.sql new file mode 100644 index 000000000..a5161a7db --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaire_responses_update.sql @@ -0,0 +1,6 @@ +CREATE OR REPLACE FUNCTION on_questionnaire_responses_update() RETURNS TRIGGER AS $$ +BEGIN + PERFORM on_resources_update(NEW.deleted, NEW.questionnaire_response_id, NEW.version, NEW.questionnaire_response); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaires_insert.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaires_insert.sql new file mode 100644 index 000000000..121524eeb --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaires_insert.sql @@ -0,0 +1,6 @@ +CREATE OR REPLACE FUNCTION on_questionnaires_insert() RETURNS TRIGGER AS $$ +BEGIN + PERFORM on_resources_insert(NEW.questionnaire_id, NEW.version, NEW.questionnaire); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaires_update.sql b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaires_update.sql new file mode 100644 index 000000000..1b4d84d82 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/trigger_functions/on_questionnaires_update.sql @@ -0,0 +1,6 @@ +CREATE OR REPLACE FUNCTION on_questionnaires_update() RETURNS TRIGGER AS $$ +BEGIN + PERFORM on_resources_update(NEW.deleted, NEW.questionnaire_id, NEW.version, NEW.questionnaire); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.css b/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.css new file mode 100644 index 000000000..8deb6d22e --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.css @@ -0,0 +1,207 @@ +form { + border: 1px solid #ccc; + background-color: #ffffff; + padding: 20px 20px 10px 20px; + font-family: Epilogue, sans-serif; +} + +fieldset#qr-form-fieldset { + display: block; + margin: 0; + padding: 0; + min-inline-size: min-content; + border: none; +} + +.row { + border-radius: 5px; + padding: 0 15px 15px 15px; + margin-bottom: 10px; + background-color: #f2f2f2; +} + +.error { + background-color: #ffa590; +} + +.error-list-not-visible { + display: none; +} + +.error-list-visible { + display: block; + font-size: small; + color: #470003; + padding-left: 20px; + margin-bottom: 0; +} + +.row-info { + display: flex; + padding-top: 15px; +} + +.info-color-completed { + background-color: #a3c585; + color: #4b6043; +} + +.info-color-progress { + background-color: #edd273; + color: #864401; +} + +.info-link { + font-family: monospace; + font-size: 130%; +} + +.info-link-completed { + color: #4b6043; +} + +.info-link-progress { + color: #864401; +} + +.info-link:active { + font-family: monospace; + font-size: 130%; +} + +.info-link-completed:active { + color: #4b6043; +} + +.info-link-progress:active { + color: #864401; +} + +.info-icon { + padding-top: 15px; + padding-right: 15px; + height: 35px; + width: 35px; +} + +.info-icon > path.info-path-completed { + fill: #4b6043; +} + +.info-icon > path.info-path-progress { + fill: #864401; +} + +.info-list { + padding-left: 20px; +} + +.row:after { + content: ""; + display: table; + clear: both; +} + + +.row-submit { + background-color: #ffffff; + padding: 0; +} + +label { + display: block; + padding: 12px 12px 12px 0; + font-weight: 500; +} + +label.radio { + padding: 5px 12px 5px 0; + font-weight: 100; +} + +input[type=radio] { + margin-right: 7px; +} + +input[type=text], input[type=url], select, textarea { + width: 100%; + padding: 12px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + resize: vertical; + display: block; +} + +input[type=date], input[type=time], input[type=datetime-local], input[type=number] { + width: auto; + min-width: 250px; + padding: 12px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + resize: none; + display: block; +} + +button.submit { + background-color: #29235c; + color: white; + padding: 12px 60px; + border: none; + border-radius: 4px; + cursor: pointer; + float: left; +} + +button.submit:disabled, +button.submit[disabled] { + background-color: #ccc; + color: #555; + cursor: not-allowed; +} + +.spinner-enabled { + display: block; +} + +.spinner-disabled { + display: none; +} + +.spinner { + position: fixed; + width: 100%; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: rgba(255,255,255,0.7); + z-index: 9999; +} + +@-webkit-keyframes spin { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.spinner::after { + content: ''; + position: absolute; + left: 48%; + top: 40%; + width: 40px; + height: 40px; + border-style: solid; + border-color: #29235c; + border-top-color: transparent; + border-width: 4px; + border-radius: 50%; + -webkit-animation: spin .8s linear infinite; + animation: spin .8s linear infinite; +} \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.js b/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.js new file mode 100644 index 000000000..2b1a7b0ca --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.js @@ -0,0 +1,222 @@ +function completeQuestionnaireResponse() { + const questionnaireResponseStringBefore = document.getElementById("json").innerText + const questionnaireResponse = JSON.parse(questionnaireResponseStringBefore) + + const errors = [] + readAnswersFromForm(questionnaireResponse, errors) + + console.log(questionnaireResponse) + console.log(errors) + + if (errors.length === 0) { + const questionnaireResponseStringAfter = JSON.stringify(questionnaireResponse) + updateQuestionnaireResponse(questionnaireResponseStringAfter) + } +} + +function readAnswersFromForm(questionnaireResponse, errors) { + questionnaireResponse.status = "completed"; + + questionnaireResponse.item.forEach((item) => { + const id = item.linkId + const answer = item.answer[0] + const answerType = Object.keys(answer)[0] + + if (id !== "business-key" && id !== "user-task-id") { + answer[answerType] = readAndValidateValue(id, answerType, errors) + } + }) +} + +function readAndValidateValue(id, answerType, errors) { + const value = document.getElementById(id).value + + const rowElement = document.getElementById(id + "-row"); + const errorListElement = document.getElementById(id + "-error"); + errorListElement.replaceChildren() + + if (answerType === 'valueString') { + return validateString(rowElement, errorListElement, value, errors, id) + } else if (answerType === 'valueInteger') { + return validateInteger(rowElement, errorListElement, value, errors, id) + } else if (answerType === 'valueDecimal') { + return validateDecimal(rowElement, errorListElement, value, errors, id) + } else if (answerType === 'valueDate') { + return validateDate(rowElement, errorListElement, value, errors, id) + } else if (answerType === 'valueTime') { + return validateTime(rowElement, errorListElement, value, errors, id) + } else if (answerType === 'valueDateTime') { + return validateDateTime(rowElement, errorListElement, value, errors, id) + } else if (answerType === 'valueUri') { + return validateUrl(rowElement, errorListElement, value, errors, id) + } else if (answerType === 'valueReference') { + return validateReference(rowElement, errorListElement, value, errors, id) + } else if (answerType === 'valueBoolean') { + return document.querySelector("input[name=" + id + "]:checked").value + } else { + return null + } +} + +function validateString(rowElement, errorListElement, value, errors, id) { + if (value === null || value.trim() === "") { + addError(rowElement, errorListElement, errors, id, "Value is null or empty") + return null + } else { + removeError(rowElement, errorListElement) + return value + } +} + +function validateInteger(rowElement, errorListElement, value, errors, id) { + validateString(rowElement, errorListElement, value, errors, id) + + if (!Number.isInteger(parseInt(value))) { + addError(rowElement, errorListElement, errors, id, "Value is not an integer") + return null + } else { + removeError(rowElement, errorListElement) + return value + } +} + +function validateDecimal(rowElement, errorListElement, value, errors, id) { + validateString(rowElement, errorListElement, value, errors, id) + + if (isNaN(parseFloat(value))) { + addError(rowElement, errorListElement, errors, id, "Value is not a decimal") + return null + } else { + removeError(rowElement, errorListElement) + return value + } +} + +function validateDate(rowElement, errorListElement, value, errors, id) { + validateString(rowElement, errorListElement, value, errors, id) + + const date = new Date(value) + if ((date === "Invalid Date") || isNaN(date)) { + addError(rowElement, errorListElement, errors, id, "Value is not a date") + return null + } else { + removeError(rowElement, errorListElement) + return value + } +} + +function validateTime(rowElement, errorListElement, value, errors, id) { + validateString(rowElement, errorListElement, value, errors, id) + + if (!(new RegExp('^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$').test(value))) { + addError(rowElement, errorListElement, errors, id, "Value is not a time") + return null + } else { + removeError(rowElement, errorListElement) + return value + ":00" + } +} + +function validateDateTime(rowElement, errorListElement, value, errors, id) { + validateString(rowElement, errorListElement, value, errors, id) + + try { + const dateTime = new Date(value).toISOString() + removeError(rowElement, errorListElement) + return dateTime + } catch (_) { + addError(rowElement, errorListElement, errors, id, "Value is not a date time") + return null + } +} + +function validateReference(rowElement, errorListElement, value, errors, id) { + validateString(rowElement, errorListElement, value, errors, id) + + try { + new URL(value); + removeError(rowElement, errorListElement) + return {reference: value} + } catch (_) { + addError(rowElement, errorListElement, errors, id, "Value is not a reference") + return null + } +} + +function validateUrl(rowElement, errorListElement, value, errors, id) { + validateString(rowElement, errorListElement, value, errors, id) + + try { + new URL(value); + removeError(rowElement, errorListElement) + return value + } catch (_) { + addError(rowElement, errorListElement, errors, id, "Value is not a url") + return null + } +} + +function addError(rowElement, errorListElement, errors, id, message) { + errors.push({id: id, error: message}) + + rowElement.classList.add("error") + + const errorMessageElement = document.createElement("li"); + errorMessageElement.appendChild(document.createTextNode(message)); + + errorListElement.appendChild(errorMessageElement); + errorListElement.classList.remove("error-list-not-visible"); + errorListElement.classList.add("error-list-visible"); +} + +function removeError(rowElement, errorListElement) { + rowElement.classList.remove("error"); + + errorListElement.classList.remove("error-list-visible"); + errorListElement.classList.add("error-list-not-visible"); + errorListElement.replaceChildren() +} + +function updateQuestionnaireResponse(questionnaireResponse) { + const fullUrl = window.location.origin + window.location.pathname + const url = fullUrl.slice(0, fullUrl.indexOf("/_history") + 1) + + enableSpinner() + + fetch(url, { + method: "PUT", + headers: { + 'Content-type': 'application/json' + }, + body: questionnaireResponse + }).then(response => { + console.log(response) + + if (response.ok) { + disableSpinner() + window.scrollTo(0, 0); + location.reload(); + } else { + const status = response.status + const statusText = response.statusText === null ? " - " + response.statusText : "" + + response.text().then((responseText) => { + const alertText = "Status: " + status + statusText + "\n\n" + responseText.replace(//sg, "") + window.alert(alertText); + disableSpinner() + }) + } + }) +} + +function enableSpinner() { + const spinner = document.getElementById("spinner") + spinner.classList.remove("spinner-disabled") + spinner.classList.add("spinner-enabled") +} + +function disableSpinner() { + const spinner = document.getElementById("spinner") + spinner.classList.remove("spinner-enabled") + spinner.classList.add("spinner-disabled") +} \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/highmed.css b/dsf-fhir/dsf-fhir-server/src/main/resources/static/highmed.css index 30251ca20..3ba503411 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/highmed.css +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/highmed.css @@ -58,6 +58,11 @@ pre { margin-top: 0; } +pre.lang-html { + border: 0; + font-family: monospace; +} + li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 { list-style-type: decimal !important; } diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/tabs.js b/dsf-fhir/dsf-fhir-server/src/main/resources/static/tabs.js index 56a74d9e0..ca0724177 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/tabs.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/tabs.js @@ -1,85 +1,91 @@ function openTab(lang) { - const tabcontent = document.getElementsByClassName("prettyprint"); - for (let i = 0; i < tabcontent.length; i++) { - tabcontent[i].style.display = "none"; - } + const tabcontent = document.getElementsByClassName("prettyprint"); + for (let i = 0; i < tabcontent.length; i++) { + tabcontent[i].style.display = "none"; + } - const tablinks = document.getElementsByClassName("tablinks"); - for (let i = 0; i < tablinks.length; i++) { - tablinks[i].className = tablinks[i].className.replace(" active", ""); - } + const tablinks = document.getElementsByClassName("tablinks"); + for (let i = 0; i < tablinks.length; i++) { + tablinks[i].className = tablinks[i].className.replace(" active", ""); + } - document.getElementById(lang).style.display = "block"; - document.getElementById(lang + "-button").className += " active"; + document.getElementById(lang).style.display = "block"; + document.getElementById(lang + "-button").className += " active"; - if (localStorage != null) - localStorage.setItem('lang', lang); + if (lang != "html" && localStorage != null) + localStorage.setItem('lang', lang); + + if (lang == "html") + lang = localStorage != null && localStorage.getItem("lang") != null ? localStorage.getItem("lang") : "xml"; - setDownloadLink(lang); + setDownloadLink(lang); } -function openInitialTab() { - const lang = localStorage != null && localStorage.getItem("lang") != null ? localStorage.getItem("lang") : "xml"; - if (lang == "xml" || lang == "json") - openTab(lang); +function openInitialTab(htmlEnabled) { + if (htmlEnabled) + openTab("html"); + else { + const lang = localStorage != null && localStorage.getItem("lang") != null ? localStorage.getItem("lang") : "xml"; + if (lang == "xml" || lang == "json") + openTab(lang); + } } function setDownloadLink(lang) { - const searchParams = new URLSearchParams(document.location.search); - searchParams.set('_format', lang); - searchParams.set('_pretty', 'true'); + const searchParams = new URLSearchParams(document.location.search); + searchParams.set('_format', lang); + searchParams.set('_pretty', 'true'); - const downloadLink = document.getElementById('download-link'); - downloadLink.href = '?' + searchParams.toString(); - downloadLink.download = getDownloadFileName(lang); - downloadLink.title = 'Download as ' + lang.toUpperCase(); + const downloadLink = document.getElementById('download-link'); + downloadLink.href = '?' + searchParams.toString(); + downloadLink.download = getDownloadFileName(lang); + downloadLink.title = 'Download as ' + lang.toUpperCase(); } function getDownloadFileName(lang) { - const resourceType = getResourceTypeForCurrentUrl(); + const resourceType = getResourceTypeForCurrentUrl(); - /* /, /metadata, /_history */ - if (resourceType == null) { - if (window.location.pathname.endsWith('/metadata')) { - return "metadata." + lang; - } else if (window.location.pathname.endsWith('/_history')) { - return "history." + lang; - } else { - return "root." + lang; - } - } - else { - //Resource - if (resourceType[1] !== undefined && resourceType[2] === undefined && resourceType[3] === undefined && resourceType[4] === undefined) { - return resourceType[1] + '_Search.' + lang; - } - //Resource/_history - else if (resourceType[1] !== undefined && resourceType[2] === undefined && resourceType[3] !== undefined && resourceType[4] === undefined) { - return resourceType[1] + '_History.' + lang; - } - //Resource/id - else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] === undefined && resourceType[4] === undefined) { - return resourceType[1] + '_' + resourceType[2].replace('/','') + '.' + lang; - } - //Resource/id/_history - else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] !== undefined && resourceType[4] === undefined) { - return resourceType[1] + '_' + resourceType[2].replace('/','') + '_History.' + lang; - } - //Resource/id/_history/version - else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] !== undefined && resourceType[4] !== undefined) { - return resourceType[1] + '_' + resourceType[2].replace('/','') + '_v' + resourceType[4].replace('/','') + '.' + lang; - } - } + /* /, /metadata, /_history */ + if (resourceType == null) { + if (window.location.pathname.endsWith('/metadata')) { + return "metadata." + lang; + } else if (window.location.pathname.endsWith('/_history')) { + return "history." + lang; + } else { + return "root." + lang; + } + } else { + //Resource + if (resourceType[1] !== undefined && resourceType[2] === undefined && resourceType[3] === undefined && resourceType[4] === undefined) { + return resourceType[1] + '_Search.' + lang; + } + //Resource/_history + else if (resourceType[1] !== undefined && resourceType[2] === undefined && resourceType[3] !== undefined && resourceType[4] === undefined) { + return resourceType[1] + '_History.' + lang; + } + //Resource/id + else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] === undefined && resourceType[4] === undefined) { + return resourceType[1] + '_' + resourceType[2].replace('/', '') + '.' + lang; + } + //Resource/id/_history + else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] !== undefined && resourceType[4] === undefined) { + return resourceType[1] + '_' + resourceType[2].replace('/', '') + '_History.' + lang; + } + //Resource/id/_history/version + else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] !== undefined && resourceType[4] !== undefined) { + return resourceType[1] + '_' + resourceType[2].replace('/', '') + '_v' + resourceType[4].replace('/', '') + '.' + lang; + } + } } function getResourceTypeForCurrentUrl() { - const url = window.location.pathname; - const regex = new RegExp('(?:(?:[A-Za-z0-9\-\\\.\:\%\$]*\/)+)?' - + '(Account|ActivityDefinition|AdverseEvent|AllergyIntolerance|Appointment|AppointmentResponse|AuditEvent|Basic|Binary|BiologicallyDerivedProduct|BodyStructure|Bundle|CapabilityStatement|CarePlan|CareTeam|CatalogEntry|ChargeItem|ChargeItemDefinition|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition|Consent|Contract|Coverage|CoverageEligibilityRequest|CoverageEligibilityResponse|DetectedIssue|Device|DeviceDefinition|DeviceMetric|DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EffectEvidenceSynthesis|Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|EventDefinition|Evidence|EvidenceVariable|ExampleScenario|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|HealthcareService|ImagingStudy|Immunization|ImmunizationEvaluation|ImmunizationRecommendation|ImplementationGuide|InsurancePlan|Invoice|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|MedicationAdministration|MedicationDispense|MedicationKnowledge|MedicationRequest|MedicationStatement|MedicinalProduct|MedicinalProductAuthorization|MedicinalProductContraindication|MedicinalProductIndication|MedicinalProductIngredient|MedicinalProductInteraction|MedicinalProductManufactured|MedicinalProductPackaged|MedicinalProductPharmaceutical|MedicinalProductUndesirableEffect|MessageDefinition|MessageHeader|MolecularSequence|NamingSystem|NutritionOrder|Observation|ObservationDefinition|OperationDefinition|OperationOutcome|Organization|OrganizationAffiliation|Patient|PaymentNotice|PaymentReconciliation|Person|PlanDefinition|Practitioner|PractitionerRole|Procedure|Provenance|Questionnaire|QuestionnaireResponse|RelatedPerson|RequestGroup|ResearchDefinition|ResearchElementDefinition|ResearchStudy|ResearchSubject|RiskAssessment|RiskEvidenceSynthesis|Schedule|SearchParameter|ServiceRequest|Slot|Specimen|SpecimenDefinition|StructureDefinition|StructureMap|Subscription|Substance|SubstanceNucleicAcid|SubstancePolymer|SubstanceProtein|SubstanceReferenceInformation|SubstanceSourceMaterial|SubstanceSpecification|SupplyDelivery|SupplyRequest|Task|TerminologyCapabilities|TestReport|TestScript|ValueSet|VerificationResult|VisionPrescription)' - + '(?:(?:\/([A-Za-z0-9\-\.]{1,64}))?(?:\/(_history)(?:\/([0-9]{1,64}))?)?)?(?:\\?.*)?$'); - const match = regex.exec(url); - if (match != null) - return match; - else - return null; + const url = window.location.pathname; + const regex = new RegExp('(?:(?:[A-Za-z0-9\-\\\.\:\%\$]*\/)+)?' + + '(Account|ActivityDefinition|AdverseEvent|AllergyIntolerance|Appointment|AppointmentResponse|AuditEvent|Basic|Binary|BiologicallyDerivedProduct|BodyStructure|Bundle|CapabilityStatement|CarePlan|CareTeam|CatalogEntry|ChargeItem|ChargeItemDefinition|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition|Consent|Contract|Coverage|CoverageEligibilityRequest|CoverageEligibilityResponse|DetectedIssue|Device|DeviceDefinition|DeviceMetric|DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EffectEvidenceSynthesis|Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|EventDefinition|Evidence|EvidenceVariable|ExampleScenario|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|HealthcareService|ImagingStudy|Immunization|ImmunizationEvaluation|ImmunizationRecommendation|ImplementationGuide|InsurancePlan|Invoice|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|MedicationAdministration|MedicationDispense|MedicationKnowledge|MedicationRequest|MedicationStatement|MedicinalProduct|MedicinalProductAuthorization|MedicinalProductContraindication|MedicinalProductIndication|MedicinalProductIngredient|MedicinalProductInteraction|MedicinalProductManufactured|MedicinalProductPackaged|MedicinalProductPharmaceutical|MedicinalProductUndesirableEffect|MessageDefinition|MessageHeader|MolecularSequence|NamingSystem|NutritionOrder|Observation|ObservationDefinition|OperationDefinition|OperationOutcome|Organization|OrganizationAffiliation|Patient|PaymentNotice|PaymentReconciliation|Person|PlanDefinition|Practitioner|PractitionerRole|Procedure|Provenance|Questionnaire|QuestionnaireResponse|RelatedPerson|RequestGroup|ResearchDefinition|ResearchElementDefinition|ResearchStudy|ResearchSubject|RiskAssessment|RiskEvidenceSynthesis|Schedule|SearchParameter|ServiceRequest|Slot|Specimen|SpecimenDefinition|StructureDefinition|StructureMap|Subscription|Substance|SubstanceNucleicAcid|SubstancePolymer|SubstanceProtein|SubstanceReferenceInformation|SubstanceSourceMaterial|SubstanceSpecification|SupplyDelivery|SupplyRequest|Task|TerminologyCapabilities|TestReport|TestScript|ValueSet|VerificationResult|VisionPrescription)' + + '(?:(?:\/([A-Za-z0-9\-\.]{1,64}))?(?:\/(_history)(?:\/([0-9]{1,64}))?)?)?(?:\\?.*)?$'); + const match = regex.exec(url); + if (match != null) + return match; + else + return null; } \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/dao/QuestionnaireDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/dao/QuestionnaireDaoTest.java new file mode 100755 index 000000000..2b10b1f2d --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/dao/QuestionnaireDaoTest.java @@ -0,0 +1,297 @@ +package org.highmed.dsf.fhir.dao; + +import static org.junit.Assert.assertEquals; + +import org.highmed.dsf.fhir.dao.jdbc.QuestionnaireDaoJdbc; +import org.hl7.fhir.r4.model.Questionnaire; +import org.junit.Test; + +public class QuestionnaireDaoTest extends AbstractResourceDaoTest + implements ReadByUrlDaoTest, ReadAccessDaoTest +{ + private static final String name = "Demo Questionnaire"; + private static final String description = "Demo Questionnaire Description"; + + public QuestionnaireDaoTest() + { + super(Questionnaire.class, QuestionnaireDaoJdbc::new); + } + + @Override + public Questionnaire createResource() + { + Questionnaire questionnaire = new Questionnaire(); + questionnaire.setName(name); + return questionnaire; + } + + @Override + protected void checkCreated(Questionnaire resource) + { + assertEquals(name, resource.getName()); + } + + @Override + protected Questionnaire updateResource(Questionnaire resource) + { + resource.setDescription(description); + return resource; + } + + @Override + protected void checkUpdates(Questionnaire resource) + { + assertEquals(description, resource.getDescription()); + } + + @Override + public Questionnaire createResourceWithUrlAndVersion() + { + Questionnaire resource = createResource(); + resource.setUrl(getUrl()); + resource.setVersion(getVersion()); + return resource; + } + + @Override + public String getUrl() + { + return "http://test.com/fhir/Questionnaire/test-questionnaire"; + } + + @Override + public String getVersion() + { + return "0.6.0"; + } + + @Override + public ReadByUrlDao readByUrlDao() + { + return getDao(); + } + + @Override + @Test + public void testReadByUrlAndVersionWithUrl1() throws Exception + { + ReadByUrlDaoTest.super.testReadByUrlAndVersionWithUrl1(); + } + + @Override + @Test + public void testReadByUrlAndVersionWithUrlAndVersion1() throws Exception + { + ReadByUrlDaoTest.super.testReadByUrlAndVersionWithUrlAndVersion1(); + } + + @Override + @Test + public void testReadByUrlAndVersionWithUrl2() throws Exception + { + ReadByUrlDaoTest.super.testReadByUrlAndVersionWithUrl2(); + } + + @Override + @Test + public void testReadByUrlAndVersionWithUrlAndVersion2() throws Exception + { + ReadByUrlDaoTest.super.testReadByUrlAndVersionWithUrlAndVersion2(); + } + + @Override + @Test + public void testReadAccessTriggerAll() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerAll(); + } + + @Override + @Test + public void testReadAccessTriggerLocal() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerLocal(); + } + + @Override + @Test + public void testReadAccessTriggerOrganization() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganization(); + } + + @Override + @Test + public void testReadAccessTriggerOrganizationResourceFirst() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganizationResourceFirst(); + } + + @Override + @Test + public void testReadAccessTriggerOrganization2Organizations1Matching() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganization2Organizations1Matching(); + } + + @Override + @Test + public void testReadAccessTriggerOrganization2Organizations2Matching() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganization2Organizations2Matching(); + } + + @Override + @Test + public void testReadAccessTriggerRole() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRole(); + } + + @Override + @Test + public void testReadAccessTriggerRoleResourceFirst() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleResourceFirst(); + } + + @Override + @Test + public void testReadAccessTriggerRole2Organizations1Matching() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRole2Organizations1Matching(); + } + + @Override + @Test + public void testReadAccessTriggerRole2Organizations2Matching() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRole2Organizations2Matching(); + } + + @Override + @Test + public void testReadAccessTriggerAllUpdate() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerAllUpdate(); + } + + @Override + @Test + public void testReadAccessTriggerLocalUpdate() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerLocalUpdate(); + } + + @Override + @Test + public void testReadAccessTriggerOrganizationUpdate() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganizationUpdate(); + } + + @Override + @Test + public void testReadAccessTriggerRoleUpdate() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleUpdate(); + } + + @Override + @Test + public void testReadAccessTriggerRoleUpdateMemberOrganizationNonActive() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleUpdateMemberOrganizationNonActive(); + } + + @Override + @Test + public void testReadAccessTriggerRoleUpdateParentOrganizationNonActive() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleUpdateParentOrganizationNonActive(); + } + + @Override + @Test + public void testReadAccessTriggerRoleUpdateMemberAndParentOrganizationNonActive() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleUpdateMemberAndParentOrganizationNonActive(); + } + + @Override + @Test + public void testReadAccessTriggerAllDelete() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerAllDelete(); + } + + @Override + @Test + public void testReadAccessTriggerLocalDelete() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerLocalDelete(); + } + + @Override + @Test + public void testReadAccessTriggerOrganizationDelete() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganizationDelete(); + } + + @Override + @Test + public void testReadAccessTriggerRoleDelete() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleDelete(); + } + + @Override + @Test + public void testReadAccessTriggerRoleDeleteMember() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleDeleteMember(); + } + + @Override + @Test + public void testReadAccessTriggerRoleDeleteParent() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleDeleteParent(); + } + + @Override + @Test + public void testReadAccessTriggerRoleDeleteMemberAndParent() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleDeleteMemberAndParent(); + } + + @Override + @Test + public void testSearchWithUserFilterAfterReadAccessTriggerAllWithLocalUser() throws Exception + { + ReadAccessDaoTest.super.testSearchWithUserFilterAfterReadAccessTriggerAllWithLocalUser(); + } + + @Override + @Test + public void testSearchWithUserFilterAfterReadAccessTriggerLocalwithLocalUser() throws Exception + { + ReadAccessDaoTest.super.testSearchWithUserFilterAfterReadAccessTriggerLocalwithLocalUser(); + } + + @Override + @Test + public void testSearchWithUserFilterAfterReadAccessTriggerAllWithRemoteUser() throws Exception + { + ReadAccessDaoTest.super.testSearchWithUserFilterAfterReadAccessTriggerAllWithRemoteUser(); + } + + @Override + @Test + public void testSearchWithUserFilterAfterReadAccessTriggerLocalWithRemoteUser() throws Exception + { + ReadAccessDaoTest.super.testSearchWithUserFilterAfterReadAccessTriggerLocalWithRemoteUser(); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/dao/QuestionnaireResponseDaoTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/dao/QuestionnaireResponseDaoTest.java new file mode 100755 index 000000000..e962355d2 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/dao/QuestionnaireResponseDaoTest.java @@ -0,0 +1,241 @@ +package org.highmed.dsf.fhir.dao; + +import static org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; +import static org.junit.Assert.assertEquals; + +import org.highmed.dsf.fhir.dao.jdbc.QuestionnaireResponseDaoJdbc; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.junit.Test; + +public class QuestionnaireResponseDaoTest + extends AbstractResourceDaoTest + implements ReadAccessDaoTest +{ + public QuestionnaireResponseDaoTest() + { + super(QuestionnaireResponse.class, QuestionnaireResponseDaoJdbc::new); + } + + @Override + public QuestionnaireResponse createResource() + { + QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse(); + questionnaireResponse.setStatus(QuestionnaireResponseStatus.INPROGRESS); + return questionnaireResponse; + } + + @Override + protected void checkCreated(QuestionnaireResponse resource) + { + assertEquals(QuestionnaireResponseStatus.INPROGRESS, resource.getStatus()); + } + + @Override + protected QuestionnaireResponse updateResource(QuestionnaireResponse resource) + { + resource.setStatus(QuestionnaireResponseStatus.COMPLETED); + return resource; + } + + @Override + protected void checkUpdates(QuestionnaireResponse resource) + { + assertEquals(QuestionnaireResponseStatus.COMPLETED, resource.getStatus()); + } + + @Override + @Test + public void testReadAccessTriggerAll() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerAll(); + } + + @Override + @Test + public void testReadAccessTriggerLocal() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerLocal(); + } + + @Override + @Test + public void testReadAccessTriggerOrganization() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganization(); + } + + @Override + @Test + public void testReadAccessTriggerOrganizationResourceFirst() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganizationResourceFirst(); + } + + @Override + @Test + public void testReadAccessTriggerOrganization2Organizations1Matching() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganization2Organizations1Matching(); + } + + @Override + @Test + public void testReadAccessTriggerOrganization2Organizations2Matching() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganization2Organizations2Matching(); + } + + @Override + @Test + public void testReadAccessTriggerRole() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRole(); + } + + @Override + @Test + public void testReadAccessTriggerRoleResourceFirst() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleResourceFirst(); + } + + @Override + @Test + public void testReadAccessTriggerRole2Organizations1Matching() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRole2Organizations1Matching(); + } + + @Override + @Test + public void testReadAccessTriggerRole2Organizations2Matching() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRole2Organizations2Matching(); + } + + @Override + @Test + public void testReadAccessTriggerAllUpdate() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerAllUpdate(); + } + + @Override + @Test + public void testReadAccessTriggerLocalUpdate() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerLocalUpdate(); + } + + @Override + @Test + public void testReadAccessTriggerOrganizationUpdate() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganizationUpdate(); + } + + @Override + @Test + public void testReadAccessTriggerRoleUpdate() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleUpdate(); + } + + @Override + @Test + public void testReadAccessTriggerRoleUpdateMemberOrganizationNonActive() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleUpdateMemberOrganizationNonActive(); + } + + @Override + @Test + public void testReadAccessTriggerRoleUpdateParentOrganizationNonActive() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleUpdateParentOrganizationNonActive(); + } + + @Override + @Test + public void testReadAccessTriggerRoleUpdateMemberAndParentOrganizationNonActive() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleUpdateMemberAndParentOrganizationNonActive(); + } + + @Override + @Test + public void testReadAccessTriggerAllDelete() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerAllDelete(); + } + + @Override + @Test + public void testReadAccessTriggerLocalDelete() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerLocalDelete(); + } + + @Override + @Test + public void testReadAccessTriggerOrganizationDelete() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerOrganizationDelete(); + } + + @Override + @Test + public void testReadAccessTriggerRoleDelete() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleDelete(); + } + + @Override + @Test + public void testReadAccessTriggerRoleDeleteMember() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleDeleteMember(); + } + + @Override + @Test + public void testReadAccessTriggerRoleDeleteParent() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleDeleteParent(); + } + + @Override + @Test + public void testReadAccessTriggerRoleDeleteMemberAndParent() throws Exception + { + ReadAccessDaoTest.super.testReadAccessTriggerRoleDeleteMemberAndParent(); + } + + @Override + @Test + public void testSearchWithUserFilterAfterReadAccessTriggerAllWithLocalUser() throws Exception + { + ReadAccessDaoTest.super.testSearchWithUserFilterAfterReadAccessTriggerAllWithLocalUser(); + } + + @Override + @Test + public void testSearchWithUserFilterAfterReadAccessTriggerLocalwithLocalUser() throws Exception + { + ReadAccessDaoTest.super.testSearchWithUserFilterAfterReadAccessTriggerLocalwithLocalUser(); + } + + @Override + @Test + public void testSearchWithUserFilterAfterReadAccessTriggerAllWithRemoteUser() throws Exception + { + ReadAccessDaoTest.super.testSearchWithUserFilterAfterReadAccessTriggerAllWithRemoteUser(); + } + + @Override + @Test + public void testSearchWithUserFilterAfterReadAccessTriggerLocalWithRemoteUser() throws Exception + { + ReadAccessDaoTest.super.testSearchWithUserFilterAfterReadAccessTriggerLocalWithRemoteUser(); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/integration/QuestionnaireIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/integration/QuestionnaireIntegrationTest.java new file mode 100644 index 000000000..96b96f9e2 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/integration/QuestionnaireIntegrationTest.java @@ -0,0 +1,146 @@ +package org.highmed.dsf.fhir.integration; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Collections; +import java.util.Date; +import java.util.Map; + +import org.highmed.dsf.fhir.dao.QuestionnaireDao; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Questionnaire; +import org.junit.Test; + +public class QuestionnaireIntegrationTest extends AbstractIntegrationTest +{ + private static final Date DATE = Date.from(LocalDateTime.parse("2022-01-01T00:00:00").toInstant(ZoneOffset.UTC)); + private static final String IDENTIFIER_SYSTEM = "http://highmed.org/fhir/CodeSystem/user-task"; + private static final String IDENTIFIER_VALUE = "foo"; + private static final String URL = "http://highmed.org/fhir/Questionnaire/userTask/foo"; + private static final String VERSION = "1.0.0"; + private static final Enumerations.PublicationStatus STATUS = Enumerations.PublicationStatus.ACTIVE; + + private Questionnaire createQuestionnaire() + { + Questionnaire questionnaire = new Questionnaire(); + questionnaire.getMeta().addTag().setSystem("http://highmed.org/fhir/CodeSystem/read-access-tag").setCode("ALL"); + + questionnaire.addIdentifier().setSystem(IDENTIFIER_SYSTEM).setValue(IDENTIFIER_VALUE); + + questionnaire.setUrl(URL); + questionnaire.setVersion(VERSION); + + questionnaire.setStatus(STATUS); + questionnaire.setDate(DATE); + + questionnaire.addItem().setLinkId("foo").setText("Approve?") + .setType(Questionnaire.QuestionnaireItemType.BOOLEAN).addInitial().setValue(new BooleanType(false)); + + return questionnaire; + } + + @Test + public void testCreateValidByLocalUser() throws Exception + { + Questionnaire questionnaire = createQuestionnaire(); + + Questionnaire created = getWebserviceClient().create(questionnaire); + + assertNotNull(created); + assertNotNull(created.getIdElement().getIdPart()); + assertNotNull(created.getIdElement().getVersionIdPart()); + } + + @Test + public void testSearchByDate() throws Exception + { + Questionnaire questionnaire = createQuestionnaire(); + QuestionnaireDao questionnaireDao = getSpringWebApplicationContext().getBean(QuestionnaireDao.class); + questionnaireDao.create(questionnaire); + + Bundle searchBundle = getWebserviceClient().search(Questionnaire.class, + Map.of("date", Collections.singletonList("le2022-02-01"))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(1, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof Questionnaire); + + Questionnaire searchQuestionnaire = (Questionnaire) searchBundle.getEntry().get(0).getResource(); + assertTrue(searchQuestionnaire.hasDate()); + assertEquals(0, DATE.compareTo(searchQuestionnaire.getDate())); + } + + @Test + public void testSearchByIdentifier() throws Exception + { + Questionnaire questionnaire = createQuestionnaire(); + QuestionnaireDao questionnaireDao = getSpringWebApplicationContext().getBean(QuestionnaireDao.class); + questionnaireDao.create(questionnaire); + + Bundle searchBundle = getWebserviceClient().search(Questionnaire.class, + Map.of("identifier", Collections.singletonList(IDENTIFIER_SYSTEM + "|" + IDENTIFIER_VALUE))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(1, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof Questionnaire); + + Questionnaire searchQuestionnaire = (Questionnaire) searchBundle.getEntry().get(0).getResource(); + assertEquals(1, searchQuestionnaire.getIdentifier().size()); + assertEquals(IDENTIFIER_SYSTEM, searchQuestionnaire.getIdentifier().get(0).getSystem()); + assertEquals(IDENTIFIER_VALUE, searchQuestionnaire.getIdentifier().get(0).getValue()); + } + + @Test + public void testSearchByStatus() throws Exception + { + Questionnaire questionnaire = createQuestionnaire(); + QuestionnaireDao questionnaireDao = getSpringWebApplicationContext().getBean(QuestionnaireDao.class); + questionnaireDao.create(questionnaire); + + Bundle searchBundle = getWebserviceClient().search(Questionnaire.class, + Map.of("status", Collections.singletonList(STATUS.toCode()))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(1, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof Questionnaire); + + Questionnaire searchQuestionnaire = (Questionnaire) searchBundle.getEntry().get(0).getResource(); + assertTrue(searchQuestionnaire.hasStatus()); + assertEquals(STATUS, searchQuestionnaire.getStatus()); + } + + @Test + public void testSearchByUrlAndVersion() throws Exception + { + Questionnaire questionnaire = createQuestionnaire(); + QuestionnaireDao questionnaireDao = getSpringWebApplicationContext().getBean(QuestionnaireDao.class); + questionnaireDao.create(questionnaire); + + Bundle searchBundle = getWebserviceClient().search(Questionnaire.class, + Map.of("url", Collections.singletonList(URL), "version", Collections.singletonList(VERSION))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(1, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof Questionnaire); + + Questionnaire searchQuestionnaire = (Questionnaire) searchBundle.getEntry().get(0).getResource(); + assertTrue(searchQuestionnaire.hasUrl()); + assertEquals(URL, searchQuestionnaire.getUrl()); + assertTrue(searchQuestionnaire.hasVersion()); + assertEquals(VERSION, searchQuestionnaire.getVersion()); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/integration/QuestionnaireResponseIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/integration/QuestionnaireResponseIntegrationTest.java new file mode 100644 index 000000000..2a68d5574 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/integration/QuestionnaireResponseIntegrationTest.java @@ -0,0 +1,414 @@ +package org.highmed.dsf.fhir.integration; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Collections; +import java.util.Date; +import java.util.Map; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response.Status; + +import org.highmed.dsf.fhir.authentication.OrganizationProvider; +import org.highmed.dsf.fhir.dao.QuestionnaireDao; +import org.highmed.dsf.fhir.dao.QuestionnaireResponseDao; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; +import org.hl7.fhir.r4.model.Reference; +import org.junit.Test; + +public class QuestionnaireResponseIntegrationTest extends AbstractIntegrationTest +{ + private static final Date AUTHORED = Date + .from(LocalDateTime.parse("2022-01-01T00:00:00").toInstant(ZoneOffset.UTC)); + private static final String IDENTIFIER_SYSTEM = "http://highmed.org/fhir/CodeSystem/user-task-id"; + private static final String IDENTIFIER_VALUE = "foo"; + private static final String QUESTIONNAIRE_URL = "http://highmed.org/fhir/Questionnaire/userTask/foo"; + private static final String QUESTIONNAIRE_VERSION = "1.0.0"; + private static final String QUESTIONNAIRE = QUESTIONNAIRE_URL + "|" + QUESTIONNAIRE_VERSION; + private static final QuestionnaireResponse.QuestionnaireResponseStatus STATUS = QuestionnaireResponse.QuestionnaireResponseStatus.INPROGRESS; + + @Test + public void testCreateValidByLocalUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + + QuestionnaireResponse created = getWebserviceClient().create(questionnaireResponse); + assertNotNull(created); + assertNotNull(created.getIdElement().getIdPart()); + assertNotNull(created.getIdElement().getVersionIdPart()); + } + + @Test(expected = WebApplicationException.class) + public void testCreateNotAllowedByLocalUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + questionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); + + try + { + getWebserviceClient().create(questionnaireResponse); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + + @Test + public void testUpdateAllowedByLocalUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + created.setStatus(QuestionnaireResponseStatus.COMPLETED); + QuestionnaireResponse updated = getWebserviceClient().update(created); + + assertNotNull(updated); + assertNotNull(updated.getIdElement().getIdPart()); + assertEquals(created.getIdElement().getIdPart(), updated.getIdElement().getIdPart()); + assertNotNull(updated.getIdElement().getVersionIdPart()); + assertEquals("2", updated.getIdElement().getVersionIdPart()); + } + + @Test(expected = WebApplicationException.class) + public void testUpdateNotAllowedByLocalUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + try + { + getWebserviceClient().update(created); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + + @Test(expected = WebApplicationException.class) + public void testSecondUpdateNotAllowedByLocalUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + created.setStatus(QuestionnaireResponseStatus.COMPLETED); + QuestionnaireResponse updated = questionnaireResponseDao.update(created); + + try + { + getWebserviceClient().update(updated); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + + @Test + public void testSearchByDate() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + questionnaireResponseDao.create(questionnaireResponse); + + Bundle searchBundle = getWebserviceClient().search(QuestionnaireResponse.class, + Map.of("authored", Collections.singletonList("le2022-02-01"))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(1, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof QuestionnaireResponse); + + QuestionnaireResponse searchQuestionnaireResponse = (QuestionnaireResponse) searchBundle.getEntry().get(0) + .getResource(); + assertTrue(searchQuestionnaireResponse.hasAuthored()); + assertEquals(0, AUTHORED.compareTo(searchQuestionnaireResponse.getAuthored())); + } + + @Test + public void testSearchByIdentifier() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + questionnaireResponseDao.create(questionnaireResponse); + + Bundle searchBundle = getWebserviceClient().search(QuestionnaireResponse.class, + Map.of("identifier", Collections.singletonList(IDENTIFIER_SYSTEM + "|" + IDENTIFIER_VALUE))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(1, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof QuestionnaireResponse); + + QuestionnaireResponse searchQuestionnaireResponse = (QuestionnaireResponse) searchBundle.getEntry().get(0) + .getResource(); + assertTrue(searchQuestionnaireResponse.hasIdentifier()); + assertEquals(IDENTIFIER_SYSTEM, searchQuestionnaireResponse.getIdentifier().getSystem()); + assertEquals(IDENTIFIER_VALUE, searchQuestionnaireResponse.getIdentifier().getValue()); + } + + @Test + public void testSearchByQuestionnaireWithVersion() throws Exception + { + testSearchByQuestionnaire(QUESTIONNAIRE); + } + + @Test + public void testSearchByQuestionnaireWithoutVersion() throws Exception + { + testSearchByQuestionnaire(QUESTIONNAIRE_URL); + } + + private void testSearchByQuestionnaire(String questionnaireUrl) throws Exception + { + Questionnaire questionnaire = createQuestionnaire(); + QuestionnaireDao questionnaireDao = getSpringWebApplicationContext().getBean(QuestionnaireDao.class); + questionnaireDao.create(questionnaire); + + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + questionnaireResponseDao.create(questionnaireResponse); + + Bundle searchBundle = getWebserviceClient().search(QuestionnaireResponse.class, + Map.of("questionnaire", Collections.singletonList(questionnaireUrl), "_include", + Collections.singletonList("QuestionnaireResponse:questionnaire"))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(2, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof QuestionnaireResponse); + + QuestionnaireResponse searchQuestionnaireResponse = (QuestionnaireResponse) searchBundle.getEntry().get(0) + .getResource(); + assertTrue(searchQuestionnaireResponse.hasQuestionnaire()); + assertEquals(QUESTIONNAIRE, searchQuestionnaireResponse.getQuestionnaire()); + + assertNotNull(searchBundle.getEntry().get(1)); + assertNotNull(searchBundle.getEntry().get(1).getResource()); + assertTrue(searchBundle.getEntry().get(1).getResource() instanceof Questionnaire); + + Questionnaire searchQuestionnaire = (Questionnaire) searchBundle.getEntry().get(1).getResource(); + assertTrue(searchQuestionnaire.hasUrl()); + assertEquals(QUESTIONNAIRE_URL, searchQuestionnaire.getUrl()); + assertTrue(searchQuestionnaire.hasVersion()); + assertEquals(QUESTIONNAIRE_VERSION, searchQuestionnaire.getVersion()); + } + + @Test + public void testSearchByQuestionnaireNoVersion() throws Exception + { + Questionnaire questionnaire = createQuestionnaire().setVersion(null); + QuestionnaireDao questionnaireDao = getSpringWebApplicationContext().getBean(QuestionnaireDao.class); + questionnaireDao.create(questionnaire); + + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse().setQuestionnaire(QUESTIONNAIRE_URL); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + questionnaireResponseDao.create(questionnaireResponse); + + Bundle searchBundle = getWebserviceClient().search(QuestionnaireResponse.class, + Map.of("questionnaire", Collections.singletonList(QUESTIONNAIRE_URL), "_include", + Collections.singletonList("QuestionnaireResponse:questionnaire"))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(2, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof QuestionnaireResponse); + + QuestionnaireResponse searchQuestionnaireResponse = (QuestionnaireResponse) searchBundle.getEntry().get(0) + .getResource(); + assertTrue(searchQuestionnaireResponse.hasQuestionnaire()); + assertEquals(QUESTIONNAIRE_URL, searchQuestionnaireResponse.getQuestionnaire()); + + assertNotNull(searchBundle.getEntry().get(1)); + assertNotNull(searchBundle.getEntry().get(1).getResource()); + assertTrue(searchBundle.getEntry().get(1).getResource() instanceof Questionnaire); + + Questionnaire searchQuestionnaire = (Questionnaire) searchBundle.getEntry().get(1).getResource(); + assertTrue(searchQuestionnaire.hasUrl()); + assertEquals(QUESTIONNAIRE_URL, searchQuestionnaire.getUrl()); + assertFalse(searchQuestionnaire.hasVersion()); + } + + @Test + public void testSearchByQuestionnaireWithoutVersionButMultipleVersionExist() throws Exception + { + QuestionnaireDao questionnaireDao = getSpringWebApplicationContext().getBean(QuestionnaireDao.class); + + Questionnaire questionnaire1 = createQuestionnaire().setVersion("0.1.0"); + questionnaireDao.create(questionnaire1); + + Questionnaire questionnaire2 = createQuestionnaire(); + questionnaireDao.create(questionnaire2); + + Questionnaire questionnaire3 = createQuestionnaire().setVersion("0.2.0"); + questionnaireDao.create(questionnaire3); + + Questionnaire questionnaire4 = createQuestionnaire().setVersion(null); + questionnaireDao.create(questionnaire4); + + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + questionnaireResponseDao.create(questionnaireResponse); + + Bundle searchBundle = getWebserviceClient().search(QuestionnaireResponse.class, + Map.of("questionnaire", Collections.singletonList(QUESTIONNAIRE_URL), "_include", + Collections.singletonList("QuestionnaireResponse:questionnaire"))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(2, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof QuestionnaireResponse); + + QuestionnaireResponse searchQuestionnaireResponse = (QuestionnaireResponse) searchBundle.getEntry().get(0) + .getResource(); + assertTrue(searchQuestionnaireResponse.hasQuestionnaire()); + assertEquals(QUESTIONNAIRE, searchQuestionnaireResponse.getQuestionnaire()); + + assertNotNull(searchBundle.getEntry().get(1)); + assertNotNull(searchBundle.getEntry().get(1).getResource()); + assertTrue(searchBundle.getEntry().get(1).getResource() instanceof Questionnaire); + + Questionnaire searchQuestionnaire = (Questionnaire) searchBundle.getEntry().get(1).getResource(); + assertTrue(searchQuestionnaire.hasUrl()); + assertEquals(QUESTIONNAIRE_URL, searchQuestionnaire.getUrl()); + + // Expect newest version 1.0.0 (null, 0.1.0 and 0.2.0 exist as well) + assertTrue(searchQuestionnaire.hasVersion()); + assertEquals(QUESTIONNAIRE_VERSION, searchQuestionnaire.getVersion()); + } + + @Test + public void testSearchByStatus() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + questionnaireResponseDao.create(questionnaireResponse); + + Bundle searchBundle = getWebserviceClient().search(QuestionnaireResponse.class, + Map.of("status", Collections.singletonList(STATUS.toCode()))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(1, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof QuestionnaireResponse); + + QuestionnaireResponse searchQuestionnaireResponse = (QuestionnaireResponse) searchBundle.getEntry().get(0) + .getResource(); + assertTrue(searchQuestionnaireResponse.hasStatus()); + assertEquals(STATUS, searchQuestionnaireResponse.getStatus()); + } + + @Test + public void testSearchBySubjectReference() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + questionnaireResponseDao.create(questionnaireResponse); + + OrganizationProvider organizationProvider = getSpringWebApplicationContext() + .getBean(OrganizationProvider.class); + Organization organization = organizationProvider.getLocalOrganization().get(); + + String organizationReference = "Organization/" + organization.getIdElement().getIdPart(); + Bundle searchBundle = getWebserviceClient().search(QuestionnaireResponse.class, + Map.of("subject", Collections.singletonList(organizationReference), "_include", + Collections.singletonList("QuestionnaireResponse:subject:Organization"))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(2, searchBundle.getEntry().size()); + assertNotNull(searchBundle.getEntry().get(0)); + assertNotNull(searchBundle.getEntry().get(0).getResource()); + assertTrue(searchBundle.getEntry().get(0).getResource() instanceof QuestionnaireResponse); + + QuestionnaireResponse searchQuestionnaireResponse = (QuestionnaireResponse) searchBundle.getEntry().get(0) + .getResource(); + assertTrue(searchQuestionnaireResponse.hasStatus()); + assertEquals(organizationReference, searchQuestionnaireResponse.getSubject().getReference()); + + assertNotNull(searchBundle.getEntry().get(1)); + assertNotNull(searchBundle.getEntry().get(1).getResource()); + assertTrue(searchBundle.getEntry().get(1).getResource() instanceof Organization); + + Organization searchOrganization = (Organization) searchBundle.getEntry().get(1).getResource(); + assertEquals(organization.getIdentifierFirstRep().getSystem(), + searchOrganization.getIdentifierFirstRep().getSystem()); + assertEquals(organization.getIdentifierFirstRep().getValue(), + searchOrganization.getIdentifierFirstRep().getValue()); + } + + private Questionnaire createQuestionnaire() + { + Questionnaire questionnaire = new Questionnaire(); + questionnaire.getMeta().addTag().setSystem("http://highmed.org/fhir/CodeSystem/read-access-tag").setCode("ALL"); + + questionnaire.setUrl(QUESTIONNAIRE_URL); + questionnaire.setVersion(QUESTIONNAIRE_VERSION); + + questionnaire.setStatus(Enumerations.PublicationStatus.ACTIVE); + + questionnaire.addItem().setLinkId("foo").setText("Approve?") + .setType(Questionnaire.QuestionnaireItemType.BOOLEAN).addInitial().setValue(new BooleanType(false)); + + return questionnaire; + } + + private QuestionnaireResponse createQuestionnaireResponse() + { + OrganizationProvider organizationProvider = getSpringWebApplicationContext() + .getBean(OrganizationProvider.class); + assertNotNull(organizationProvider); + + QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse(); + questionnaireResponse.getMeta().addTag().setSystem("http://highmed.org/fhir/CodeSystem/read-access-tag") + .setCode("ALL"); + + questionnaireResponse.getIdentifier().setSystem(IDENTIFIER_SYSTEM).setValue(IDENTIFIER_VALUE); + + questionnaireResponse.setQuestionnaire(QUESTIONNAIRE); + + questionnaireResponse.setStatus(STATUS); + questionnaireResponse.setAuthored(AUTHORED); + + String organizationReference = "Organization/" + + organizationProvider.getLocalOrganization().get().getIdElement().getIdPart(); + questionnaireResponse.setSubject(new Reference(organizationReference)); + + questionnaireResponse.addItem().setLinkId("foo").setText("Approve?").addAnswer() + .setValue(new BooleanType(true)); + + return questionnaireResponse; + } +} diff --git a/dsf-fhir/dsf-fhir-validation/pom.xml b/dsf-fhir/dsf-fhir-validation/pom.xml index f9ef2bdca..829dcb62f 100644 --- a/dsf-fhir/dsf-fhir-validation/pom.xml +++ b/dsf-fhir/dsf-fhir-validation/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-fhir-pom - 0.7.0 + 0.8.0 @@ -93,7 +93,7 @@ - diff --git a/dsf-fhir/dsf-fhir-validation/src/main/java/org/highmed/dsf/fhir/validation/ResourceValidatorImpl.java b/dsf-fhir/dsf-fhir-validation/src/main/java/org/highmed/dsf/fhir/validation/ResourceValidatorImpl.java index 26210291c..bbe6ca790 100755 --- a/dsf-fhir/dsf-fhir-validation/src/main/java/org/highmed/dsf/fhir/validation/ResourceValidatorImpl.java +++ b/dsf-fhir/dsf-fhir-validation/src/main/java/org/highmed/dsf/fhir/validation/ResourceValidatorImpl.java @@ -1,15 +1,21 @@ package org.highmed.dsf.fhir.validation; +import java.util.regex.Pattern; + import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.r4.model.Resource; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.ValidationResult; public class ResourceValidatorImpl implements ResourceValidator { + private static final Pattern AT_DEFAULT_SLICE_PATTERN = Pattern + .compile(".*(Questionnaire|QuestionnaireResponse).item:@default.*"); + private final FhirValidator validator; public ResourceValidatorImpl(FhirContext context, IValidationSupport validationSupport) @@ -27,6 +33,17 @@ protected FhirValidator configureValidator(FhirValidator validator, IValidationS @Override public ValidationResult validate(Resource resource) { - return validator.validateWithResult(resource); + ValidationResult result = validator.validateWithResult(resource); + + // TODO: remove after HAPI validator is fixed: https://github.com/hapifhir/org.hl7.fhir.core/issues/193 + adaptDefaultSliceValidationErrorToWarning(result); + + return result; + } + + private void adaptDefaultSliceValidationErrorToWarning(ValidationResult result) + { + result.getMessages().stream().filter(m -> AT_DEFAULT_SLICE_PATTERN.matcher(m.getMessage()).matches()) + .forEach(m -> m.setSeverity(ResultSeverityEnum.WARNING)); } } diff --git a/dsf-fhir/dsf-fhir-validation/src/main/java/org/highmed/dsf/fhir/validation/ValidationSupportWithCustomResources.java b/dsf-fhir/dsf-fhir-validation/src/main/java/org/highmed/dsf/fhir/validation/ValidationSupportWithCustomResources.java index 29a831173..a02e1dd22 100644 --- a/dsf-fhir/dsf-fhir-validation/src/main/java/org/highmed/dsf/fhir/validation/ValidationSupportWithCustomResources.java +++ b/dsf-fhir/dsf-fhir-validation/src/main/java/org/highmed/dsf/fhir/validation/ValidationSupportWithCustomResources.java @@ -37,11 +37,11 @@ public ValidationSupportWithCustomResources(FhirContext context, this.context = context; if (structureDefinitions != null) - structureDefinitions.forEach(s -> structureDefinitionsByUrl.put(s.getUrl(), s)); + structureDefinitions.forEach(this::addOrReplace); if (codeSystems != null) - codeSystems.forEach(s -> codeSystemsByUrl.put(s.getUrl(), s)); + codeSystems.forEach(this::addOrReplace); if (valueSets != null) - valueSets.forEach(s -> valueSetsByUrl.put(s.getUrl(), s)); + valueSets.forEach(this::addOrReplace); } @Override @@ -75,6 +75,7 @@ public StructureDefinition fetchStructureDefinition(String url) public void addOrReplace(StructureDefinition s) { structureDefinitionsByUrl.put(s.getUrl(), s); + structureDefinitionsByUrl.put(s.getUrl() + "|" + s.getVersion(), s); } @Override @@ -92,6 +93,7 @@ public boolean isCodeSystemSupported(ValidationSupportContext theRootValidationS public void addOrReplace(CodeSystem s) { codeSystemsByUrl.put(s.getUrl(), s); + codeSystemsByUrl.put(s.getUrl() + "|" + s.getVersion(), s); } @Override @@ -109,5 +111,6 @@ public boolean isValueSetSupported(ValidationSupportContext theRootValidationSup public void addOrReplace(ValueSet s) { valueSetsByUrl.put(s.getUrl(), s); + valueSetsByUrl.put(s.getUrl() + "|" + s.getVersion(), s); } } diff --git a/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-0.8.0.xml b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-0.8.0.xml new file mode 100644 index 000000000..371401aef --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-0.8.0.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-0.8.0.xml.post b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-0.8.0.xml.post new file mode 100644 index 000000000..ae0a8e800 --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-0.8.0.xml.post @@ -0,0 +1 @@ +url=http://highmed.org/fhir/StructureDefinition/questionnaire&version=0.8.0 \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-response-0.8.0.xml b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-response-0.8.0.xml new file mode 100644 index 000000000..ce16df829 --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-response-0.8.0.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-response-0.8.0.xml.post b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-response-0.8.0.xml.post new file mode 100644 index 000000000..8d2cefaaa --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-response-0.8.0.xml.post @@ -0,0 +1 @@ +url=http://highmed.org/fhir/StructureDefinition/questionnaire-response&version=0.8.0 \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/Subscription/highmed-bpmn-questionnaire-response-subscription.xml b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/Subscription/highmed-bpmn-questionnaire-response-subscription.xml new file mode 100755 index 000000000..b518163fb --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/Subscription/highmed-bpmn-questionnaire-response-subscription.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/Subscription/highmed-bpmn-questionnaire-response-subscription.xml.post b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/Subscription/highmed-bpmn-questionnaire-response-subscription.xml.post new file mode 100755 index 000000000..e305c959b --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/Subscription/highmed-bpmn-questionnaire-response-subscription.xml.post @@ -0,0 +1 @@ +criteria=QuestionnaireResponse%3Fstatus%3Dcompleted&status=active&type=websocket&payload=application/fhir%2Bjson \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-validation/src/test/java/org/highmed/dsf/fhir/profiles/QuestionnaireProfileTest.java b/dsf-fhir/dsf-fhir-validation/src/test/java/org/highmed/dsf/fhir/profiles/QuestionnaireProfileTest.java new file mode 100644 index 000000000..92c7bf2b6 --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/test/java/org/highmed/dsf/fhir/profiles/QuestionnaireProfileTest.java @@ -0,0 +1,180 @@ +package org.highmed.dsf.fhir.profiles; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; + +import org.highmed.dsf.fhir.validation.ResourceValidator; +import org.highmed.dsf.fhir.validation.ResourceValidatorImpl; +import org.highmed.dsf.fhir.validation.ValidationSupportRule; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Questionnaire; +import org.junit.ClassRule; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ca.uhn.fhir.validation.ResultSeverityEnum; +import ca.uhn.fhir.validation.ValidationResult; + +public class QuestionnaireProfileTest +{ + private static final Logger logger = LoggerFactory.getLogger(ResearchStudyProfileTest.class); + + @ClassRule + public static final ValidationSupportRule validationRule = new ValidationSupportRule( + Arrays.asList("highmed-questionnaire-0.8.0.xml"), Collections.emptyList(), Collections.emptyList()); + + private ResourceValidator resourceValidator = new ResourceValidatorImpl(validationRule.getFhirContext(), + validationRule.getValidationSupport()); + + @Test + public void testQuestionnaireValidTypeString() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.STRING); + } + + @Test + public void testQuestionnaireValidTypeText() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.TEXT); + } + + @Test + public void testQuestionnaireValidTypeInteger() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.INTEGER); + } + + @Test + public void testQuestionnaireValidTypeDecimal() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.DECIMAL); + } + + @Test + public void testQuestionnaireValidTypeBoolean() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.BOOLEAN); + } + + @Test + public void testQuestionnaireValidTypeDate() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.DATE); + } + + @Test + public void testQuestionnaireValidTypeTime() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.TIME); + } + + @Test + public void testQuestionnaireValidTypeDateTime() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.DATETIME); + } + + @Test + public void testQuestionnaireValidTypeUrl() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.URL); + } + + @Test + public void testQuestionnaireValidTypeReference() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.REFERENCE); + } + + private void testQuestionnaireValidType(Questionnaire.QuestionnaireItemType type) + { + Questionnaire res = createQuestionnaire(type); + + ValidationResult result = resourceValidator.validate(res); + result.getMessages().stream().map(m -> m.getLocationString() + " " + m.getLocationLine() + ":" + + m.getLocationCol() + " - " + m.getSeverity() + ": " + m.getMessage()).forEach(logger::info); + + assertEquals(0, result.getMessages().stream().filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) + || ResultSeverityEnum.FATAL.equals(m.getSeverity())).count()); + } + + @Test + public void testQuestionnaireInvalidTypeGroup() + { + testQuestionnaireInvalidType(Questionnaire.QuestionnaireItemType.GROUP); + } + + @Test + public void testQuestionnaireInvalidTypeDisplay() + { + testQuestionnaireInvalidType(Questionnaire.QuestionnaireItemType.DISPLAY); + } + + @Test + public void testQuestionnaireInvalidTypeQuestion() + { + testQuestionnaireInvalidType(Questionnaire.QuestionnaireItemType.QUESTION); + } + + @Test + public void testQuestionnaireInvalidTypeChoice() + { + testQuestionnaireInvalidType(Questionnaire.QuestionnaireItemType.CHOICE); + } + + @Test + public void testQuestionnaireInvalidTypeOpenChoice() + { + testQuestionnaireInvalidType(Questionnaire.QuestionnaireItemType.OPENCHOICE); + } + + @Test + public void testQuestionnaireInvalidTypeAttachment() + { + testQuestionnaireInvalidType(Questionnaire.QuestionnaireItemType.ATTACHMENT); + } + + @Test + public void testQuestionnaireInvalidTypeQuantity() + { + testQuestionnaireInvalidType(Questionnaire.QuestionnaireItemType.QUANTITY); + } + + private void testQuestionnaireInvalidType(Questionnaire.QuestionnaireItemType type) + { + Questionnaire res = createQuestionnaire(Questionnaire.QuestionnaireItemType.STRING); + res.addItem().setLinkId("invalid-type").setType(type).setText("Invalid type"); + + ValidationResult result = resourceValidator.validate(res); + result.getMessages().stream().map(m -> m.getLocationString() + " " + m.getLocationLine() + ":" + + m.getLocationCol() + " - " + m.getSeverity() + ": " + m.getMessage()).forEach(logger::info); + + assertEquals(1, + result.getMessages().stream() + .filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) + || ResultSeverityEnum.FATAL.equals(m.getSeverity())) + .filter(m -> m.getMessage() != null).filter(m -> m.getMessage().startsWith("type-code")) + .count()); + } + + private Questionnaire createQuestionnaire(Questionnaire.QuestionnaireItemType type) + { + Questionnaire res = new Questionnaire(); + res.getMeta().addProfile("http://highmed.org/fhir/StructureDefinition/questionnaire"); + res.setUrl("http://highmed.org/fhir/Questionnaire/hello-world"); + res.setVersion("0.1.0"); + res.setDate(new Date()); + res.setStatus(Enumerations.PublicationStatus.ACTIVE); + res.addItem().setLinkId("business-key").setType(Questionnaire.QuestionnaireItemType.STRING) + .setText("The business-key of the process execution"); + res.addItem().setLinkId("user-task-id").setType(Questionnaire.QuestionnaireItemType.STRING) + .setText("The user-task-id of the process execution"); + res.addItem().setLinkId("valid-type").setType(type).setText("valid type"); + + return res; + } +} diff --git a/dsf-fhir/dsf-fhir-validation/src/test/java/org/highmed/dsf/fhir/profiles/QuestionnaireResponseProfileTest.java b/dsf-fhir/dsf-fhir-validation/src/test/java/org/highmed/dsf/fhir/profiles/QuestionnaireResponseProfileTest.java new file mode 100644 index 000000000..a66b0bbe4 --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/test/java/org/highmed/dsf/fhir/profiles/QuestionnaireResponseProfileTest.java @@ -0,0 +1,201 @@ +package org.highmed.dsf.fhir.profiles; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.UUID; + +import org.highmed.dsf.fhir.validation.ResourceValidator; +import org.highmed.dsf.fhir.validation.ResourceValidatorImpl; +import org.highmed.dsf.fhir.validation.ValidationSupportRule; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.TimeType; +import org.hl7.fhir.r4.model.Type; +import org.hl7.fhir.r4.model.UriType; +import org.junit.ClassRule; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ca.uhn.fhir.validation.ResultSeverityEnum; +import ca.uhn.fhir.validation.ValidationResult; + +public class QuestionnaireResponseProfileTest +{ + private static final Logger logger = LoggerFactory.getLogger(ResearchStudyProfileTest.class); + + @ClassRule + public static final ValidationSupportRule validationRule = new ValidationSupportRule( + Arrays.asList("highmed-questionnaire-response-0.8.0.xml"), Collections.emptyList(), + Collections.emptyList()); + + private ResourceValidator resourceValidator = new ResourceValidatorImpl(validationRule.getFhirContext(), + validationRule.getValidationSupport()); + + @Test + public void testQuestionnaireResponseValidTypeString() + { + testQuestionnaireResponseValidType(new StringType("foo")); + } + + @Test + public void testQuestionnaireResponseValidTypeInteger() + { + testQuestionnaireResponseValidType(new IntegerType(-1)); + } + + @Test + public void testQuestionnaireResponseValidTypeDecimal() + { + testQuestionnaireResponseValidType(new DecimalType(-1)); + } + + @Test + public void testQuestionnaireResponseValidTypeBoolean() + { + testQuestionnaireResponseValidType(new BooleanType(false)); + } + + @Test + public void testQuestionnaireResponseValidTypeDate() + { + testQuestionnaireResponseValidType(new DateType("1900-01-01")); + } + + @Test + public void testQuestionnaireResponseValidTypeTime() + { + testQuestionnaireResponseValidType(new TimeType("00:00:00")); + } + + @Test + public void testQuestionnaireResponseValidTypeDateTime() + { + testQuestionnaireResponseValidType(new DateTimeType("1900-01-01T00:00:00.000Z")); + } + + @Test + public void testQuestionnaireResponseValidTypeUri() + { + testQuestionnaireResponseValidType(new UriType("http://example.de/foo")); + } + + @Test + public void testQuestionnaireResponseValidTypeReference() + { + testQuestionnaireResponseValidType(new Reference("Observation/foo")); + } + + private void testQuestionnaireResponseValidType(Type type) + { + QuestionnaireResponse res = createQuestionnaireResponse(type); + + ValidationResult result = resourceValidator.validate(res); + result.getMessages().stream().map(m -> m.getLocationString() + " " + m.getLocationLine() + ":" + + m.getLocationCol() + " - " + m.getSeverity() + ": " + m.getMessage()).forEach(logger::info); + + assertEquals(0, result.getMessages().stream().filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) + || ResultSeverityEnum.FATAL.equals(m.getSeverity())).count()); + } + + @Test + public void testQuestionnaireResponseInvalidType() + { + // TODO: activate after HAPI validator is fixed: https://github.com/hapifhir/org.hl7.fhir.core/issues/193 + + // QuestionnaireResponse res = createValidQuestionnaireResponse(new + // Coding().setSystem("http://system.foo").setCode("code")); + // + // ValidationResult result = resourceValidator.validate(res); + // result.getMessages().stream() + // .map(m -> m.getLocationString() + " " + m.getLocationLine() + ":" + m.getLocationCol() + " - " + // + m.getSeverity() + ": " + m.getMessage()).forEach(logger::info); + // + // assertEquals(1, result.getMessages().stream() + // .filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) || ResultSeverityEnum.FATAL.equals( + // m.getSeverity())).count()); + } + + @Test + public void testQuestionnaireResponseValidCompleted() + { + QuestionnaireResponse res = createQuestionnaireResponse(new StringType("foo")); + res.setStatus(QuestionnaireResponse.QuestionnaireResponseStatus.COMPLETED); + res.setAuthored(new Date()); + res.setAuthor(new Reference().setIdentifier( + new Identifier().setSystem("http://highmed.org/sid/organization-identifier").setValue("foo.de"))); + + ValidationResult result = resourceValidator.validate(res); + result.getMessages().stream().map(m -> m.getLocationString() + " " + m.getLocationLine() + ":" + + m.getLocationCol() + " - " + m.getSeverity() + ": " + m.getMessage()).forEach(logger::info); + + assertEquals(0, result.getMessages().stream().filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) + || ResultSeverityEnum.FATAL.equals(m.getSeverity())).count()); + } + + @Test + public void testQuestionnaireResponseInvalidCompletedNoAuthorAndNoAuthored() + { + QuestionnaireResponse res = createQuestionnaireResponse(new StringType("foo")); + res.setStatus(QuestionnaireResponse.QuestionnaireResponseStatus.COMPLETED); + + ValidationResult result = resourceValidator.validate(res); + result.getMessages().stream().map(m -> m.getLocationString() + " " + m.getLocationLine() + ":" + + m.getLocationCol() + " - " + m.getSeverity() + ": " + m.getMessage()).forEach(logger::info); + + assertEquals(2, + result.getMessages().stream() + .filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) + || ResultSeverityEnum.FATAL.equals(m.getSeverity())) + .filter(m -> m.getMessage() != null) + .filter(m -> m.getMessage().startsWith("authored-if-completed") + || m.getMessage().startsWith("author-if-completed")) + .count()); + } + + @Test + public void testQuestionnaireResponseInvalidCompletedWithAuthorReferenceAndAuthored() + { + QuestionnaireResponse res = createQuestionnaireResponse(new StringType("foo")); + res.setStatus(QuestionnaireResponse.QuestionnaireResponseStatus.COMPLETED); + res.setAuthored(new Date()); + res.setAuthor(new Reference("Organization/" + UUID.randomUUID().toString())); + + ValidationResult result = resourceValidator.validate(res); + result.getMessages().stream().map(m -> m.getLocationString() + " " + m.getLocationLine() + ":" + + m.getLocationCol() + " - " + m.getSeverity() + ": " + m.getMessage()).forEach(logger::info); + + assertEquals(1, + result.getMessages().stream() + .filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) + || ResultSeverityEnum.FATAL.equals(m.getSeverity())) + .filter(m -> m.getMessage() != null) + .filter(m -> m.getMessage().startsWith("author-if-completed")).count()); + } + + private QuestionnaireResponse createQuestionnaireResponse(Type type) + { + QuestionnaireResponse res = new QuestionnaireResponse(); + res.getMeta().addProfile("http://highmed.org/fhir/StructureDefinition/questionnaire-response"); + res.setQuestionnaire("http://highmed.org/fhir/Questionnaire/hello-world|0.1.0"); + res.setStatus(QuestionnaireResponse.QuestionnaireResponseStatus.INPROGRESS); + res.addItem().setLinkId("business-key").setText("The business-key of the process execution").addAnswer() + .setValue(new StringType(UUID.randomUUID().toString())); + res.addItem().setLinkId("user-task-id").setText("The user-task-id of the process execution").addAnswer() + .setValue(new StringType("1")); + ; + res.addItem().setLinkId("valid-answer").setText("valid answer").addAnswer().setValue(type); + + return res; + } +} diff --git a/dsf-fhir/dsf-fhir-webservice-client/pom.xml b/dsf-fhir/dsf-fhir-webservice-client/pom.xml index 8a0da2e6e..e6304ed77 100755 --- a/dsf-fhir/dsf-fhir-webservice-client/pom.xml +++ b/dsf-fhir/dsf-fhir-webservice-client/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-fhir-pom - 0.7.0 + 0.8.0 diff --git a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/org/highmed/fhir/client/FhirWebserviceClientJersey.java b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/org/highmed/fhir/client/FhirWebserviceClientJersey.java index 0e676b685..d1f01650b 100755 --- a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/org/highmed/fhir/client/FhirWebserviceClientJersey.java +++ b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/org/highmed/fhir/client/FhirWebserviceClientJersey.java @@ -71,6 +71,10 @@ import org.highmed.dsf.fhir.adapter.PractitionerXmlFhirAdapter; import org.highmed.dsf.fhir.adapter.ProvenanceJsonFhirAdapter; import org.highmed.dsf.fhir.adapter.ProvenanceXmlFhirAdapter; +import org.highmed.dsf.fhir.adapter.QuestionnaireJsonFhirAdapter; +import org.highmed.dsf.fhir.adapter.QuestionnaireResponseJsonFhirAdapter; +import org.highmed.dsf.fhir.adapter.QuestionnaireResponseXmlFhirAdapter; +import org.highmed.dsf.fhir.adapter.QuestionnaireXmlFhirAdapter; import org.highmed.dsf.fhir.adapter.ResearchStudyJsonFhirAdapter; import org.highmed.dsf.fhir.adapter.ResearchStudyXmlFhirAdapter; import org.highmed.dsf.fhir.adapter.StructureDefinitionJsonFhirAdapter; @@ -155,7 +159,9 @@ public static List> components(FhirContext fhirContext) new PatientXmlFhirAdapter(fhirContext), new PractitionerJsonFhirAdapter(fhirContext), new PractitionerXmlFhirAdapter(fhirContext), new PractitionerRoleJsonFhirAdapter(fhirContext), new PractitionerRoleXmlFhirAdapter(fhirContext), new ProvenanceJsonFhirAdapter(fhirContext), - new ProvenanceXmlFhirAdapter(fhirContext), new ResearchStudyJsonFhirAdapter(fhirContext), + new ProvenanceXmlFhirAdapter(fhirContext), new QuestionnaireXmlFhirAdapter(fhirContext), + new QuestionnaireJsonFhirAdapter(fhirContext), new QuestionnaireResponseXmlFhirAdapter(fhirContext), + new QuestionnaireResponseJsonFhirAdapter(fhirContext), new ResearchStudyJsonFhirAdapter(fhirContext), new ResearchStudyXmlFhirAdapter(fhirContext), new StructureDefinitionJsonFhirAdapter(fhirContext), new StructureDefinitionXmlFhirAdapter(fhirContext), new SubscriptionJsonFhirAdapter(fhirContext), new SubscriptionXmlFhirAdapter(fhirContext), new TaskJsonFhirAdapter(fhirContext), diff --git a/dsf-fhir/dsf-fhir-websocket-client/pom.xml b/dsf-fhir/dsf-fhir-websocket-client/pom.xml index f5828e649..e3bafc629 100755 --- a/dsf-fhir/dsf-fhir-websocket-client/pom.xml +++ b/dsf-fhir/dsf-fhir-websocket-client/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-fhir-pom - 0.7.0 + 0.8.0 diff --git a/dsf-fhir/pom.xml b/dsf-fhir/pom.xml index 9147cb5ae..7d0c7171e 100755 --- a/dsf-fhir/pom.xml +++ b/dsf-fhir/pom.xml @@ -8,7 +8,7 @@ org.highmed.dsf dsf-pom - 0.7.0 + 0.8.0 diff --git a/dsf-mpi/dsf-mpi-client-pdq/pom.xml b/dsf-mpi/dsf-mpi-client-pdq/pom.xml index a2fd8907a..56005968a 100644 --- a/dsf-mpi/dsf-mpi-client-pdq/pom.xml +++ b/dsf-mpi/dsf-mpi-client-pdq/pom.xml @@ -9,7 +9,7 @@ dsf-mpi-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-mpi/dsf-mpi-client-stub/pom.xml b/dsf-mpi/dsf-mpi-client-stub/pom.xml index 50589e69d..4001fed2d 100644 --- a/dsf-mpi/dsf-mpi-client-stub/pom.xml +++ b/dsf-mpi/dsf-mpi-client-stub/pom.xml @@ -9,7 +9,7 @@ dsf-mpi-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-mpi/dsf-mpi-client/pom.xml b/dsf-mpi/dsf-mpi-client/pom.xml index b9001f5fa..f2f283a83 100644 --- a/dsf-mpi/dsf-mpi-client/pom.xml +++ b/dsf-mpi/dsf-mpi-client/pom.xml @@ -9,7 +9,7 @@ dsf-mpi-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-mpi/pom.xml b/dsf-mpi/pom.xml index a6a247fd5..8ca3691f9 100644 --- a/dsf-mpi/pom.xml +++ b/dsf-mpi/pom.xml @@ -10,7 +10,7 @@ dsf-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-openehr/dsf-openehr-client-impl/pom.xml b/dsf-openehr/dsf-openehr-client-impl/pom.xml index a06dcb7dc..568b21393 100644 --- a/dsf-openehr/dsf-openehr-client-impl/pom.xml +++ b/dsf-openehr/dsf-openehr-client-impl/pom.xml @@ -9,7 +9,7 @@ dsf-openehr-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-openehr/dsf-openehr-client-stub/pom.xml b/dsf-openehr/dsf-openehr-client-stub/pom.xml index 64e51b14f..a818a4794 100644 --- a/dsf-openehr/dsf-openehr-client-stub/pom.xml +++ b/dsf-openehr/dsf-openehr-client-stub/pom.xml @@ -8,7 +8,7 @@ dsf-openehr-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-openehr/dsf-openehr-client/pom.xml b/dsf-openehr/dsf-openehr-client/pom.xml index 68608f0aa..ec3f0f171 100644 --- a/dsf-openehr/dsf-openehr-client/pom.xml +++ b/dsf-openehr/dsf-openehr-client/pom.xml @@ -8,7 +8,7 @@ dsf-openehr-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-openehr/dsf-openehr-model/pom.xml b/dsf-openehr/dsf-openehr-model/pom.xml index ea657afa1..dd70be2bc 100644 --- a/dsf-openehr/dsf-openehr-model/pom.xml +++ b/dsf-openehr/dsf-openehr-model/pom.xml @@ -8,7 +8,7 @@ dsf-openehr-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-openehr/pom.xml b/dsf-openehr/pom.xml index ebc9f7c79..fcc551984 100755 --- a/dsf-openehr/pom.xml +++ b/dsf-openehr/pom.xml @@ -10,7 +10,7 @@ dsf-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-pseudonymization/dsf-pseudonymization-base/pom.xml b/dsf-pseudonymization/dsf-pseudonymization-base/pom.xml index 9af74e8fd..c442a48da 100644 --- a/dsf-pseudonymization/dsf-pseudonymization-base/pom.xml +++ b/dsf-pseudonymization/dsf-pseudonymization-base/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-pseudonymization-pom - 0.7.0 + 0.8.0 diff --git a/dsf-pseudonymization/dsf-pseudonymization-client-stub/pom.xml b/dsf-pseudonymization/dsf-pseudonymization-client-stub/pom.xml index ab174b257..1534d6423 100644 --- a/dsf-pseudonymization/dsf-pseudonymization-client-stub/pom.xml +++ b/dsf-pseudonymization/dsf-pseudonymization-client-stub/pom.xml @@ -9,7 +9,7 @@ dsf-pseudonymization-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-pseudonymization/dsf-pseudonymization-client/pom.xml b/dsf-pseudonymization/dsf-pseudonymization-client/pom.xml index 9ca985988..12e3094ba 100644 --- a/dsf-pseudonymization/dsf-pseudonymization-client/pom.xml +++ b/dsf-pseudonymization/dsf-pseudonymization-client/pom.xml @@ -9,7 +9,7 @@ dsf-pseudonymization-pom org.highmed.dsf - 0.7.0 + 0.8.0 diff --git a/dsf-pseudonymization/dsf-pseudonymization-medic/pom.xml b/dsf-pseudonymization/dsf-pseudonymization-medic/pom.xml index b522db305..91654d4c5 100644 --- a/dsf-pseudonymization/dsf-pseudonymization-medic/pom.xml +++ b/dsf-pseudonymization/dsf-pseudonymization-medic/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-pseudonymization-pom - 0.7.0 + 0.8.0 diff --git a/dsf-pseudonymization/dsf-pseudonymization-ttp/pom.xml b/dsf-pseudonymization/dsf-pseudonymization-ttp/pom.xml index f6d3dbad3..8c6f8000c 100644 --- a/dsf-pseudonymization/dsf-pseudonymization-ttp/pom.xml +++ b/dsf-pseudonymization/dsf-pseudonymization-ttp/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-pseudonymization-pom - 0.7.0 + 0.8.0 diff --git a/dsf-pseudonymization/pom.xml b/dsf-pseudonymization/pom.xml index b7c6ed798..c416cdc81 100644 --- a/dsf-pseudonymization/pom.xml +++ b/dsf-pseudonymization/pom.xml @@ -8,7 +8,7 @@ org.highmed.dsf dsf-pom - 0.7.0 + 0.8.0 diff --git a/dsf-tools/dsf-tools-build-info-reader/pom.xml b/dsf-tools/dsf-tools-build-info-reader/pom.xml index 972e812d8..5bf0c9ce4 100644 --- a/dsf-tools/dsf-tools-build-info-reader/pom.xml +++ b/dsf-tools/dsf-tools-build-info-reader/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-tools-pom - 0.7.0 + 0.8.0 diff --git a/dsf-tools/dsf-tools-bundle-generator/pom.xml b/dsf-tools/dsf-tools-bundle-generator/pom.xml index 2bf548612..841048370 100755 --- a/dsf-tools/dsf-tools-bundle-generator/pom.xml +++ b/dsf-tools/dsf-tools-bundle-generator/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-tools-pom - 0.7.0 + 0.8.0 diff --git a/dsf-tools/dsf-tools-db-migration/pom.xml b/dsf-tools/dsf-tools-db-migration/pom.xml index 2306a5d5d..75776c53e 100755 --- a/dsf-tools/dsf-tools-db-migration/pom.xml +++ b/dsf-tools/dsf-tools-db-migration/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-tools-pom - 0.7.0 + 0.8.0 diff --git a/dsf-tools/dsf-tools-docker-secrets-reader/pom.xml b/dsf-tools/dsf-tools-docker-secrets-reader/pom.xml index c6fd2658a..6a2365eee 100644 --- a/dsf-tools/dsf-tools-docker-secrets-reader/pom.xml +++ b/dsf-tools/dsf-tools-docker-secrets-reader/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-tools-pom - 0.7.0 + 0.8.0 diff --git a/dsf-tools/dsf-tools-documentation-generator/pom.xml b/dsf-tools/dsf-tools-documentation-generator/pom.xml index 309c9ac11..475c590d2 100644 --- a/dsf-tools/dsf-tools-documentation-generator/pom.xml +++ b/dsf-tools/dsf-tools-documentation-generator/pom.xml @@ -9,7 +9,7 @@ org.highmed.dsf dsf-tools-pom - 0.7.0 + 0.8.0 diff --git a/dsf-tools/dsf-tools-proxy-test/pom.xml b/dsf-tools/dsf-tools-proxy-test/pom.xml index 34306cce3..7a0988305 100755 --- a/dsf-tools/dsf-tools-proxy-test/pom.xml +++ b/dsf-tools/dsf-tools-proxy-test/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-tools-pom - 0.7.0 + 0.8.0 @@ -84,6 +84,7 @@ + false diff --git a/dsf-tools/dsf-tools-test-data-generator/pom.xml b/dsf-tools/dsf-tools-test-data-generator/pom.xml index 8d4968fd8..c9390cb00 100755 --- a/dsf-tools/dsf-tools-test-data-generator/pom.xml +++ b/dsf-tools/dsf-tools-test-data-generator/pom.xml @@ -7,7 +7,7 @@ org.highmed.dsf dsf-tools-pom - 0.7.0 + 0.8.0 diff --git a/dsf-tools/dsf-tools-test-data-generator/src/main/java/org/highmed/dsf/tools/generator/EnvGenerator.java b/dsf-tools/dsf-tools-test-data-generator/src/main/java/org/highmed/dsf/tools/generator/EnvGenerator.java index c81b59c95..1030232de 100644 --- a/dsf-tools/dsf-tools-test-data-generator/src/main/java/org/highmed/dsf/tools/generator/EnvGenerator.java +++ b/dsf-tools/dsf-tools-test-data-generator/src/main/java/org/highmed/dsf/tools/generator/EnvGenerator.java @@ -71,7 +71,7 @@ public void generateAndWriteDockerTest3MedicTtpFhirEnvFiles( "medic1-client", "Webbrowser Test User").collect(Collectors.toList()); List confMedic1UserThumbprintsPermanentDelete = filterAndMapToThumbprint( clientCertificateFilesByCommonName, "medic1-client", "Webbrowser Test User") - .collect(Collectors.toList()); + .collect(Collectors.toList()); String bundleMedic2UserThumbprint = filterAndMapToThumbprint(clientCertificateFilesByCommonName, "medic2-client").findFirst().get(); @@ -79,7 +79,7 @@ public void generateAndWriteDockerTest3MedicTtpFhirEnvFiles( "medic2-client", "Webbrowser Test User").collect(Collectors.toList()); List confMedic2UserThumbprintsPermanentDelete = filterAndMapToThumbprint( clientCertificateFilesByCommonName, "medic2-client", "Webbrowser Test User") - .collect(Collectors.toList()); + .collect(Collectors.toList()); String bundleMedic3UserThumbprint = filterAndMapToThumbprint(clientCertificateFilesByCommonName, "medic3-client").findFirst().get(); @@ -87,7 +87,7 @@ public void generateAndWriteDockerTest3MedicTtpFhirEnvFiles( "medic3-client", "Webbrowser Test User").collect(Collectors.toList()); List confMedic3UserThumbprintsPermanentDelete = filterAndMapToThumbprint( clientCertificateFilesByCommonName, "medic3-client", "Webbrowser Test User") - .collect(Collectors.toList()); + .collect(Collectors.toList()); String bundleTtpUserThumbprint = filterAndMapToThumbprint(clientCertificateFilesByCommonName, "ttp-client") .findFirst().get(); @@ -152,7 +152,7 @@ public void generateAndWriteDockerTest3MedicTtpDockerFhirEnvFiles( "medic1-client", "Webbrowser Test User").collect(Collectors.toList()); List confMedic1UserThumbprintsPermanentDelete = filterAndMapToThumbprint( clientCertificateFilesByCommonName, "medic1-client", "Webbrowser Test User") - .collect(Collectors.toList()); + .collect(Collectors.toList()); String bundleMedic2UserThumbprint = filterAndMapToThumbprint(clientCertificateFilesByCommonName, "medic2-client").findFirst().get(); @@ -160,7 +160,7 @@ public void generateAndWriteDockerTest3MedicTtpDockerFhirEnvFiles( "medic2-client", "Webbrowser Test User").collect(Collectors.toList()); List confMedic2UserThumbprintsPermanentDelete = filterAndMapToThumbprint( clientCertificateFilesByCommonName, "medic2-client", "Webbrowser Test User") - .collect(Collectors.toList()); + .collect(Collectors.toList()); String bundleMedic3UserThumbprint = filterAndMapToThumbprint(clientCertificateFilesByCommonName, "medic3-client").findFirst().get(); @@ -168,7 +168,7 @@ public void generateAndWriteDockerTest3MedicTtpDockerFhirEnvFiles( "medic3-client", "Webbrowser Test User").collect(Collectors.toList()); List confMedic3UserThumbprintsPermanentDelete = filterAndMapToThumbprint( clientCertificateFilesByCommonName, "medic3-client", "Webbrowser Test User") - .collect(Collectors.toList()); + .collect(Collectors.toList()); String bundleTtpUserThumbprint = filterAndMapToThumbprint(clientCertificateFilesByCommonName, "ttp-client") .findFirst().get(); diff --git a/dsf-tools/pom.xml b/dsf-tools/pom.xml index d61f03611..cb029123f 100755 --- a/dsf-tools/pom.xml +++ b/dsf-tools/pom.xml @@ -8,7 +8,7 @@ org.highmed.dsf dsf-pom - 0.7.0 + 0.8.0 diff --git a/pom.xml b/pom.xml index 916f78483..3527b7003 100755 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.highmed.dsf dsf-pom - 0.7.0 + 0.8.0 pom @@ -25,12 +25,12 @@ ${project.basedir} 1.8.0-beta4 - 2.17.2 + 2.19.0 - 9.4.46.v20220331 - 2.35 - 1.18 - 5.3.21 + 9.4.49.v20220914 + 2.37 + 1.20 + 5.3.23 5.1.0 2.3 @@ -93,11 +93,16 @@ disruptor 3.4.4 - + - javax.mail - mail - 1.4.7 + com.sun.mail + jakarta.mail + 1.6.7 + + + org.bouncycastle + bcmail-jdk15on + 1.70 @@ -109,7 +114,7 @@ org.mockito mockito-core - 3.12.4 + 4.8.0 org.bouncycastle @@ -126,34 +131,34 @@ org.liquibase liquibase-core - 4.9.1 + 4.16.1 org.postgresql postgresql - 42.3.6 + 42.5.0 de.hs-heilbronn.mi jetty-utils - 0.17.0 + 0.18.0 de.hs-heilbronn.mi crypto-utils - 3.3.0 + 3.4.0 de.hs-heilbronn.mi log4j2-utils - 0.13.0 + 0.14.0 de.hs-heilbronn.mi db-test-utils - 0.17.0 + 0.18.0 @@ -206,12 +211,12 @@ com.fasterxml.jackson.core jackson-databind - 2.13.3 + 2.13.4 com.fasterxml.jackson.core jackson-annotations - 2.13.3 + 2.13.4 @@ -345,7 +350,7 @@ com.google.code.gson gson - 2.9.0 + 2.9.1 @@ -389,7 +394,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.2.2 + 3.3.0 @@ -428,33 +433,33 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.3.2 + 3.4.1 org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + 3.0.0-M7 org.apache.maven.plugins maven-failsafe-plugin - 2.22.2 + 3.0.0-M7 org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.4.0 org.apache.maven.plugins maven-assembly-plugin - 3.3.0 + 3.4.2 org.apache.maven.plugins maven-resources-plugin - 3.2.0 + 3.3.0 org.apache.maven.plugins @@ -469,17 +474,17 @@ org.apache.maven.plugins maven-clean-plugin - 3.1.0 + 3.2.0 net.revelc.code.formatter formatter-maven-plugin - 2.18.0 + 2.20.0 net.revelc.code impsort-maven-plugin - 1.6.2 + 1.7.0 org.apache.maven.plugins @@ -491,6 +496,11 @@ buildnumber-maven-plugin 1.4 + + io.fabric8 + docker-maven-plugin + 0.40.2 +