diff --git a/.gitignore b/.gitignore index be25738e7..f747c3080 100755 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ dsf-docker-test-setup-3medic-ttp-docker/**/fhir/log/*.log.gz dsf-docker-test-setup-3medic-ttp-docker/secrets/*.pem dsf-docker-test-setup-3medic-ttp-docker/.env +dsf-docker-test-setup-3medic-ttp-docker/docker-compose.override.yml ### # dsf-tools ignores diff --git a/CITATION.cff b/CITATION.cff index 2647bf3b0..ce5fcd561 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.8.0 -date-released: 2022-10-11 +version: 0.9.0 +date-released: 2022-10-17 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 diff --git a/dsf-bpe/dsf-bpe-process-base/pom.xml b/dsf-bpe/dsf-bpe-process-base/pom.xml index c09d953f2..6fc0db8f7 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.8.0 + 0.9.0 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 3979c6602..253a73cb7 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 @@ -12,6 +12,7 @@ import org.highmed.dsf.fhir.authorization.read.ReadAccessHelper; import org.highmed.dsf.fhir.client.FhirWebserviceClientProvider; import org.highmed.dsf.fhir.task.TaskHelper; +import org.hl7.fhir.r4.model.ResourceType; import org.hl7.fhir.r4.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,12 +26,6 @@ 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, ReadAccessHelper readAccessHelper) { @@ -50,8 +45,6 @@ public void afterPropertiesSet() throws Exception @Override public final void execute(DelegateExecution execution) throws Exception { - this.execution = execution; - try { logger.trace("Execution of task with id='{}'", execution.getCurrentActivityId()); @@ -61,12 +54,12 @@ public final void execute(DelegateExecution execution) throws Exception // Error boundary event, do not stop process execution catch (BpmnError error) { - Task task = getTask(); + Task task = getTask(execution); logger.debug("Error while executing service delegate " + getClass().getName(), error); logger.error( - "Process {} encountered error boundary event in step {} for task with id {}, error-code: {}, message: {}", - execution.getProcessDefinitionId(), execution.getActivityInstanceId(), task.getId(), + "Process {} encountered error boundary event in step {} for task {}, error-code: {}, message: {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), getTaskAbsoluteUrl(task), error.getErrorCode(), error.getMessage()); throw error; @@ -74,12 +67,12 @@ public final void execute(DelegateExecution execution) throws Exception // Not an error boundary event, stop process execution catch (Exception exception) { - Task task = getTask(); + Task task = getTask(execution); logger.debug("Error while executing service delegate " + getClass().getName(), exception); - logger.error("Process {} has fatal error in step {} for task with id {}, reason: {}", - execution.getProcessDefinitionId(), execution.getActivityInstanceId(), task.getId(), - exception.getMessage()); + logger.error("Process {} has fatal error in step {} for task {}, reason: {} - {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), getTaskAbsoluteUrl(task), + exception.getClass().getName(), exception.getMessage()); String errorMessage = "Process " + execution.getProcessDefinitionId() + " has fatal error in step " + execution.getActivityInstanceId() + ", reason: " + exception.getMessage(); @@ -96,6 +89,13 @@ public final void execute(DelegateExecution execution) throws Exception } } + protected final String getTaskAbsoluteUrl(Task task) + { + return task == null ? "?" + : task.getIdElement().toVersionless() + .withServerBase(clientProvider.getLocalBaseUrl(), ResourceType.Task.name()).getValue(); + } + /** * Method called by a BPMN service task * @@ -109,11 +109,6 @@ public final void execute(DelegateExecution execution) throws Exception */ protected abstract void doExecute(DelegateExecution execution) throws BpmnError, Exception; - protected final DelegateExecution getExecution() - { - return execution; - } - protected final TaskHelper getTaskHelper() { return taskHelper; @@ -130,36 +125,42 @@ protected final ReadAccessHelper getReadAccessHelper() } /** + * @param execution + * not null * @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() + protected final Task getTask(DelegateExecution execution) { return taskHelper.getTask(execution); } /** + * @param execution + * not null * @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() + protected final Task getCurrentTaskFromExecutionVariables(DelegateExecution execution) { return taskHelper.getCurrentTaskFromExecutionVariables(execution); } /** + * @param execution + * not null * @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() + protected final Task getLeadingTaskFromExecutionVariables(DelegateExecution execution) { return taskHelper.getLeadingTaskFromExecutionVariables(execution); } @@ -168,13 +169,15 @@ protected final Task getLeadingTaskFromExecutionVariables() * Use this method to update the process engine variable {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_TASK}, * after modifying the {@link Task}. * + * @param execution + * not null * @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) + protected final void updateCurrentTaskInExecutionVariables(DelegateExecution execution, Task task) { taskHelper.updateCurrentTaskInExecutionVariables(execution, task); } @@ -185,13 +188,15 @@ protected final void updateCurrentTaskInExecutionVariables(Task task) *

* Updates the current task if no leading task is set. * + * @param execution + * not null * @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) + protected final void updateLeadingTaskInExecutionVariables(DelegateExecution execution, Task 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 index 86bc1bbae..63e2f8152 100644 --- 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 @@ -22,9 +22,9 @@ 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.QuestionnaireResponse.QuestionnaireResponseItemComponent; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.ResourceType; import org.hl7.fhir.r4.model.StringType; @@ -44,12 +44,6 @@ public class DefaultUserTaskListener implements TaskListener, InitializingBean 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) @@ -74,7 +68,7 @@ public void afterPropertiesSet() throws Exception @Override public final void notify(DelegateTask userTask) { - this.execution = userTask.getExecution(); + DelegateExecution execution = userTask.getExecution(); try { @@ -88,25 +82,31 @@ public final void notify(DelegateTask userTask) QuestionnaireResponse questionnaireResponse = createDefaultQuestionnaireResponse( questionnaireUrlWithVersion, businessKey, userTaskId); - addPlaceholderAnswersToQuestionnaireResponse(questionnaireResponse, questionnaire); - - modifyQuestionnaireResponse(userTask, questionnaireResponse); + transformQuestionnaireItemsToQuestionnaireResponseItems(questionnaireResponse, questionnaire); + beforeQuestionnaireResponseCreate(userTask, questionnaireResponse); checkQuestionnaireResponse(questionnaireResponse); - IdType created = clientProvider.getLocalWebserviceClient().withRetryForever(60000) - .create(questionnaireResponse).getIdElement(); - execution.setVariable(BPMN_EXECUTION_VARIABLE_QUESTIONNAIRE_RESPONSE_ID, created.getIdPart()); + QuestionnaireResponse created = clientProvider.getLocalWebserviceClient().withRetryForever(60000) + .create(questionnaireResponse); + execution.setVariable(BPMN_EXECUTION_VARIABLE_QUESTIONNAIRE_RESPONSE_ID, + created.getIdElement().getIdPart()); + + logger.info("Created QuestionnaireResponse for user task at {}, process waiting for it's completion", + created.getIdElement().toVersionless().withServerBase(clientProvider.getLocalBaseUrl(), + ResourceType.QuestionnaireResponse.name())); - logger.info("Created user task with id={}, process waiting for it's completion", created.getValue()); + afterQuestionnaireResponseCreate(userTask, created); } catch (Exception exception) { - Task task = getTask(); + Task task = getTask(execution); 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(), + logger.error("Process {} has fatal error in step {} for task {}, reason: {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), + task.getIdElement().toVersionless() + .withServerBase(clientProvider.getLocalBaseUrl(), ResourceType.Task.name()).getValue(), exception.getMessage()); String errorMessage = "Process " + execution.getProcessDefinitionId() + " has fatal error in step " @@ -153,41 +153,55 @@ private QuestionnaireResponse createDefaultQuestionnaireResponse(String question 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, + if (addBusinessKeyToQuestionnaireResponse()) + { + questionnaireResponseHelper.addItemLeafWithAnswer(questionnaireResponse, + CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY, "The business-key of the process execution", + new StringType(businessKey)); + } + + questionnaireResponseHelper.addItemLeafWithAnswer(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, + private void transformQuestionnaireItemsToQuestionnaireResponseItems(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)); + .forEach(i -> transformItem(questionnaireResponse, i)); } - private void createAndAddAnswerPlaceholder(QuestionnaireResponse questionnaireResponse, + private void transformItem(QuestionnaireResponse questionnaireResponse, Questionnaire.QuestionnaireItemComponent question) { - Type answer = questionnaireResponseHelper.transformQuestionTypeToAnswerType(question); - questionnaireResponseHelper.addItemLeave(questionnaireResponse, question.getLinkId(), question.getText(), - answer); + if (Questionnaire.QuestionnaireItemType.DISPLAY.equals(question.getType())) + { + questionnaireResponseHelper.addItemLeafWithoutAnswer(questionnaireResponse, question.getLinkId(), + question.getText()); + } + else + { + Type answer = questionnaireResponseHelper.transformQuestionTypeToAnswerType(question); + questionnaireResponseHelper.addItemLeafWithAnswer(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 + "'")); + if (addBusinessKeyToQuestionnaireResponse()) + { + 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() @@ -199,23 +213,44 @@ private void checkQuestionnaireResponse(QuestionnaireResponse questionnaireRespo } /** - * Use this method to modify the {@link QuestionnaireResponse} before it will be created in state - * {@link QuestionnaireResponse.QuestionnaireResponseStatus#INPROGRESS} + * Override this method to decided if you want to add the Business-Key to the {@link QuestionnaireResponse} + * resource as an item with {@link QuestionnaireResponseItemComponent#getLinkId()} equal to + * {@link ConstantsBase#CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY} + * + * @return false + */ + protected boolean addBusinessKeyToQuestionnaireResponse() + { + return false; + } + + /** + * Override this method to modify the {@link QuestionnaireResponse} before it will be created in state + * {@link QuestionnaireResponse.QuestionnaireResponseStatus#INPROGRESS} on the DSF FHIR server * * @param userTask * not null, user task on which this {@link QuestionnaireResponse} is based - * @param questionnaireResponse + * @param beforeCreate * not null, containing an answer placeholder for every item in the corresponding * {@link Questionnaire} */ - protected void modifyQuestionnaireResponse(DelegateTask userTask, QuestionnaireResponse questionnaireResponse) + protected void beforeQuestionnaireResponseCreate(DelegateTask userTask, QuestionnaireResponse beforeCreate) { - // Nothing to do in default behaviour + // Nothing to do in default behavior } - protected final DelegateExecution getExecution() + /** + * Override this method to execute code after the {@link QuestionnaireResponse} resource has been created on the + * DSF FHIR server + * + * @param userTask + * not null, user task on which this {@link QuestionnaireResponse} is based + * @param afterCreate + * not null, created on the DSF FHIR server + */ + protected void afterQuestionnaireResponseCreate(DelegateTask userTask, QuestionnaireResponse afterCreate) { - return execution; + // Nothing to do in default behavior } protected final TaskHelper getTaskHelper() @@ -234,36 +269,42 @@ protected final ReadAccessHelper getReadAccessHelper() } /** + * @param execution + * not null * @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() + protected final Task getTask(DelegateExecution execution) { return taskHelper.getTask(execution); } /** + * @param execution + * not null * @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() + protected final Task getCurrentTaskFromExecutionVariables(DelegateExecution execution) { return taskHelper.getCurrentTaskFromExecutionVariables(execution); } /** + * @param execution + * not null * @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() + protected final Task getLeadingTaskFromExecutionVariables(DelegateExecution execution) { return taskHelper.getLeadingTaskFromExecutionVariables(execution); } @@ -272,13 +313,15 @@ protected final Task getLeadingTaskFromExecutionVariables() * Use this method to update the process engine variable {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_TASK}, * after modifying the {@link Task}. * + * @param execution + * not null * @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) + protected final void updateCurrentTaskInExecutionVariables(DelegateExecution execution, Task task) { taskHelper.updateCurrentTaskInExecutionVariables(execution, task); } @@ -289,13 +332,15 @@ protected final void updateCurrentTaskInExecutionVariables(Task task) *

* Updates the current task if no leading task is set. * + * @param execution + * not null * @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) + protected final void updateLeadingTaskInExecutionVariables(DelegateExecution execution, Task task) { taskHelper.updateLeadingTaskInExecutionVariables(execution, task); } 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 index e0fcf307a..8270ba425 100644 --- 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 @@ -37,5 +37,7 @@ Stream getItemLeavesAs Type transformQuestionTypeToAnswerType(Questionnaire.QuestionnaireItemComponent question); - void addItemLeave(QuestionnaireResponse questionnaireResponse, String linkId, String text, Type answer); + void addItemLeafWithoutAnswer(QuestionnaireResponse questionnaireResponse, String linkId, String text); + + void addItemLeafWithAnswer(QuestionnaireResponse questionnaireResponse, String linkId, String text, Type answer); } 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 36053a38d..492750202 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 @@ -84,11 +84,11 @@ public void doExecute(DelegateExecution execution) throws Exception // String targetOrganizationId = (String) execution.getVariable(Constants.VARIABLE_TARGET_ORGANIZATION_ID); // String correlationKey = (String) execution.getVariable(Constants.VARIABLE_CORRELATION_KEY); - Target target = getTarget(); + Target target = getTarget(execution); try { - sendTask(target, instantiatesUri, messageName, businessKey, profile, + sendTask(execution, target, instantiatesUri, messageName, businessKey, profile, getAdditionalInputParameters(execution)); } catch (Exception e) @@ -102,11 +102,11 @@ public void doExecute(DelegateExecution execution) throws Exception logger.debug("Error while sending Task", e); if (execution.getBpmnModelElementInstance() instanceof IntermediateThrowEvent) - handleIntermediateThrowEventError(e, errorMessage); + handleIntermediateThrowEventError(execution, e, errorMessage); else if (execution.getBpmnModelElementInstance() instanceof EndEvent) - handleEndEventError(e, errorMessage); + handleEndEventError(execution, e, errorMessage); else if (execution.getBpmnModelElementInstance() instanceof SendTask) - handleSendTaskError(e, errorMessage); + handleSendTaskError(execution, e, errorMessage); else logger.warn("Error handling for {} not implemented", execution.getBpmnModelElementInstance().getClass().getName()); @@ -119,14 +119,15 @@ protected void addErrorMessage(Task task, String errorMessage) errorMessage)); } - protected void handleIntermediateThrowEventError(Exception exception, String errorMessage) + protected void handleIntermediateThrowEventError(DelegateExecution execution, Exception exception, + String errorMessage) { - Task task = getLeadingTaskFromExecutionVariables(); + Task task = getLeadingTaskFromExecutionVariables(execution); logger.debug("Error while executing Task message send " + getClass().getName(), exception); - logger.error("Process {} has fatal error in step {} for task with id {}, reason: {}", - getExecution().getProcessDefinitionId(), getExecution().getActivityInstanceId(), - task == null ? "?" : task.getId(), exception.getMessage()); + logger.error("Process {} has fatal error in step {} for task {}, reason: {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), getTaskAbsoluteUrl(task), + exception.getMessage()); try { @@ -141,25 +142,25 @@ protected void handleIntermediateThrowEventError(Exception exception, String err } finally { - getExecution().getProcessEngine().getRuntimeService() - .deleteProcessInstance(getExecution().getProcessInstanceId(), exception.getMessage()); + execution.getProcessEngine().getRuntimeService().deleteProcessInstance(execution.getProcessInstanceId(), + exception.getMessage()); } } - protected void handleEndEventError(Exception exception, String errorMessage) + protected void handleEndEventError(DelegateExecution execution, Exception exception, String errorMessage) { - Task task = getLeadingTaskFromExecutionVariables(); + Task task = getLeadingTaskFromExecutionVariables(execution); logger.debug("Error while executing Task message send " + getClass().getName(), exception); - logger.error("Process {} has fatal error in step {} for task with id {}, reason: {}", - getExecution().getProcessDefinitionId(), getExecution().getActivityInstanceId(), - task == null ? "?" : task.getId(), exception.getMessage()); + logger.error("Process {} has fatal error in step {} for task {}, reason: {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), getTaskAbsoluteUrl(task), + exception.getMessage()); if (task != null) { addErrorMessage(task, errorMessage); task.setStatus(Task.TaskStatus.FAILED); - updateLeadingTaskInExecutionVariables(task); + updateLeadingTaskInExecutionVariables(execution, task); } else logger.warn("Leading Task null, unable to set failed state"); @@ -167,20 +168,20 @@ protected void handleEndEventError(Exception exception, String errorMessage) // Task update and process-end handled by EndListener } - protected void handleSendTaskError(Exception exception, String errorMessage) + protected void handleSendTaskError(DelegateExecution execution, Exception exception, String errorMessage) { - Task task = getLeadingTaskFromExecutionVariables(); - Targets targets = getTargets(); + Task task = getLeadingTaskFromExecutionVariables(execution); + Targets targets = getTargets(execution); // if we are a multi instance message send task, remove target if (targets != null) { addErrorMessage(task, errorMessage); - updateLeadingTaskInExecutionVariables(task); + updateLeadingTaskInExecutionVariables(execution, task); - Target target = getTarget(); + Target target = getTarget(execution); targets = targets.removeByEndpointIdentifierValue(target); - updateTargets(targets); + updateTargets(execution, targets); logger.debug("Target organization {}, endpoint {} with error {} removed from target list", target.getOrganizationIdentifierValue(), target.getEndpointIdentifierValue(), exception.getMessage()); @@ -188,9 +189,9 @@ protected void handleSendTaskError(Exception exception, String errorMessage) if (targets.isEmpty()) { 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: {}", - getExecution().getProcessDefinitionId(), getExecution().getActivityInstanceId(), - task == null ? "?" : task.getId(), exception.getMessage()); + logger.error("Process {} has fatal error in step {} for task {}, last reason: {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), getTaskAbsoluteUrl(task), + exception.getMessage()); try { @@ -204,8 +205,8 @@ protected void handleSendTaskError(Exception exception, String errorMessage) } finally { - getExecution().getProcessEngine().getRuntimeService() - .deleteProcessInstance(getExecution().getProcessInstanceId(), exception.getMessage()); + execution.getProcessEngine().getRuntimeService() + .deleteProcessInstance(execution.getProcessInstanceId(), exception.getMessage()); } } } @@ -216,57 +217,39 @@ protected void handleSendTaskError(Exception exception, String errorMessage) * then {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_TARGET}. * * @param execution - * the delegate execution of this process instance + * not null * @return {@link Target} that should receive the message - * @deprecated use {@link #getTarget()} */ - @Deprecated protected Target getTarget(DelegateExecution execution) { return (Target) execution.getVariable(BPMN_EXECUTION_VARIABLE_TARGET); } - /** - * Override this method if the {@link Target} variable is stored in a different process engine variable other - * then {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_TARGET}. - * - * @return {@link Target} that should receive the message - */ - protected Target getTarget() - { - if (getExecution() == null) - throw new IllegalStateException("execution not started"); - - return (Target) getExecution().getVariable(BPMN_EXECUTION_VARIABLE_TARGET); - } - /** * Override this method if the {@link Targets} variable is stored in a different process engine variable other * then {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_TARGETS}. * + * @param execution + * not null * @return {@link Targets} that should receive the message */ - protected Targets getTargets() + protected Targets getTargets(DelegateExecution execution) { - if (getExecution() == null) - throw new IllegalStateException("execution not started"); - - return (Targets) getExecution().getVariable(BPMN_EXECUTION_VARIABLE_TARGETS); + return (Targets) execution.getVariable(BPMN_EXECUTION_VARIABLE_TARGETS); } /** * Override this method if the {@link Targets} variable should stored in a different process engine variable * other then {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_TARGETS}. * + * @param execution + * not null * @param targets * the targets to save in process engine variable {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_TARGETS} */ - protected void updateTargets(Targets targets) + protected void updateTargets(DelegateExecution execution, Targets targets) { - if (getExecution() == null) - throw new IllegalStateException("execution not started"); - - getExecution().setVariable(BPMN_EXECUTION_VARIABLE_TARGETS, TargetsValues.create(targets)); + execution.setVariable(BPMN_EXECUTION_VARIABLE_TARGETS, TargetsValues.create(targets)); } /** @@ -286,33 +269,35 @@ protected Stream getAdditionalInputParameters(DelegateExecut * {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_ALTERNATIVE_BUSINESS_KEY}
*
* Use this method in combination with overriding - * {@link #sendTask(Target, String, String, String, String, Stream)} to use an alternative business-key with the - * communication target. + * {@link #sendTask(DelegateExecution, Target, String, String, String, String, Stream)} to use an alternative + * business-key with the communication target. * *

 	 * @Override
-	 * protected void sendTask(Target target, String instantiatesUri, String messageName, String businessKey,
-	 * 		String profile, Stream<ParameterComponent> additionalInputParameters)
+	 * protected void sendTask(DelegateExecution execution, Target target, String instantiatesUri, String messageName,
+	 * 		String businessKey, String profile, Stream<ParameterComponent> additionalInputParameters)
 	 * {
 	 * 	String alternativeBusinesKey = createAndSaveAlternativeBusinessKey();
-	 * 	super.sendTask(target, instantiatesUri, messageName, alternativeBusinesKey, profile,
+	 * 	super.sendTask(execution, target, instantiatesUri, messageName, alternativeBusinesKey, profile,
 	 * 			additionalInputParameters);
 	 * }
 	 * 
* + * @param execution + * not null * @return the alternative business-key stored as variable * {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_ALTERNATIVE_BUSINESS_KEY} */ - protected final String createAndSaveAlternativeBusinessKey() + protected final String createAndSaveAlternativeBusinessKey(DelegateExecution execution) { String alternativeBusinessKey = UUID.randomUUID().toString(); - getExecution().setVariable(BPMN_EXECUTION_VARIABLE_ALTERNATIVE_BUSINESS_KEY, alternativeBusinessKey); + execution.setVariable(BPMN_EXECUTION_VARIABLE_ALTERNATIVE_BUSINESS_KEY, alternativeBusinessKey); return alternativeBusinessKey; } - protected void sendTask(Target target, String instantiatesUri, String messageName, String businessKey, - String profile, Stream additionalInputParameters) + protected void sendTask(DelegateExecution execution, Target target, String instantiatesUri, String messageName, + String businessKey, String profile, Stream additionalInputParameters) { if (messageName.isEmpty() || instantiatesUri.isEmpty()) throw new IllegalStateException("Next process-id or message-name not definied"); diff --git a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/variables/FhirResourceSerializer.java b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/variables/FhirResourceSerializer.java index bdc7c8241..6cd603a12 100644 --- a/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/variables/FhirResourceSerializer.java +++ b/dsf-bpe/dsf-bpe-process-base/src/main/java/org/highmed/dsf/fhir/variables/FhirResourceSerializer.java @@ -9,6 +9,8 @@ import org.camunda.bpm.engine.variable.impl.value.UntypedValueImpl; import org.highmed.dsf.fhir.variables.FhirResourceValues.FhirResourceValue; import org.hl7.fhir.r4.model.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import ca.uhn.fhir.context.FhirContext; @@ -17,6 +19,8 @@ public class FhirResourceSerializer extends PrimitiveValueSerializer implements InitializingBean { + private static final Logger logger = LoggerFactory.getLogger(FhirResourceSerializer.class); + private final FhirContext fhirContext; public FhirResourceSerializer(FhirContext fhirContext) @@ -73,9 +77,18 @@ public FhirResourceValue readValue(ValueFields valueFields, boolean asTransientV try { - @SuppressWarnings("unchecked") - Class clazz = (Class) Class.forName(className); - Resource resource = newJsonParser().parseResource(clazz, new ByteArrayInputStream(bytes)); + Resource resource; + if (className != null) + { + @SuppressWarnings("unchecked") + Class clazz = (Class) Class.forName(className); + resource = newJsonParser().parseResource(clazz, new ByteArrayInputStream(bytes)); + } + else + { + logger.warn("ClassName from DB null, trying to parse FHIR resource without type information"); + resource = (Resource) newJsonParser().parseResource(new ByteArrayInputStream(bytes)); + } return FhirResourceValues.create(resource); } diff --git a/dsf-bpe/dsf-bpe-server-jetty/pom.xml b/dsf-bpe/dsf-bpe-server-jetty/pom.xml index 9d1ea2ce0..b97454738 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.8.0 + 0.9.0 diff --git a/dsf-bpe/dsf-bpe-server/pom.xml b/dsf-bpe/dsf-bpe-server/pom.xml index 08f531e0d..e8a52ec65 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.8.0 + 0.9.0 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 index 752d0f52a..d7d54b9af 100644 --- 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 @@ -5,7 +5,6 @@ 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; @@ -55,7 +54,7 @@ protected ExecutionListener getExecutionListenerInstance(ProcessKeyAndVersion pr { Object delegateInstance = instantiateDelegate(processKeyAndVersion, className, fieldDeclarations); - if (delegateInstance instanceof TaskListener) + if (delegateInstance instanceof ExecutionListener) { return (ExecutionListener) delegateInstance; } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/DebugLoggingBpmnParseListener.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/DebugLoggingBpmnParseListener.java new file mode 100644 index 000000000..bd0055acc --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/DebugLoggingBpmnParseListener.java @@ -0,0 +1,216 @@ +package org.highmed.dsf.bpe.listener; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.camunda.bpm.engine.impl.bpmn.parser.AbstractBpmnParseListener; +import org.camunda.bpm.engine.impl.bpmn.parser.BpmnParseListener; +import org.camunda.bpm.engine.impl.pvm.process.ActivityImpl; +import org.camunda.bpm.engine.impl.pvm.process.ScopeImpl; +import org.camunda.bpm.engine.impl.util.xml.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +public class DebugLoggingBpmnParseListener extends AbstractBpmnParseListener + implements BpmnParseListener, InitializingBean +{ + private static final class ExecutionListenerLogger implements ExecutionListener + { + final boolean logVariables; + + ExecutionListenerLogger(boolean logVariables) + { + this.logVariables = logVariables; + } + + @Override + public void notify(DelegateExecution execution) throws Exception + { + if (execution != null) + { + logger.debug( + "EventName: '{}', ActivityInstanceId: '{}', BusinessKey: '{}', CurrentActivityId: '{}', " + + "CurrentActivityName: '{}', ProcessDefinitionId: '{}', ProcessInstanceId: '{}'", + execution.getEventName(), execution.getActivityInstanceId(), execution.getBusinessKey(), + execution.getCurrentActivityId(), execution.getCurrentActivityName(), + execution.getProcessDefinitionId(), execution.getProcessInstanceId()); + + if (logVariables) + logger.debug("Variables: {}", execution.getVariables()); + } + else + { + logger.warn("Can't log, DelegateExecution is 'null'"); + } + } + } + + private static final Logger logger = LoggerFactory.getLogger(DebugLoggingBpmnParseListener.class); + + private final boolean logActivityStart; + private final boolean logActivityEnd; + private final boolean logVariables; + + private final ExecutionListener listener; + + public DebugLoggingBpmnParseListener(boolean logActivityStart, boolean logActivityEnd, boolean logVariables) + { + this.logActivityStart = logActivityStart; + this.logActivityEnd = logActivityEnd; + this.logVariables = logVariables; + + listener = new ExecutionListenerLogger(logVariables); + } + + @Override + public void afterPropertiesSet() throws Exception + { + if (logActivityStart) + logger.warn( + "Process activity start debug logging enabled. This should only be activated during process plugin development"); + + if (logActivityEnd) + logger.warn( + "Process activity end debug logging enabled. This should only be activated during process plugin development"); + + if (logVariables) + logger.warn( + "Process variable debug logging enabled. This should only be activated during process plugin development. WARNNING: Confidential information may be leaked via the debug log!"); + } + + private void addListeners(ActivityImpl activity) + { + if (logActivityStart) + activity.addListener(ExecutionListener.EVENTNAME_START, listener, 0); + + if (logActivityEnd) + activity.addListener(ExecutionListener.EVENTNAME_END, listener, 0); + } + + @Override + public void parseStartEvent(Element startEventElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseExclusiveGateway(Element exclusiveGwElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseInclusiveGateway(Element inclusiveGwElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseParallelGateway(Element parallelGwElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseScriptTask(Element scriptTaskElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseServiceTask(Element serviceTaskElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseBusinessRuleTask(Element businessRuleTaskElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseTask(Element taskElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseManualTask(Element manualTaskElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseUserTask(Element userTaskElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseEndEvent(Element endEventElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseSubProcess(Element subProcessElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseCallActivity(Element callActivityElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseSendTask(Element sendTaskElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseReceiveTask(Element receiveTaskElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseEventBasedGateway(Element eventBasedGwElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseTransaction(Element transactionElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseIntermediateThrowEvent(Element intermediateEventElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseIntermediateCatchEvent(Element intermediateEventElement, ScopeImpl scope, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseBoundaryEvent(Element boundaryEventElement, ScopeImpl scopeElement, ActivityImpl activity) + { + addListeners(activity); + } + + @Override + public void parseMultiInstanceLoopCharacteristics(Element activityElement, + Element multiInstanceLoopCharacteristicsElement, ActivityImpl activity) + { + addListeners(activity); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/DefaultBpmnParseListener.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/DefaultBpmnParseListener.java index 25412420e..94ac8b2e5 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/DefaultBpmnParseListener.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/DefaultBpmnParseListener.java @@ -47,7 +47,10 @@ public void parseStartEvent(Element startEventElement, ScopeImpl scope, Activity @Override public void parseEndEvent(Element endEventElement, ScopeImpl scope, ActivityImpl endEventActivity) { - endEventActivity.addListener(ExecutionListener.EVENTNAME_END, endListener); + // Adding at index 0 to the end phase of the EndEvent, so processes can execute listeners after the Task + // resource has been updated. + // Listeners added to the end phase of the EndEvent via bpmn are execute after this listener + endEventActivity.addListener(ExecutionListener.EVENTNAME_END, endListener, 0); } @Override diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/EndListener.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/EndListener.java index 2051de522..b0adeccbb 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/EndListener.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/EndListener.java @@ -8,15 +8,19 @@ import static org.highmed.dsf.bpe.ConstantsBase.CODESYSTEM_HIGHMED_BPMN_VALUE_CORRELATION_KEY; import static org.highmed.dsf.bpe.ConstantsBase.CODESYSTEM_HIGHMED_BPMN_VALUE_MESSAGE_NAME; +import java.util.Objects; + import org.camunda.bpm.engine.delegate.DelegateExecution; import org.camunda.bpm.engine.delegate.ExecutionListener; import org.camunda.bpm.engine.variable.Variables; import org.highmed.dsf.bpe.ConstantsBase; import org.highmed.dsf.fhir.task.TaskHelper; import org.highmed.fhir.client.FhirWebserviceClient; +import org.hl7.fhir.r4.model.ResourceType; import org.hl7.fhir.r4.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; /** * Added to each BPMN EndEvent by the {@link DefaultBpmnParseListener}. Is used to set the FHIR {@link Task} status as @@ -24,17 +28,24 @@ * {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_IN_CALLED_PROCESS} back to false if a called sub process * ends. */ -public class EndListener implements ExecutionListener +public class EndListener implements ExecutionListener, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(EndListener.class); - private final TaskHelper taskHelper; private final FhirWebserviceClient webserviceClient; + private final TaskHelper taskHelper; public EndListener(FhirWebserviceClient webserviceClient, TaskHelper taskHelper) { - this.taskHelper = taskHelper; this.webserviceClient = webserviceClient; + this.taskHelper = taskHelper; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(webserviceClient, "webserviceClient"); + Objects.requireNonNull(taskHelper, "taskHelper"); } @Override @@ -82,9 +93,10 @@ private void log(DelegateExecution execution, Task task) CODESYSTEM_HIGHMED_BPMN_VALUE_BUSINESS_KEY).orElse(null); String correlationKey = taskHelper.getFirstInputParameterStringValue(task, CODESYSTEM_HIGHMED_BPMN, CODESYSTEM_HIGHMED_BPMN_VALUE_CORRELATION_KEY).orElse(null); - String taskId = task.getIdElement().getIdPart(); + String taskUrl = task.getIdElement().toVersionless() + .withServerBase(webserviceClient.getBaseUrl(), ResourceType.Task.name()).getValue(); - logger.info("Process {} finished [message: {}, businessKey: {}, correlationKey: {}, taskId: {}]", processUrl, - messageName, businessKey, correlationKey, taskId); + logger.info("Process {} finished [message: {}, businessKey: {}, correlationKey: {}, task: {}]", processUrl, + messageName, businessKey, correlationKey, taskUrl); } } \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/StartListener.java b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/StartListener.java index b5b3f8601..0e7200864 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/StartListener.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/org/highmed/dsf/bpe/listener/StartListener.java @@ -16,24 +16,35 @@ import org.highmed.dsf.bpe.ConstantsBase; import org.highmed.dsf.fhir.task.TaskHelper; import org.highmed.dsf.fhir.variables.FhirResourceValues; +import org.hl7.fhir.r4.model.ResourceType; import org.hl7.fhir.r4.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; /** * Added to each BPMN StartEvent by the {@link DefaultBpmnParseListener}. Initializes the * {@link ConstantsBase#BPMN_EXECUTION_VARIABLE_IN_CALLED_PROCESS} variable with false for processes * started via a {@link Task} resource. */ -public class StartListener implements ExecutionListener +public class StartListener implements ExecutionListener, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(StartListener.class); - private TaskHelper taskHelper; + private final TaskHelper taskHelper; + private final String baseUrl; - public StartListener(TaskHelper taskHelper) + public StartListener(TaskHelper taskHelper, String baseUrl) { - this.taskHelper = Objects.requireNonNull(taskHelper, "taskHelper"); + this.taskHelper = taskHelper; + this.baseUrl = baseUrl; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(taskHelper, "taskHelper"); + Objects.requireNonNull(baseUrl, "baseUrl"); } @Override @@ -69,9 +80,10 @@ private void log(DelegateExecution execution, Task task) CODESYSTEM_HIGHMED_BPMN_VALUE_BUSINESS_KEY).orElse(null); String correlationKey = taskHelper.getFirstInputParameterStringValue(task, CODESYSTEM_HIGHMED_BPMN, CODESYSTEM_HIGHMED_BPMN_VALUE_CORRELATION_KEY).orElse(null); - String taskId = task.getIdElement().getIdPart(); + String taskUrl = task.getIdElement().toVersionless().withServerBase(baseUrl, ResourceType.Task.name()) + .getValue(); - logger.info("Starting process {} [message: {}, businessKey: {}, correlationKey: {}, taskId: {}]", processUrl, - messageName, businessKey, correlationKey, taskId); + logger.info("Starting process {} [message: {}, businessKey: {}, correlationKey: {}, task: {}]", processUrl, + messageName, businessKey, correlationKey, taskUrl); } } 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 b89ee21d0..7b808d412 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 @@ -5,6 +5,9 @@ import org.camunda.bpm.engine.ProcessEngine; import org.camunda.bpm.engine.RepositoryService; +import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.camunda.bpm.engine.delegate.JavaDelegate; +import org.camunda.bpm.engine.delegate.TaskListener; import org.camunda.bpm.engine.repository.ProcessDefinition; import org.camunda.bpm.model.bpmn.instance.EndEvent; import org.camunda.bpm.model.bpmn.instance.ExtensionElements; @@ -50,7 +53,7 @@ public void afterPropertiesSet() throws Exception @Override public void validateModels() { - logger.debug("Validating bpmn models, checking service delegate availability"); + logger.debug("Validating bpmn models, checking service delegate / listener availability"); RepositoryService repositoryService = processEngine.getRepositoryService(); @@ -73,44 +76,48 @@ private void validateBeanAvailabilityForProcess(ModelElementInstance parent, Pro { // service tasks parent.getChildElementsByType(ServiceTask.class).stream().filter(t -> t != null) - .map(ServiceTask::getCamundaClass).forEach(c -> validateBeanAvailability(process, c)); + .map(ServiceTask::getCamundaClass) + .forEach(c -> validateBeanAvailability(process, c, JavaDelegate.class)); // send tasks parent.getChildElementsByType(SendTask.class).stream().filter(t -> t != null).map(SendTask::getCamundaClass) - .forEach(c -> validateBeanAvailability(process, c)); + .forEach(c -> validateBeanAvailability(process, c, JavaDelegate.class)); // 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)); + .map(CamundaTaskListener::getCamundaClass) + .forEach(c -> validateBeanAvailability(process, c, TaskListener.class)); // 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)); + .map(CamundaExecutionListener::getCamundaClass) + .forEach(c -> validateBeanAvailability(process, c, ExecutionListener.class)); // 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(MessageEventDefinition::getCamundaClass) - .forEach(c -> validateBeanAvailability(process, c)); + .forEach(c -> validateBeanAvailability(process, c, JavaDelegate.class)); // 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(MessageEventDefinition::getCamundaClass) - .forEach(c -> validateBeanAvailability(process, c)); + .forEach(c -> validateBeanAvailability(process, c, JavaDelegate.class)); // sub processes parent.getChildElementsByType(SubProcess.class).stream().filter(s -> s != null) .forEach(subProcess -> validateBeanAvailabilityForProcess(subProcess, process)); } - private void validateBeanAvailability(Process process, String className) + // throws exceptions and stops bpe startup, these exceptions are not expected for tested processes + private void validateBeanAvailability(Process process, String className, Class expectedInterface) { if (className == null || className.isBlank()) return; @@ -120,11 +127,12 @@ private void validateBeanAvailability(Process process, String className) logger.trace("Checking {} available in {}", className, processKeyAndVersion); - Class serviceClass = loadClass(processKeyAndVersion, className); - loadBean(processKeyAndVersion, serviceClass); + Class serviceClass = loadClass(processKeyAndVersion, expectedInterface, className); + checkExpectedInterface(processKeyAndVersion, expectedInterface, serviceClass); + loadBean(processKeyAndVersion, expectedInterface, serviceClass); } - private Class loadClass(ProcessKeyAndVersion processKeyAndVersion, String className) + private Class loadClass(ProcessKeyAndVersion processKeyAndVersion, Class expectedInterface, String className) { try { @@ -133,12 +141,24 @@ private Class loadClass(ProcessKeyAndVersion processKeyAndVersion, String cla } catch (ClassNotFoundException e) { - logger.warn("Service delegate class {} defined in process {} not found", className, processKeyAndVersion); + logger.warn("{} {} defined in process {} not found", expectedInterface.getClass().getSimpleName(), + className, processKeyAndVersion); throw new RuntimeException(e); } } - private void loadBean(ProcessKeyAndVersion processKeyAndVersion, Class serviceClass) + private void checkExpectedInterface(ProcessKeyAndVersion processKeyAndVersion, Class expectedInterface, + Class serviceClass) + { + if (!expectedInterface.isAssignableFrom(serviceClass)) + { + logger.warn("Class {} defined in process {} is not implementing the {} interface", serviceClass.getName(), + processKeyAndVersion, expectedInterface.getSimpleName()); + throw new RuntimeException(serviceClass.getName() + " must implement " + expectedInterface.getName()); + } + } + + private void loadBean(ProcessKeyAndVersion processKeyAndVersion, Class expectedInterface, Class serviceClass) { try { @@ -147,8 +167,9 @@ private void loadBean(ProcessKeyAndVersion processKeyAndVersion, Class servic } catch (BeansException e) { - logger.error("Unable to find service delegate bean of type {} defined in process {}: {}", - serviceClass.getName(), processKeyAndVersion, e.getMessage()); + logger.warn("Unable to find {} bean of type {} defined in process {}: {}", + expectedInterface.getSimpleName(), serviceClass.getName(), processKeyAndVersion, e.getMessage()); + throw 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 index 122e6c250..127ce65f4 100644 --- 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 @@ -508,9 +508,12 @@ private MimeMultipart signMessage(MimeBodyPart body) { try { - return new MimeMultipart(body); + if (body.getContent() != null && body.getContent() instanceof MimeMultipart) + return (MimeMultipart) body.getContent(); + else + return new MimeMultipart(body); } - catch (MessagingException e) + catch (MessagingException | IOException e) { throw new RuntimeException(e); } 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 2e27b3676..a99576a7a 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 @@ -15,6 +15,7 @@ import org.highmed.dsf.bpe.delegate.DelegateProvider; import org.highmed.dsf.bpe.delegate.DelegateProviderImpl; import org.highmed.dsf.bpe.listener.CallActivityListener; +import org.highmed.dsf.bpe.listener.DebugLoggingBpmnParseListener; import org.highmed.dsf.bpe.listener.DefaultBpmnParseListener; import org.highmed.dsf.bpe.listener.DefaultUserTaskListener; import org.highmed.dsf.bpe.listener.EndListener; @@ -88,7 +89,7 @@ private String toString(char[] password) @Bean public StartListener startListener() { - return new StartListener(fhirConfig.taskHelper()); + return new StartListener(fhirConfig.taskHelper(), clientProvider.getLocalBaseUrl()); } @Bean @@ -109,6 +110,13 @@ public DefaultBpmnParseListener defaultBpmnParseListener() return new DefaultBpmnParseListener(startListener(), endListener(), callActivityListener()); } + @Bean + public DebugLoggingBpmnParseListener debugLoggingBpmnParseListener() + { + return new DebugLoggingBpmnParseListener(propertiesConfig.getDebugLogMessageOnActivityStart(), + propertiesConfig.getDebugLogMessageOnActivityEnd(), propertiesConfig.getDebugLogMessageVariables()); + } + @Bean public SpringProcessEngineConfiguration processEngineConfiguration() throws IOException { @@ -118,7 +126,7 @@ public SpringProcessEngineConfiguration processEngineConfiguration() throws IOEx c.setTransactionManager(transactionManager()); c.setDatabaseSchemaUpdate("false"); c.setJobExecutorActivate(true); - c.setCustomPreBPMNParseListeners(List.of(defaultBpmnParseListener())); + c.setCustomPreBPMNParseListeners(List.of(defaultBpmnParseListener(), debugLoggingBpmnParseListener())); c.setCustomPreVariableSerializers(baseSerializers); c.setFallbackSerializerFactory(getFallbackSerializerFactory()); 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 c3c19baed..8dae12e6a 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 @@ -250,6 +250,18 @@ public class PropertiesConfig @Value("${org.highmed.dsf.bpe.mail.mailOnErrorLogEventDebugLogLocation:/opt/bpe/log/bpe.log}") private String mailOnErrorLogEventDebugLogLocation; + @Documentation(description = "To enable debug log messages for every bpmn activity start, set to `true`.", recommendation = "This debug function should only be activated during process plugin development.") + @Value("${org.highmed.dsf.bpe.debug.log.message.onActivityStart:false}") + private boolean debugLogMessageOnActivityStart; + + @Documentation(description = "To enable debug log messages for every bpmn activity end, set to `true`.", recommendation = "This debug function should only be activated during process plugin development.") + @Value("${org.highmed.dsf.bpe.debug.log.message.onActivityEnd:false}") + private boolean debugLogMessageOnActivityEnd; + + @Documentation(description = "To enable loging bpmn variables for every bpmn activity start or end, when logging of these events is enabled, set to `true`.", recommendation = "This debug function should only be activated during process plugin development. WARNNING: Confidential information may be leaked via the debug log!") + @Value("${org.highmed.dsf.bpe.debug.log.message.variables:false}") + private boolean debugLogMessageVariables; + @Bean // static in order to initialize before @Configuration classes public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer( ConfigurableEnvironment environment) @@ -548,4 +560,19 @@ public String getMailOnErrorLogEventDebugLogLocation() { return mailOnErrorLogEventDebugLogLocation; } + + public boolean getDebugLogMessageOnActivityStart() + { + return debugLogMessageOnActivityStart; + } + + public boolean getDebugLogMessageOnActivityEnd() + { + return debugLogMessageOnActivityEnd; + } + + public boolean getDebugLogMessageVariables() + { + return debugLogMessageVariables; + } } 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 index 2cfd534fd..c9ac07000 100644 --- 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 @@ -47,25 +47,32 @@ public void onResource(QuestionnaireResponse questionnaireResponse) 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); + questionnaireResponseId).orElse("?"); + + Optional userTaskIdOpt = getStringValueFromItems(items, + CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID, questionnaireResponseId); + + userTaskIdOpt.ifPresentOrElse(userTaskId -> + { + logger.info( + "QuestionnaireResponse '{}' for Questionnaire '{}' completed [userTaskId: {}, businessKey: {}, user: {}]", + questionnaireResponseId, questionnaire, userTaskId, businessKey, user + "|" + userType); + + Map variables = Map.of(BPMN_EXECUTION_VARIABLE_QUESTIONNAIRE_RESPONSE_COMPLETED, + FhirResourceValues.create(questionnaireResponse)); + userTaskService.complete(userTaskId, variables); + }, () -> + { + logger.warn( + "QuestionnaireResponse '{}' for Questionnaire '{}' has no answer with item.linkId '{}' [businessKey: {}, user: {}], ignoring QuestionnaireResponse", + questionnaireResponseId, questionnaire, CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID, + businessKey, user + "|" + userType); + }); } - catch (Exception exception) + catch (Exception e) { - // TODO handle exception - throw new RuntimeException(exception); + logger.warn("Unable to complete UserTask", e); + throw new RuntimeException(e); } } @@ -79,7 +86,7 @@ private Optional getStringValueFromItems( if (answers.size() == 0) { - logger.warn("QuestionnaireResponse with id '{}' did not contain any linkId '{}'", questionnaireResponseId, + logger.info("QuestionnaireResponse with id '{}' did not contain any linkId '{}'", questionnaireResponseId, linkId); return Optional.empty(); } 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 index 5ef5b2c49..7134e043a 100644 --- 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 @@ -49,7 +49,8 @@ private Stream leaves( } @Override - public void addItemLeave(QuestionnaireResponse questionnaireResponse, String linkId, String text, Type answer) + public void addItemLeafWithAnswer(QuestionnaireResponse questionnaireResponse, String linkId, String text, + Type answer) { List answerComponent = Collections .singletonList(new QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(answer)); @@ -57,6 +58,12 @@ public void addItemLeave(QuestionnaireResponse questionnaireResponse, String lin questionnaireResponse.addItem().setLinkId(linkId).setText(text).setAnswer(answerComponent); } + @Override + public void addItemLeafWithoutAnswer(QuestionnaireResponse questionnaireResponse, String linkId, String text) + { + questionnaireResponse.addItem().setLinkId(linkId).setText(text); + } + @Override public Type transformQuestionTypeToAnswerType(Questionnaire.QuestionnaireItemComponent question) { @@ -82,8 +89,8 @@ public Type transformQuestionTypeToAnswerType(Questionnaire.QuestionnaireItemCom 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"); + throw new RuntimeException("Type '" + question.getType().getDisplay() + + "' in Questionnaire.item is not supported as answer type"); } } } diff --git a/dsf-bpe/dsf-bpe-webservice-client/pom.xml b/dsf-bpe/dsf-bpe-webservice-client/pom.xml index 03abb546c..896a48f7c 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.8.0 + 0.9.0 diff --git a/dsf-bpe/pom.xml b/dsf-bpe/pom.xml index 33e260d2f..a26f67448 100755 --- a/dsf-bpe/pom.xml +++ b/dsf-bpe/pom.xml @@ -8,7 +8,7 @@ org.highmed.dsf dsf-pom - 0.8.0 + 0.9.0 diff --git a/dsf-consent/dsf-consent-client-stub/pom.xml b/dsf-consent/dsf-consent-client-stub/pom.xml index 3f565da81..76a7be8ee 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.8.0 + 0.9.0 diff --git a/dsf-consent/dsf-consent-client/pom.xml b/dsf-consent/dsf-consent-client/pom.xml index a8c6b2bdf..870cd2c59 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.8.0 + 0.9.0 diff --git a/dsf-consent/pom.xml b/dsf-consent/pom.xml index 24d963486..029275508 100644 --- a/dsf-consent/pom.xml +++ b/dsf-consent/pom.xml @@ -10,7 +10,7 @@ dsf-pom org.highmed.dsf - 0.8.0 + 0.9.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 7680914cd..8d477c66c 100644 --- a/dsf-docker-test-setup-3medic-ttp-docker/docker-compose.yml +++ b/dsf-docker-test-setup-3medic-ttp-docker/docker-compose.yml @@ -354,7 +354,7 @@ services: 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 + ORG_HIGHMED_DSF_BPE_PROCESS_EXCLUDED: highmedorg_computeFeasibility/0.7.0,highmedorg_computeDataSharing/0.7.0,highmedorg_requestUpdateResources/0.7.0,highmedorg_updateAllowList/0.7.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 networks: @@ -420,7 +420,7 @@ services: 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 + ORG_HIGHMED_DSF_BPE_PROCESS_EXCLUDED: highmedorg_computeFeasibility/0.7.0, highmedorg_computeDataSharing/0.7.0, highmedorg_requestUpdateResources/0.7.0, highmedorg_updateAllowList/0.7.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 networks: @@ -487,10 +487,10 @@ services: 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, - highmedorg_requestUpdateResources/0.6.0, - highmedorg_updateAllowList/0.6.0 + highmedorg_computeFeasibility/0.7.0, + highmedorg_computeDataSharing/0.7.0, + highmedorg_requestUpdateResources/0.7.0, + highmedorg_updateAllowList/0.7.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 networks: @@ -559,16 +559,16 @@ services: 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 - highmedorg_localServicesIntegration/0.6.0 - highmedorg_requestFeasibility/0.6.0 - highmedorg_executeFeasibility/0.6.0 - highmedorg_requestDataSharing/0.6.0 - highmedorg_executeDataSharing/0.6.0 - highmedorg_executeFeasibilityMpcMultiShare/0.6.0 - highmedorg_executeFeasibilityMpcSingleShare/0.6.0 - highmedorg_requestFeasibilityMpc/0.6.0 + highmedorg_executeUpdateResources/0.7.0 + highmedorg_downloadAllowList/0.7.0 + highmedorg_localServicesIntegration/0.7.0 + highmedorg_requestFeasibility/0.7.0 + highmedorg_executeFeasibility/0.7.0 + highmedorg_requestDataSharing/0.7.0 + highmedorg_executeDataSharing/0.7.0 + highmedorg_executeFeasibilityMpcMultiShare/0.7.0 + highmedorg_executeFeasibilityMpcSingleShare/0.7.0 + highmedorg_requestFeasibilityMpc/0.7.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 networks: 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 1c1a71865..8267c8474 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 @@ -45,10 +45,10 @@ services: ORG_HIGHMED_DSF_BPE_FHIR_SERVER_ORGANIZATION_IDENTIFIER_VALUE: Test_MeDIC_1 ORG_HIGHMED_DSF_BPE_FHIR_SERVER_BASE_URL: https://medic1/fhir 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 + highmedorg_computeFeasibility/0.7.0 + highmedorg_computeDataSharing/0.7.0 + highmedorg_requestUpdateResources/0.7.0 + highmedorg_updateAllowList/0.7.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 networks: 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 e8f1bb807..85de518c8 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 @@ -45,10 +45,10 @@ services: ORG_HIGHMED_DSF_BPE_FHIR_SERVER_ORGANIZATION_IDENTIFIER_VALUE: Test_MeDIC_2 ORG_HIGHMED_DSF_BPE_FHIR_SERVER_BASE_URL: https://medic2/fhir 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 + highmedorg_computeFeasibility/0.7.0 + highmedorg_computeDataSharing/0.7.0 + highmedorg_requestUpdateResources/0.7.0 + highmedorg_updateAllowList/0.7.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 networks: 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 d9bbb345b..5604b0532 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 @@ -45,10 +45,10 @@ services: ORG_HIGHMED_DSF_BPE_FHIR_SERVER_ORGANIZATION_IDENTIFIER_VALUE: Test_MeDIC_3 ORG_HIGHMED_DSF_BPE_FHIR_SERVER_BASE_URL: https://medic3/fhir 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 + highmedorg_computeFeasibility/0.7.0 + highmedorg_computeDataSharing/0.7.0 + highmedorg_requestUpdateResources/0.7.0 + highmedorg_updateAllowList/0.7.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 networks: 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 994f5da78..86c96e8a2 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 @@ -45,16 +45,16 @@ services: ORG_HIGHMED_DSF_BPE_FHIR_SERVER_ORGANIZATION_IDENTIFIER_VALUE: Test_TTP ORG_HIGHMED_DSF_BPE_FHIR_SERVER_BASE_URL: https://ttp/fhir ORG_HIGHMED_DSF_BPE_PROCESS_EXCLUDED: | - highmedorg_executeUpdateResources/0.6.0 - highmedorg_downloadAllowList/0.6.0 - highmedorg_localServicesIntegration/0.6.0 - highmedorg_requestFeasibility/0.6.0 - highmedorg_executeFeasibility/0.6.0 - highmedorg_requestDataSharing/0.6.0 - highmedorg_executeDataSharing/0.6.0 - highmedorg_executeFeasibilityMpcMultiShare/0.6.0 - highmedorg_executeFeasibilityMpcSingleShare/0.6.0 - highmedorg_requestFeasibilityMpc/0.6.0 + highmedorg_executeUpdateResources/0.7.0 + highmedorg_downloadAllowList/0.7.0 + highmedorg_localServicesIntegration/0.7.0 + highmedorg_requestFeasibility/0.7.0 + highmedorg_executeFeasibility/0.7.0 + highmedorg_requestDataSharing/0.7.0 + highmedorg_executeDataSharing/0.7.0 + highmedorg_executeFeasibilityMpcMultiShare/0.7.0 + highmedorg_executeFeasibilityMpcSingleShare/0.7.0 + highmedorg_requestFeasibilityMpc/0.7.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 networks: diff --git a/dsf-fhir/dsf-fhir-auth/pom.xml b/dsf-fhir/dsf-fhir-auth/pom.xml index 79c4296cc..ea41878d1 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.8.0 + 0.9.0 diff --git a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml index aaccbb44a..210c29c9b 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.8.0 + 0.9.0 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 index c87c6fff1..388e00bc6 100644 --- 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 @@ -113,18 +113,43 @@ private String getProcessInstanceId(QuestionnaireResponse questionnaireResponse) private void writeRow(QuestionnaireResponse.QuestionnaireResponseItemComponent item, boolean isCompleted, OutputStreamWriter out) throws IOException + { + if (item.hasAnswer()) + writeFormRow(item, isCompleted, out); + else + writeDisplayRow(item, out); + } + + private void writeDisplayRow(QuestionnaireResponse.QuestionnaireResponseItemComponent item, OutputStreamWriter out) + throws IOException { String linkId = item.getLinkId(); - String style = display(linkId) ? "" : "style=\"display:none;\""; - out.write("
\n"); + out.write("
\n"); + out.write("

" + item.getText() + "\n"); + out.write("

\n"); + } + + private void writeFormRow(QuestionnaireResponse.QuestionnaireResponseItemComponent item, boolean isCompleted, + OutputStreamWriter out) throws IOException + { + String linkId = item.getLinkId(); + + out.write("
\n"); out.write("\n"); + writeFormInput(item.getAnswerFirstRep(), linkId, isCompleted, out); + out.write("
    \n"); out.write("
\n"); out.write("
\n"); } + private String style(String linkId) + { + return display(linkId) ? "" : "style=\"display:none;\""; + } + private boolean display(String linkId) { return !(CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY.equals(linkId) @@ -136,74 +161,79 @@ private void writeFormInput(QuestionnaireResponse.QuestionnaireResponseItemAnswe { 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 + // if type is null, the corresponding Questionnaire.item is of type display + if (type != null) { - throw new RuntimeException("Answer type '" + ((type != null) ? type.getClass().getName() : "null") - + "' in QuestionnaireResponse.item is not supported"); + 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.getClass().getName() + + "' in QuestionnaireResponse.item is not supported"); + } } } } diff --git a/dsf-fhir/dsf-fhir-server-jetty/pom.xml b/dsf-fhir/dsf-fhir-server-jetty/pom.xml index 49d5c86a5..3bfeb3b7a 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.8.0 + 0.9.0 diff --git a/dsf-fhir/dsf-fhir-server/pom.xml b/dsf-fhir/dsf-fhir-server/pom.xml index a3f937b7f..9db6c4ed1 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.8.0 + 0.9.0 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 6c31e0d7e..6dacd21ff 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 @@ -3,6 +3,7 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -44,6 +45,14 @@ public AbstractMetaTagAuthorizationRule(Class resourceType, DaoProvider daoPr resourceTypeName = resourceType.getAnnotation(ResourceDef.class).name(); } + @Override + public void afterPropertiesSet() throws Exception + { + super.afterPropertiesSet(); + + Objects.requireNonNull(parameterConverter, "parameterConverter"); + } + protected final boolean hasValidReadAccessTag(Connection connection, Resource resource) { return readAccessHelper.isValid(resource, 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 index 3fd1afcc0..756a0bb2b 100644 --- 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 @@ -1,9 +1,12 @@ package org.highmed.dsf.fhir.authorization; +import static org.highmed.dsf.bpe.ConstantsBase.*; + import java.sql.Connection; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -15,39 +18,82 @@ 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.QuestionnaireResponseItemAnswerComponent; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; +import org.hl7.fhir.r4.model.StringType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class QuestionnaireResponseAuthorizationRule - extends AbstractMetaTagAuthorizationRule + extends AbstractAuthorizationRule { private static final Logger logger = LoggerFactory.getLogger(QuestionnaireResponseAuthorizationRule.class); + private final ParameterConverter parameterConverter; + public QuestionnaireResponseAuthorizationRule(DaoProvider daoProvider, String serverBase, ReferenceResolver referenceResolver, OrganizationProvider organizationProvider, ReadAccessHelper readAccessHelper, ParameterConverter parameterConverter) { super(QuestionnaireResponse.class, daoProvider, serverBase, referenceResolver, organizationProvider, - readAccessHelper, parameterConverter); + readAccessHelper); + this.parameterConverter = parameterConverter; } @Override - protected Optional newResourceOkForCreate(Connection connection, User user, - QuestionnaireResponse newResource) + public void afterPropertiesSet() throws Exception { - List errors = new ArrayList(); + super.afterPropertiesSet(); - if (!hasValidReadAccessTag(connection, newResource)) + Objects.requireNonNull(parameterConverter, "parameterConverter"); + } + + @Override + public Optional reasonCreateAllowed(Connection connection, User user, QuestionnaireResponse newResource) + { + if (isLocalUser(user)) { - errors.add("QuestionnaireResponse is missing valid read access tag"); + Optional errors = newResourceOk(connection, user, newResource, + EnumSet.of(QuestionnaireResponseStatus.INPROGRESS)); + if (errors.isEmpty()) + { + if (!resourceExists(connection, newResource)) + { + logger.info( + "Create of QuestionnaireResponse authorized for local user '{}', QuestionnaireResponse does not exist", + user.getName()); + return Optional.of("local user, QuestionnaireResponse does not exist yet"); + } + else + { + logger.warn("Create of QuestionnaireResponse unauthorized, QuestionnaireResponse already exists"); + return Optional.empty(); + } + } + else + { + logger.warn("Create of QuestionnaireResponse unauthorized, {}", errors.get()); + return Optional.empty(); + } } + else + { + logger.warn("Create of QuestionnaireResponse unauthorized, not a local user"); + return Optional.empty(); + } + } + + private Optional newResourceOk(Connection connection, User user, QuestionnaireResponse newResource, + EnumSet allowedStatus) + { + List errors = new ArrayList(); if (newResource.hasStatus()) { - if (!QuestionnaireResponseStatus.INPROGRESS.equals(newResource.getStatus())) + if (!allowedStatus.contains(newResource.getStatus())) { - errors.add("QuestionnaireResponse.status not in-progress and version 1"); + errors.add("QuestionnaireResponse.status not one of " + allowedStatus); } } else @@ -55,51 +101,118 @@ protected Optional newResourceOkForCreate(Connection connection, User us errors.add("QuestionnaireResponse.status missing"); } + getItemAndValidate(newResource, CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID, errors); + 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) + private Optional getItemAndValidate(QuestionnaireResponse newResource, String linkId, List errors) { - List errors = new ArrayList(); + List userTaskIds = newResource.getItem().stream() + .filter(QuestionnaireResponseItemComponent::hasLinkId).filter(i -> linkId.equals(i.getLinkId())) + .collect(Collectors.toList()); - if (!hasValidReadAccessTag(connection, newResource)) + if (userTaskIds.size() != 1) { - errors.add("QuestionnaireResponse is missing valid read access tag"); + if (errors != null) + errors.add("QuestionnaireResponse.item('user-task-id') missing or more than one"); + + return Optional.empty(); } - if (newResource.hasStatus()) + QuestionnaireResponseItemComponent item = userTaskIds.get(0); + + if (!item.hasAnswer() || item.getAnswer().size() != 1) { - if (!EnumSet.of(QuestionnaireResponseStatus.COMPLETED, QuestionnaireResponseStatus.STOPPED) - .contains(newResource.getStatus())) - { - errors.add("QuestionnaireResponse.status not (completed or stopped) and version 2"); - } + if (errors != null) + errors.add("QuestionnaireResponse.item('user-task-id').answer missing or more than one"); + + return Optional.empty(); } - else + + QuestionnaireResponseItemAnswerComponent answer = item.getAnswerFirstRep(); + + if (!answer.hasValue() || !(answer.getValue() instanceof StringType)) { - errors.add("QuestionnaireResponse.status missing"); + if (errors != null) + errors.add("QuestionnaireResponse.item('user-task-id').answer.value missing or not a string"); + + return Optional.empty(); } - if (errors.isEmpty()) + StringType value = (StringType) answer.getValue(); + + if (!value.hasValue()) + { + if (errors != null) + errors.add("QuestionnaireResponse.item('user-task-id').answer.value is blank"); + return Optional.empty(); - else - return Optional.of(errors.stream().collect(Collectors.joining(", "))); + } + + return Optional.of(value.getValue()); } - @Override - protected boolean resourceExists(Connection connection, QuestionnaireResponse newResource) + private boolean resourceExists(Connection connection, QuestionnaireResponse newResource) { - // no unique criteria for QuestionnaireResponse + // TODO implement unique criteria based on UserTask.id when implemented as identifier return false; } @Override - protected boolean modificationsOk(Connection connection, QuestionnaireResponse oldResource, + public Optional reasonReadAllowed(Connection connection, User user, QuestionnaireResponse existingResource) + { + if (isLocalUser(user)) + { + logger.info("Read of QuestionnaireResponse authorized for local user '{}'", user.getName()); + return Optional.of("task.restriction.recipient resolved and local user part of referenced organization"); + } + else + { + logger.warn("Read of QuestionnaireResponse unauthorized, not a local user", user.getName()); + return Optional.empty(); + } + } + + @Override + public Optional reasonUpdateAllowed(Connection connection, User user, QuestionnaireResponse oldResource, + QuestionnaireResponse newResource) + { + if (isLocalUser(user)) + { + Optional errors = newResourceOk(connection, user, newResource, + EnumSet.of(QuestionnaireResponseStatus.COMPLETED, QuestionnaireResponseStatus.STOPPED)); + if (errors.isEmpty()) + { + if (modificationsOk(connection, oldResource, newResource)) + { + logger.info("Update of QuestionnaireResponse authorized for local user '{}', modification allowed", + user.getName()); + return Optional.of("local user; modification allowed"); + } + else + { + logger.warn("Update of QuestionnaireResponse unauthorized, modification not allowed"); + return Optional.empty(); + } + } + else + { + logger.warn("Update of QuestionnaireResponse unauthorized, {}", errors.get()); + return Optional.empty(); + } + } + else + { + logger.warn("Update of QuestionnaireResponse unauthorized, not a local user"); + return Optional.empty(); + } + } + + private boolean modificationsOk(Connection connection, QuestionnaireResponse oldResource, QuestionnaireResponse newResource) { boolean statusModificationOk = QuestionnaireResponseStatus.INPROGRESS.equals(oldResource.getStatus()) @@ -113,6 +226,78 @@ protected boolean modificationsOk(Connection connection, QuestionnaireResponse o QuestionnaireResponseStatus.COMPLETED + "|" + QuestionnaireResponseStatus.STOPPED, oldResource.getStatus(), newResource.getStatus()); - return statusModificationOk; + String oldUserTaskId = getItemAndValidate(oldResource, CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID, + new ArrayList<>()).orElse(null); + String newUserTaskId = getItemAndValidate(newResource, CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID, + new ArrayList<>()).orElse(null); + + boolean userTaskIdOk = Objects.equals(oldUserTaskId, newUserTaskId); + + if (!userTaskIdOk) + logger.warn( + "Modifications only allowed if item.answer with linkId '{}' not changed, change from '{}' to '{}' not allowed", + CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID, oldUserTaskId, newUserTaskId); + + String oldBusinessKey = getItemAndValidate(oldResource, CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY, + new ArrayList<>()).orElse(null); + String newBusinessKey = getItemAndValidate(newResource, CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY, + new ArrayList<>()).orElse(null); + + boolean businesssKeyOk = Objects.equals(oldBusinessKey, newBusinessKey); + + if (!userTaskIdOk) + logger.warn( + "Modifications only allowed if item.answer with linkId '{}' not changed, change from '{}' to '{}' not allowed", + CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_BUSINESS_KEY, oldUserTaskId, newUserTaskId); + + return statusModificationOk && userTaskIdOk && businesssKeyOk; + } + + @Override + public Optional reasonDeleteAllowed(Connection connection, User user, QuestionnaireResponse oldResource) + { + if (isLocalUser(user)) + { + logger.info("Delete of QuestionnaireResponse authorized for local user '{}'", user.getName()); + return Optional.of("local user"); + } + else + { + logger.warn("Delete of QuestionnaireResponse unauthorized, not a local user"); + return Optional.empty(); + } + } + + @Override + public final Optional reasonSearchAllowed(User user) + { + logger.info("Search of QuestionnaireResponse authorized for {} user '{}', will be filtered by user role", + user.getRole(), user.getName()); + return Optional.of("Allowed for all, filtered by user role"); + } + + @Override + public final Optional reasonHistoryAllowed(User user) + { + logger.info("History of {} authorized for {} user '{}', will be filtered by user role", user.getRole(), + user.getName()); + return Optional.of("Allowed for all, filtered by user role"); + } + + @Override + public Optional reasonPermanentDeleteAllowed(Connection connection, User user, + QuestionnaireResponse oldResource) + { + if (isLocalPermanentDeleteUser(user)) + { + logger.info("Permanent delete of QuestionnaireResponse authorized for local delete user '{}'", resourceType, + user.getName()); + return Optional.of("local delete user"); + } + else + { + logger.warn("Permanent delete of QuestionnaireResponse unauthorized, not a local delete user"); + return Optional.empty(); + } } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/history/user/HistoryUserFilterFactoryImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/history/user/HistoryUserFilterFactoryImpl.java index cff351bc5..e6f8c8258 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/history/user/HistoryUserFilterFactoryImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/history/user/HistoryUserFilterFactoryImpl.java @@ -26,6 +26,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; @@ -58,6 +60,8 @@ public HistoryUserFilterFactoryImpl() filtersByResource.put(Practitioner.class, PractitionerHistoryUserFilter::new); filtersByResource.put(PractitionerRole.class, PractitionerRoleHistoryUserFilter::new); filtersByResource.put(Provenance.class, ProvenanceHistoryUserFilter::new); + filtersByResource.put(Questionnaire.class, QuestionnaireHistoryUserFilter::new); + filtersByResource.put(QuestionnaireResponse.class, QuestionnaireResponseHistoryUserFilter::new); filtersByResource.put(ResearchStudy.class, ResearchStudyHistoryUserFilter::new); filtersByResource.put(StructureDefinition.class, StructureDefinitionHistoryUserFilter::new); filtersByResource.put(Subscription.class, SubscriptionHistoryUserFilter::new); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/history/user/QuestionnaireHistoryUserFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/history/user/QuestionnaireHistoryUserFilter.java new file mode 100644 index 000000000..e73a58c72 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/history/user/QuestionnaireHistoryUserFilter.java @@ -0,0 +1,23 @@ +package org.highmed.dsf.fhir.history.user; + +import org.highmed.dsf.fhir.authentication.User; +import org.highmed.dsf.fhir.search.parameters.user.QuestionnaireUserFilter; +import org.hl7.fhir.r4.model.Questionnaire; + +import ca.uhn.fhir.model.api.annotation.ResourceDef; + +public class QuestionnaireHistoryUserFilter extends QuestionnaireUserFilter implements HistoryUserFilter +{ + private static final String RESOURCE_TYPE = Questionnaire.class.getAnnotation(ResourceDef.class).name(); + + public QuestionnaireHistoryUserFilter(User user) + { + super(user, HistoryUserFilter.RESOURCE_TABLE, HistoryUserFilter.RESOURCE_ID_COLUMN); + } + + @Override + public String getFilterQuery() + { + return HistoryUserFilter.getFilterQuery(RESOURCE_TYPE, super.getFilterQuery()); + } +} diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/history/user/QuestionnaireResponseHistoryUserFilter.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/history/user/QuestionnaireResponseHistoryUserFilter.java new file mode 100644 index 000000000..8079d0298 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/history/user/QuestionnaireResponseHistoryUserFilter.java @@ -0,0 +1,23 @@ +package org.highmed.dsf.fhir.history.user; + +import org.highmed.dsf.fhir.authentication.User; +import org.highmed.dsf.fhir.search.parameters.user.QuestionnaireResponseUserFilter; +import org.hl7.fhir.r4.model.QuestionnaireResponse; + +import ca.uhn.fhir.model.api.annotation.ResourceDef; + +public class QuestionnaireResponseHistoryUserFilter extends QuestionnaireResponseUserFilter implements HistoryUserFilter +{ + private static final String RESOURCE_TYPE = QuestionnaireResponse.class.getAnnotation(ResourceDef.class).name(); + + public QuestionnaireResponseHistoryUserFilter(User user) + { + super(user); + } + + @Override + public String getFilterQuery() + { + return HistoryUserFilter.getFilterQuery(RESOURCE_TYPE, super.getFilterQuery()); + } +} 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 index f21f32a16..07aac517b 100644 --- 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 @@ -1,19 +1,41 @@ package org.highmed.dsf.fhir.search.parameters.user; +import java.sql.PreparedStatement; +import java.sql.SQLException; + import org.highmed.dsf.fhir.authentication.User; +import org.highmed.dsf.fhir.authentication.UserRole; -public class QuestionnaireResponseUserFilter extends AbstractMetaTagAuthorizationRoleUserFilter +public class QuestionnaireResponseUserFilter extends AbstractUserFilter { - 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); + super(user, null, null); + } + + @Override + public String getFilterQuery() + { + // read allowed for local users + if (UserRole.LOCAL.equals(user.getRole())) + return ""; + + // read not allowed for non local users + else + return "FALSE"; + } + + @Override + public int getSqlParameterCount() + { + // no parameters + return 0; } - public QuestionnaireResponseUserFilter(User user, String resourceTable, String resourceIdColumn) + @Override + public void modifyStatement(int parameterIndex, int subqueryParameterIndex, PreparedStatement statement) + throws SQLException { - super(user, resourceTable, resourceIdColumn); + // no parameters to modify } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java index e196a5ad7..4da402786 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/org/highmed/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java @@ -235,17 +235,36 @@ else if (read.hasEntity()) { audit.info("Read of resource {} denied for user '{}' ({}), not a {}", resourceTypeName + "/" + id, getCurrentUser().getName(), getCurrentUser().getSubjectDn(), resourceTypeName); - - logger.warn("Not allowing access to entity of type {}", read.getEntity().getClass().getName()); return forbidden("read"); } + else if (!read.hasEntity() && Status.NOT_MODIFIED.getStatusCode() == read.getStatus()) + { + Optional dbResource = exceptionHandler.handleSqlAndResourceDeletedException(serverBase, resourceTypeName, + () -> dao.read(parameterConverter.toUuid(resourceTypeName, id))); + Optional reasonReadAllowed = authorizationRule.reasonReadAllowed(getCurrentUser(), + dbResource.get()); + + if (reasonReadAllowed.isEmpty()) + { + audit.info("Read of resource {} denied for user '{}' ({})", dbResource.get().getIdElement().getValue(), + getCurrentUser().getName(), getCurrentUser().getSubjectDn()); + return forbidden("read"); + } + else + { + audit.info("Read of resource {} allowed for user '{}' ({}): {}", + dbResource.get().getIdElement().getValue(), getCurrentUser().getName(), + getCurrentUser().getSubjectDn(), reasonReadAllowed.get()); + return read; + } + } else { audit.info("Read of resource {} for user '{}' ({}) returned without entity, status {}", resourceTypeName + "/" + id, getCurrentUser().getName(), getCurrentUser().getSubjectDn(), read.getStatus()); - logger.warn("Returning with status {}, but no entity", read.getStatus()); + logger.info("Returning with status {}, but no entity", read.getStatus()); return read; } } @@ -289,17 +308,36 @@ else if (read.hasEntity()) audit.info("Read of resource {} denied for user '{}' ({}), not a {}", resourceTypeName + "/" + id + "/_history/" + version, getCurrentUser().getName(), getCurrentUser().getSubjectDn(), resourceTypeName); - - logger.warn("Not allowing access to entity of type {}", read.getEntity().getClass().getName()); return forbidden("read"); } + else if (!read.hasEntity() && Status.NOT_MODIFIED.getStatusCode() == read.getStatus()) + { + Optional dbResource = exceptionHandler.handleSqlAndResourceDeletedException(serverBase, resourceTypeName, + () -> dao.readVersion(parameterConverter.toUuid(resourceTypeName, id), version)); + Optional reasonReadAllowed = authorizationRule.reasonReadAllowed(getCurrentUser(), + dbResource.get()); + + if (reasonReadAllowed.isEmpty()) + { + audit.info("Read of resource {} denied for user '{}' ({})", dbResource.get().getIdElement().getValue(), + getCurrentUser().getName(), getCurrentUser().getSubjectDn()); + return forbidden("read"); + } + else + { + audit.info("Read of resource {} allowed for user '{}' ({}): {}", + dbResource.get().getIdElement().getValue(), getCurrentUser().getName(), + getCurrentUser().getSubjectDn(), reasonReadAllowed.get()); + return read; + } + } else { audit.info("Read of resource {} for user '{}' ({}) returned without entity, status {}", resourceTypeName + "/" + id + "/_history/" + version, getCurrentUser().getName(), getCurrentUser().getSubjectDn(), read.getStatus()); - logger.warn("Returning with status {}, but no entity", read.getStatus()); + logger.info("Returning with status {}, but no entity", read.getStatus()); return read; } } 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 5a3e26361..344e6e0bf 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 @@ -65,4 +65,7 @@ + + + diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.history.changelog-0.9.0.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.history.changelog-0.9.0.xml new file mode 100644 index 000000000..77038a510 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.history.changelog-0.9.0.xml @@ -0,0 +1,372 @@ + + + + + + + + + SELECT id, version, type, method, last_updated, resource + FROM ( + + SELECT activity_definition_id AS id, version, 'ActivityDefinition' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (activity_definition->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + activity_definition AS resource + FROM activity_definitions + + UNION + + SELECT activity_definition_id AS id, version + 1, 'ActivityDefinition' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM activity_definitions + WHERE deleted IS NOT NULL + + UNION + + SELECT binary_id AS id, version, 'Binary' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (binary_json->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + binary_json AS resource + FROM binaries + + UNION + + SELECT binary_id AS id, version + 1, 'Binary' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM binaries + WHERE deleted IS NOT NULL + + UNION + + SELECT bundle_id AS id, version, 'Bundle' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (bundle->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + bundle AS resource + FROM bundles + + UNION + + SELECT bundle_id AS id, version + 1, 'Bundle' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM bundles + WHERE deleted IS NOT NULL + + UNION + + SELECT code_system_id AS id, version, 'CodeSystem' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (code_system->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + code_system AS resource + FROM code_systems + + UNION + + SELECT code_system_id AS id, version + 1, 'CodeSystem' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM code_systems + WHERE deleted IS NOT NULL + + UNION + + SELECT endpoint_id AS id, version, 'Endpoint' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (endpoint->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + endpoint AS resource + FROM endpoints + + UNION + + SELECT endpoint_id AS id, version + 1, 'Endpoint' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM endpoints + WHERE deleted IS NOT NULL + + UNION + + SELECT group_id AS id, version, 'Group' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (group_json->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + group_json AS resource + FROM groups + + UNION + + SELECT group_id AS id, version + 1, 'Group' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM groups + WHERE deleted IS NOT NULL + + UNION + + SELECT healthcare_service_id AS id, version, 'HealthcareService' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (healthcare_service->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + healthcare_service AS resource + FROM healthcare_services + + UNION + + SELECT healthcare_service_id AS id, version + 1, 'HealthcareService' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM healthcare_services + WHERE deleted IS NOT NULL + + UNION + + SELECT library_id AS id, version, 'Library' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (library->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + library AS resource + FROM libraries + + UNION + + SELECT library_id AS id, version + 1, 'Library' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM libraries + WHERE deleted IS NOT NULL + + UNION + + SELECT location_id AS id, version, 'Location' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (location->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + location AS resource + FROM locations + + UNION + + SELECT location_id AS id, version + 1, 'Location' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM locations + WHERE deleted IS NOT NULL + + UNION + + SELECT measure_id AS id, version, 'Measure' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (measure->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + measure AS resource + FROM measures + + UNION + + SELECT measure_id AS id, version + 1, 'Measure' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM measures + WHERE deleted IS NOT NULL + + UNION + + SELECT measure_report_id AS id, version, 'MeasureReport' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (measure_report->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + measure_report AS resource + FROM measure_reports + + UNION + + SELECT measure_report_id AS id, version + 1, 'MeasureReport' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM measure_reports + WHERE deleted IS NOT NULL + + UNION + + SELECT naming_system_id AS id, version, 'NamingSystem' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (naming_system->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + naming_system AS resource + FROM naming_systems + + UNION + + SELECT naming_system_id AS id, version + 1, 'NamingSystem' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM naming_systems + WHERE deleted IS NOT NULL + + UNION + + SELECT organization_id AS id, version, 'Organization' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (organization->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + organization AS resource + FROM organizations + + UNION + + SELECT organization_id AS id, version + 1, 'Organization' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM organizations + WHERE deleted IS NOT NULL + + UNION + + SELECT organization_affiliation_id AS id, version, 'OrganizationAffiliation' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (organization_affiliation->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + organization_affiliation AS resource + FROM organization_affiliations + + UNION + + SELECT organization_affiliation_id AS id, version + 1, 'OrganizationAffiliation' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM organization_affiliations + WHERE deleted IS NOT NULL + + UNION + + SELECT questionnaire_id AS id, version, 'Questionnaire' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (questionnaire->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + questionnaire AS resource + FROM questionnaires + + UNION + + SELECT questionnaire_id AS id, version + 1, 'Questionnaire' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM questionnaires + WHERE deleted IS NOT NULL + + UNION + + SELECT questionnaire_response_id AS id, version, 'QuestionnaireResponse' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (questionnaire_response->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + questionnaire_response AS resource + FROM questionnaire_responses + + UNION + + SELECT questionnaire_response_id AS id, version + 1, 'QuestionnaireResponse' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM questionnaire_responses + WHERE deleted IS NOT NULL + + UNION + + SELECT patient_id AS id, version, 'Patient' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (patient->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + patient AS resource + FROM patients + + UNION + + SELECT patient_id AS id, version + 1, 'Patient' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM patients + WHERE deleted IS NOT NULL + + UNION + + SELECT practitioner_role_id AS id, version, 'PractitionerRole' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (practitioner_role->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + practitioner_role AS resource + FROM practitioner_roles + + UNION + + SELECT practitioner_role_id AS id, version + 1, 'PractitionerRole' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM practitioner_roles + WHERE deleted IS NOT NULL + + UNION + + SELECT practitioner_id AS id, version, 'Practitioner' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (practitioner->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + practitioner AS resource + FROM practitioners + + UNION + + SELECT practitioner_id AS id, version + 1, 'Practitioner' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM practitioners + WHERE deleted IS NOT NULL + + UNION + + SELECT provenance_id AS id, version, 'Provenance' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (provenance->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + provenance AS resource + FROM provenances + + UNION + + SELECT provenance_id AS id, version + 1, 'Provenance' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM provenances + WHERE deleted IS NOT NULL + + UNION + + SELECT research_study_id AS id, version, 'ResearchStudy' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (research_study->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + research_study AS resource + FROM research_studies + + UNION + + SELECT research_study_id AS id, version + 1, 'ResearchStudy' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM research_studies + WHERE deleted IS NOT NULL + + UNION + + SELECT structure_definition_id AS id, version, 'StructureDefinition' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (structure_definition->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + structure_definition AS resource + FROM structure_definitions + + UNION + + SELECT structure_definition_id AS id, version + 1, 'StructureDefinition' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM structure_definitions + WHERE deleted IS NOT NULL + + UNION + + SELECT subscription_id AS id, version, 'Subscription' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (subscription->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + subscription AS resource + FROM subscriptions + + UNION + + SELECT subscription_id AS id, version + 1, 'Subscription' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM subscriptions + WHERE deleted IS NOT NULL + + UNION + + SELECT task_id AS id, version, 'Task' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (task->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + task AS resource + FROM tasks + + UNION + + SELECT task_id AS id, version + 1, 'Task' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM tasks + WHERE deleted IS NOT NULL + + UNION + + SELECT value_set_id AS id, version, 'ValueSet' AS type, + CASE WHEN version = 1 THEN 'POST' ELSE 'PUT' END AS method, + (value_set->'meta'->>'lastUpdated')::TIMESTAMP AS last_updated, + value_set AS resource + FROM value_sets + + UNION + + SELECT value_set_id AS id, version + 1, 'ValueSet' AS type, 'DELETE' AS method, deleted AS last_updated, NULL AS resource + FROM value_sets + WHERE deleted IS NOT NULL + + ) AS history + ORDER BY last_updated, id, version + + + + ALTER TABLE history OWNER TO ${db.liquibase_user}; + GRANT ALL ON TABLE history TO ${db.liquibase_user}; + GRANT SELECT ON TABLE history TO ${db.server_users_group}; + + + \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.questionnaire_responses.changelog-0.9.0.xml b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.questionnaire_responses.changelog-0.9.0.xml new file mode 100644 index 000000000..ef112f272 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/db/db.questionnaire_responses.changelog-0.9.0.xml @@ -0,0 +1,17 @@ + + + + + + + + + DROP TRIGGER questionnaire_responses_insert ON questionnaire_responses; + DROP TRIGGER questionnaire_responses_update ON questionnaire_responses; + DROP FUNCTION on_questionnaire_responses_update; + DROP FUNCTION on_questionnaire_responses_insert; + + + 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 index 8deb6d22e..41dcb0b29 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.css +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.css @@ -20,6 +20,14 @@ fieldset#qr-form-fieldset { background-color: #f2f2f2; } +.row-display { + padding-top: 15px; +} + +.p-display { + margin: 0; +} + .error { background-color: #ffa590; } 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 index 2b1a7b0ca..b6e5e388f 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.js @@ -18,12 +18,15 @@ 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 (item.hasOwnProperty('answer')) { + const id = item.linkId - if (id !== "business-key" && id !== "user-task-id") { - answer[answerType] = readAndValidateValue(id, answerType, errors) + if (id !== "business-key" && id !== "user-task-id") { + const answer = item.answer[0] + const answerType = Object.keys(answer)[0] + + answer[answerType] = readAndValidateValue(id, answerType, errors) + } } }) } @@ -31,7 +34,7 @@ function readAnswersFromForm(questionnaireResponse, errors) { function readAndValidateValue(id, answerType, errors) { const value = document.getElementById(id).value - const rowElement = document.getElementById(id + "-row"); + const rowElement = document.getElementById(id + "-answer-row"); const errorListElement = document.getElementById(id + "-error"); errorListElement.replaceChildren() @@ -196,7 +199,13 @@ function updateQuestionnaireResponse(questionnaireResponse) { disableSpinner() window.scrollTo(0, 0); location.reload(); - } else { + } else if (response.status >= 400 && response.status < 600) { + response.text().then((responseText) => { + document.open(); + document.write(responseText); + document.close(); + }); + } else { const status = response.status const statusText = response.statusText === null ? " - " + response.statusText : "" 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 index e962355d2..71a09b1a3 100755 --- 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 @@ -1,15 +1,13 @@ 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; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; public class QuestionnaireResponseDaoTest extends AbstractResourceDaoTest - implements ReadAccessDaoTest { public QuestionnaireResponseDaoTest() { @@ -42,200 +40,4 @@ 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/QuestionnaireResponseIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/org/highmed/dsf/fhir/integration/QuestionnaireResponseIntegrationTest.java index 2a68d5574..f82a44f03 100644 --- 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 @@ -3,17 +3,22 @@ import static junit.framework.TestCase.assertEquals; import static org.junit.Assert.assertFalse; 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.ZoneOffset; import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response.Status; +import org.highmed.dsf.bpe.ConstantsBase; import org.highmed.dsf.fhir.authentication.OrganizationProvider; import org.highmed.dsf.fhir.dao.QuestionnaireDao; import org.highmed.dsf.fhir.dao.QuestionnaireResponseDao; @@ -25,14 +30,14 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Type; 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; @@ -66,6 +71,22 @@ public void testCreateNotAllowedByLocalUser() throws Exception } } + @Test(expected = WebApplicationException.class) + public void testCreateNotAllowedByRemoteUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + + try + { + getExternalWebserviceClient().create(questionnaireResponse); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + @Test public void testUpdateAllowedByLocalUser() throws Exception { @@ -103,6 +124,52 @@ public void testUpdateNotAllowedByLocalUser() throws Exception } } + @Test(expected = WebApplicationException.class) + public void testUpdateNotAllowedByLocalUserNowUserTaskId() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + created.setStatus(QuestionnaireResponseStatus.STOPPED); + created.getItem().clear(); + + try + { + getWebserviceClient().update(created); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + + @Test(expected = WebApplicationException.class) + public void testUpdateNotAllowedByLocalUserChangedUserTaskId() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + created.setStatus(QuestionnaireResponseStatus.STOPPED); + created.getItem().clear(); + addItem(created, ConstantsBase.CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID, "UserTask ID", + new StringType(UUID.randomUUID().toString())); + + 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 { @@ -124,6 +191,26 @@ public void testSecondUpdateNotAllowedByLocalUser() throws Exception } } + @Test(expected = WebApplicationException.class) + public void testUpdateNotAllowedByRemoteUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + created.setStatus(QuestionnaireResponseStatus.COMPLETED); + try + { + getExternalWebserviceClient().update(created); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + @Test public void testSearchByDate() throws Exception { @@ -150,13 +237,17 @@ public void testSearchByDate() throws Exception @Test public void testSearchByIdentifier() throws Exception { + final String value = UUID.randomUUID().toString(); + final String system = "http://foo/fhir/sid/Test"; + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + questionnaireResponse.getIdentifier().setSystem(system).setValue(value); QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() .getBean(QuestionnaireResponseDao.class); questionnaireResponseDao.create(questionnaireResponse); Bundle searchBundle = getWebserviceClient().search(QuestionnaireResponse.class, - Map.of("identifier", Collections.singletonList(IDENTIFIER_SYSTEM + "|" + IDENTIFIER_VALUE))); + Map.of("identifier", Collections.singletonList(system + "|" + value))); assertNotNull(searchBundle.getEntry()); assertEquals(1, searchBundle.getEntry().size()); @@ -164,11 +255,28 @@ public void testSearchByIdentifier() throws Exception assertNotNull(searchBundle.getEntry().get(0).getResource()); assertTrue(searchBundle.getEntry().get(0).getResource() instanceof QuestionnaireResponse); - QuestionnaireResponse searchQuestionnaireResponse = (QuestionnaireResponse) searchBundle.getEntry().get(0) + QuestionnaireResponse foundQuestionnaireResponse = (QuestionnaireResponse) searchBundle.getEntry().get(0) .getResource(); - assertTrue(searchQuestionnaireResponse.hasIdentifier()); - assertEquals(IDENTIFIER_SYSTEM, searchQuestionnaireResponse.getIdentifier().getSystem()); - assertEquals(IDENTIFIER_VALUE, searchQuestionnaireResponse.getIdentifier().getValue()); + assertTrue(foundQuestionnaireResponse.hasIdentifier()); + } + + @Test + public void testSearchByIdentifierRemoteUser() throws Exception + { + final String value = UUID.randomUUID().toString(); + final String system = "http://foo/fhir/sid/Test"; + + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + questionnaireResponse.getIdentifier().setSystem(system).setValue(value); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + questionnaireResponseDao.create(questionnaireResponse); + + Bundle searchBundle = getExternalWebserviceClient().search(QuestionnaireResponse.class, + Map.of("identifier", Collections.singletonList(system + "|" + value))); + + assertNotNull(searchBundle.getEntry()); + assertEquals(0, searchBundle.getEntry().size()); } @Test @@ -392,10 +500,6 @@ private QuestionnaireResponse createQuestionnaireResponse() 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); @@ -406,9 +510,273 @@ private QuestionnaireResponse createQuestionnaireResponse() + organizationProvider.getLocalOrganization().get().getIdElement().getIdPart(); questionnaireResponse.setSubject(new Reference(organizationReference)); - questionnaireResponse.addItem().setLinkId("foo").setText("Approve?").addAnswer() - .setValue(new BooleanType(true)); + addItem(questionnaireResponse, ConstantsBase.CODESYSTEM_HIGHMED_BPMN_USER_TASK_VALUE_USER_TASK_ID, + "UserTask ID", new StringType(UUID.randomUUID().toString())); return questionnaireResponse; } + + private void addItem(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); + } + + @Test + public void testDeleteAllowedByLocalUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + getWebserviceClient().delete(QuestionnaireResponse.class, created.getIdElement().getIdPart()); + } + + @Test(expected = WebApplicationException.class) + public void testDeleteNotAllowedByRemoteUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + try + { + getExternalWebserviceClient().delete(QuestionnaireResponse.class, created.getIdElement().getIdPart()); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + + @Test + public void testReadAllowedByLocalUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + QuestionnaireResponse read = getWebserviceClient().read(QuestionnaireResponse.class, + created.getIdElement().getIdPart()); + + assertNotNull(read); + assertNotNull(read.getIdElement().getIdPart()); + assertEquals(created.getIdElement().getIdPart(), read.getIdElement().getIdPart()); + assertNotNull(read.getIdElement().getVersionIdPart()); + assertEquals("1", read.getIdElement().getVersionIdPart()); + } + + @Test(expected = WebApplicationException.class) + public void testReadNotAllowedByRemoteUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + try + { + getExternalWebserviceClient().read(QuestionnaireResponse.class, created.getIdElement().getIdPart()); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + + @Test(expected = WebApplicationException.class) + public void testReadNotAllowedByRemoteUserWithVersion() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + try + { + getExternalWebserviceClient().read(QuestionnaireResponse.class, created.getIdElement().getIdPart(), + created.getIdElement().getVersionIdPart()); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + + @Test + public void testNotModifiedCheckAllowedByLocalUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + QuestionnaireResponse read = getWebserviceClient().read(created); + assertNotNull(read); + assertTrue(created == read); + } + + @Test(expected = WebApplicationException.class) + public void testNotModifiedCheckNotAllowedByRemoteUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + + try + { + getExternalWebserviceClient().read(created); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + + @Test + public void testNotModifiedCheckAllowedByLocalUserWithModification() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + QuestionnaireResponse updated = questionnaireResponseDao.update(created); + assertEquals("2", updated.getIdElement().getVersionIdPart()); + + QuestionnaireResponse read = getWebserviceClient().read(created); + assertNotNull(read); + assertTrue(created != read); + + assertEquals("2", read.getIdElement().getVersionIdPart()); + } + + @Test(expected = WebApplicationException.class) + public void testNotModifiedCheckNotAllowedByRemoteUserWithModification() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + QuestionnaireResponse updated = questionnaireResponseDao.update(created); + assertEquals("2", updated.getIdElement().getVersionIdPart()); + + try + { + getExternalWebserviceClient().read(created); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } + + @Test + public void testHistory() throws Exception + { + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(createQuestionnaireResponse()); + + Bundle historyBundle = getWebserviceClient().history(QuestionnaireResponse.class, + created.getIdElement().getIdPart()); + + assertNotNull(historyBundle.getEntry()); + assertEquals(1, historyBundle.getEntry().size()); + assertNotNull(historyBundle.getEntry().get(0)); + assertNotNull(historyBundle.getEntry().get(0).getResource()); + assertTrue(historyBundle.getEntry().get(0).getResource() instanceof QuestionnaireResponse); + + Bundle historyBundle2 = getWebserviceClient().history(QuestionnaireResponse.class); + + assertNotNull(historyBundle2.getEntry()); + assertEquals(1, historyBundle2.getEntry().size()); + assertNotNull(historyBundle2.getEntry().get(0)); + assertNotNull(historyBundle2.getEntry().get(0).getResource()); + assertTrue(historyBundle2.getEntry().get(0).getResource() instanceof QuestionnaireResponse); + + Bundle historyBundle3 = getWebserviceClient().history(1, Integer.MAX_VALUE); + + assertNotNull(historyBundle3.getEntry()); + + List qrFromBundle = historyBundle3.getEntry().stream() + .filter(e -> e.hasResource() && e.getResource() instanceof QuestionnaireResponse) + .map(e -> (QuestionnaireResponse) e.getResource()).collect(Collectors.toList()); + + assertEquals(1, qrFromBundle.size()); + assertNotNull(qrFromBundle.get(0)); + } + + @Test + public void testHistoryRemoteUser() throws Exception + { + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(createQuestionnaireResponse()); + + Bundle historyBundle = getExternalWebserviceClient().history(QuestionnaireResponse.class, + created.getIdElement().getIdPart()); + + assertNotNull(historyBundle.getEntry()); + assertEquals(0, historyBundle.getEntry().size()); + + Bundle historyBundle2 = getExternalWebserviceClient().history(QuestionnaireResponse.class); + + assertNotNull(historyBundle2.getEntry()); + assertEquals(0, historyBundle2.getEntry().size()); + + Bundle historyBundle3 = getExternalWebserviceClient().history(1, Integer.MAX_VALUE); + + assertNotNull(historyBundle3.getEntry()); + assertNotSame(0, historyBundle3.getEntry().size()); + + List qrFromBundle = historyBundle3.getEntry().stream() + .filter(e -> e.hasResource() && e.getResource() instanceof QuestionnaireResponse) + .map(e -> (QuestionnaireResponse) e.getResource()).collect(Collectors.toList()); + + assertEquals(0, qrFromBundle.size()); + } + + @Test + public void testDeletePermanentlyAllowedByLocalUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + questionnaireResponseDao.delete(UUID.fromString(created.getIdElement().getIdPart())); + + getWebserviceClient().deletePermanently(QuestionnaireResponse.class, created.getIdElement().getIdPart()); + } + + @Test(expected = WebApplicationException.class) + public void testDeletePermanentlyNotAllowedByRemoteUser() throws Exception + { + QuestionnaireResponse questionnaireResponse = createQuestionnaireResponse(); + QuestionnaireResponseDao questionnaireResponseDao = getSpringWebApplicationContext() + .getBean(QuestionnaireResponseDao.class); + QuestionnaireResponse created = questionnaireResponseDao.create(questionnaireResponse); + questionnaireResponseDao.delete(UUID.fromString(created.getIdElement().getIdPart())); + + try + { + getExternalWebserviceClient().deletePermanently(QuestionnaireResponse.class, + created.getIdElement().getIdPart()); + } + catch (WebApplicationException e) + { + assertEquals(Status.FORBIDDEN.getStatusCode(), e.getResponse().getStatus()); + throw e; + } + } } diff --git a/dsf-fhir/dsf-fhir-validation/pom.xml b/dsf-fhir/dsf-fhir-validation/pom.xml index 829dcb62f..19002618d 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.8.0 + 0.9.0 @@ -93,7 +93,7 @@ - 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.9.0.xml similarity index 93% rename from dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-0.8.0.xml rename to dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-0.9.0.xml index 371401aef..9f57e353f 100644 --- 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.9.0.xml @@ -6,11 +6,11 @@ - + - + @@ -43,113 +43,113 @@ - + - + + value="(type = 'display') or (type = 'string') or (type = 'text') or (type = 'integer') or (type = 'decimal') or (type = 'boolean') or (type = 'date') or (type = 'time') or (type = 'dateTime') or (type = 'reference') or (type = 'url')"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -170,6 +170,7 @@ + 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.9.0.xml.post similarity index 85% rename from dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-0.8.0.xml.post rename to dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-0.9.0.xml.post index ae0a8e800..6d52c310c 100644 --- 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.9.0.xml.post @@ -1 +1 @@ -url=http://highmed.org/fhir/StructureDefinition/questionnaire&version=0.8.0 \ No newline at end of file +url=http://highmed.org/fhir/StructureDefinition/questionnaire&version=0.9.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.9.0.xml similarity index 97% rename from dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-response-0.8.0.xml rename to dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-response-0.9.0.xml index ce16df829..fb2a23c02 100644 --- 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.9.0.xml @@ -6,11 +6,11 @@ - + - + @@ -50,90 +50,90 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + @@ -146,12 +146,13 @@ - + + 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.9.0.xml.post similarity index 76% rename from dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-response-0.8.0.xml.post rename to dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/StructureDefinition/highmed-questionnaire-response-0.9.0.xml.post index 8d2cefaaa..daecb75f0 100644 --- 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.9.0.xml.post @@ -1 +1 @@ -url=http://highmed.org/fhir/StructureDefinition/questionnaire-response&version=0.8.0 \ No newline at end of file +url=http://highmed.org/fhir/StructureDefinition/questionnaire-response&version=0.9.0 \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/resources.delete b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/resources.delete index 7bec00659..18613cd5e 100644 --- a/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/resources.delete +++ b/dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/resources.delete @@ -1,4 +1,6 @@ ValueSet?url=http://hl7.org/fhir/ValueSet/mimetypes&version=4.0.1&date=eq2019-11-01T09:29:23 ValueSet?url=http://hl7.org/fhir/ValueSet/mimetypes&version=4.0.1&date=eq2021-02-12 CodeSystem?url=urn:ietf:bcp:13&version=4.0.1&date=eq2020-05-29 -CodeSystem?url=urn:ietf:bcp:13&version=4.0.1&date=eq2021-02-12 \ No newline at end of file +CodeSystem?url=urn:ietf:bcp:13&version=4.0.1&date=eq2021-02-12 +StructureDefinition?url=http://highmed.org/fhir/StructureDefinition/questionnaire&version=0.8.0&date=eq2022-10-11 +StructureDefinition?url=http://highmed.org/fhir/StructureDefinition/questionnaire-response&version=0.8.0&date=eq2022-10-11 \ 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 index 92c7bf2b6..555d98b04 100644 --- 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 @@ -25,7 +25,7 @@ public class QuestionnaireProfileTest @ClassRule public static final ValidationSupportRule validationRule = new ValidationSupportRule( - Arrays.asList("highmed-questionnaire-0.8.0.xml"), Collections.emptyList(), Collections.emptyList()); + Arrays.asList("highmed-questionnaire-0.9.0.xml"), Collections.emptyList(), Collections.emptyList()); private ResourceValidator resourceValidator = new ResourceValidatorImpl(validationRule.getFhirContext(), validationRule.getValidationSupport()); @@ -90,6 +90,12 @@ public void testQuestionnaireValidTypeReference() testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.REFERENCE); } + @Test + public void testQuestionnaireValidTypeDisplay() + { + testQuestionnaireValidType(Questionnaire.QuestionnaireItemType.DISPLAY); + } + private void testQuestionnaireValidType(Questionnaire.QuestionnaireItemType type) { Questionnaire res = createQuestionnaire(type); @@ -108,12 +114,6 @@ public void testQuestionnaireInvalidTypeGroup() testQuestionnaireInvalidType(Questionnaire.QuestionnaireItemType.GROUP); } - @Test - public void testQuestionnaireInvalidTypeDisplay() - { - testQuestionnaireInvalidType(Questionnaire.QuestionnaireItemType.DISPLAY); - } - @Test public void testQuestionnaireInvalidTypeQuestion() { 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 index a66b0bbe4..6315db17f 100644 --- 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 @@ -36,7 +36,7 @@ public class QuestionnaireResponseProfileTest @ClassRule public static final ValidationSupportRule validationRule = new ValidationSupportRule( - Arrays.asList("highmed-questionnaire-response-0.8.0.xml"), Collections.emptyList(), + Arrays.asList("highmed-questionnaire-response-0.9.0.xml"), Collections.emptyList(), Collections.emptyList()); private ResourceValidator resourceValidator = new ResourceValidatorImpl(validationRule.getFhirContext(), @@ -96,10 +96,26 @@ public void testQuestionnaireResponseValidTypeReference() testQuestionnaireResponseValidType(new Reference("Observation/foo")); } + @Test + public void testQuestionnaireResponseValidTypeReferenceWithBusinessKey() + { + testQuestionnaireResponseValidTypeWithBusinessKey(new Reference("Observation/foo")); + } + + private void testQuestionnaireResponseValidTypeWithBusinessKey(Type type) + { + QuestionnaireResponse res = createQuestionnaireResponseWithBusinessKey(type); + testQuestionnaireResponse(res); + } + private void testQuestionnaireResponseValidType(Type type) { QuestionnaireResponse res = createQuestionnaireResponse(type); + testQuestionnaireResponse(res); + } + private void testQuestionnaireResponse(QuestionnaireResponse res) + { ValidationResult result = resourceValidator.validate(res); result.getMessages().stream().map(m -> m.getLocationString() + " " + m.getLocationLine() + ":" + m.getLocationCol() + " - " + m.getSeverity() + ": " + m.getMessage()).forEach(logger::info); @@ -183,17 +199,24 @@ public void testQuestionnaireResponseInvalidCompletedWithAuthorReferenceAndAutho .filter(m -> m.getMessage().startsWith("author-if-completed")).count()); } + private QuestionnaireResponse createQuestionnaireResponseWithBusinessKey(Type type) + { + QuestionnaireResponse res = createQuestionnaireResponse(type); + res.addItem().setLinkId("business-key").setText("The business-key of the process execution").addAnswer() + .setValue(new StringType(UUID.randomUUID().toString())); + + return res; + } + 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-display").setText("valid-display"); 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 e6304ed77..91805ca49 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.8.0 + 0.9.0 diff --git a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/org/highmed/fhir/client/BasicFhirWebserviceClient.java b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/org/highmed/fhir/client/BasicFhirWebserviceClient.java index b95450b47..4ccd57091 100644 --- a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/org/highmed/fhir/client/BasicFhirWebserviceClient.java +++ b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/org/highmed/fhir/client/BasicFhirWebserviceClient.java @@ -97,12 +97,12 @@ default Bundle history(int page, int count) default Bundle history(Class resourceType) { - return history(null, null); + return history(resourceType, null); } default Bundle history(Class resourceType, int page, int count) { - return history(null, null, page, count); + return history(resourceType, null, page, count); } default Bundle history(Class resourceType, String id) diff --git a/dsf-fhir/dsf-fhir-websocket-client/pom.xml b/dsf-fhir/dsf-fhir-websocket-client/pom.xml index e3bafc629..0e9b598e1 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.8.0 + 0.9.0 diff --git a/dsf-fhir/pom.xml b/dsf-fhir/pom.xml index 7d0c7171e..1e061701d 100755 --- a/dsf-fhir/pom.xml +++ b/dsf-fhir/pom.xml @@ -8,7 +8,7 @@ org.highmed.dsf dsf-pom - 0.8.0 + 0.9.0 diff --git a/dsf-mpi/dsf-mpi-client-pdq/pom.xml b/dsf-mpi/dsf-mpi-client-pdq/pom.xml index 56005968a..aafc79430 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.8.0 + 0.9.0 diff --git a/dsf-mpi/dsf-mpi-client-stub/pom.xml b/dsf-mpi/dsf-mpi-client-stub/pom.xml index 4001fed2d..1e6100592 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.8.0 + 0.9.0 diff --git a/dsf-mpi/dsf-mpi-client/pom.xml b/dsf-mpi/dsf-mpi-client/pom.xml index f2f283a83..9b09c92b1 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.8.0 + 0.9.0 diff --git a/dsf-mpi/pom.xml b/dsf-mpi/pom.xml index 8ca3691f9..a24a23851 100644 --- a/dsf-mpi/pom.xml +++ b/dsf-mpi/pom.xml @@ -10,7 +10,7 @@ dsf-pom org.highmed.dsf - 0.8.0 + 0.9.0 diff --git a/dsf-openehr/dsf-openehr-client-impl/pom.xml b/dsf-openehr/dsf-openehr-client-impl/pom.xml index 568b21393..2412446ba 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.8.0 + 0.9.0 diff --git a/dsf-openehr/dsf-openehr-client-stub/pom.xml b/dsf-openehr/dsf-openehr-client-stub/pom.xml index a818a4794..71e607aba 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.8.0 + 0.9.0 diff --git a/dsf-openehr/dsf-openehr-client/pom.xml b/dsf-openehr/dsf-openehr-client/pom.xml index ec3f0f171..8d357c1e5 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.8.0 + 0.9.0 diff --git a/dsf-openehr/dsf-openehr-model/pom.xml b/dsf-openehr/dsf-openehr-model/pom.xml index dd70be2bc..31ebfee04 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.8.0 + 0.9.0 diff --git a/dsf-openehr/pom.xml b/dsf-openehr/pom.xml index fcc551984..2eec4d13d 100755 --- a/dsf-openehr/pom.xml +++ b/dsf-openehr/pom.xml @@ -10,7 +10,7 @@ dsf-pom org.highmed.dsf - 0.8.0 + 0.9.0 diff --git a/dsf-pseudonymization/dsf-pseudonymization-base/pom.xml b/dsf-pseudonymization/dsf-pseudonymization-base/pom.xml index c442a48da..362b5d0a4 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.8.0 + 0.9.0 diff --git a/dsf-pseudonymization/dsf-pseudonymization-client-stub/pom.xml b/dsf-pseudonymization/dsf-pseudonymization-client-stub/pom.xml index 1534d6423..0310e04e9 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.8.0 + 0.9.0 diff --git a/dsf-pseudonymization/dsf-pseudonymization-client/pom.xml b/dsf-pseudonymization/dsf-pseudonymization-client/pom.xml index 12e3094ba..06e58cbbf 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.8.0 + 0.9.0 diff --git a/dsf-pseudonymization/dsf-pseudonymization-medic/pom.xml b/dsf-pseudonymization/dsf-pseudonymization-medic/pom.xml index 91654d4c5..9b5c1e041 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.8.0 + 0.9.0 diff --git a/dsf-pseudonymization/dsf-pseudonymization-ttp/pom.xml b/dsf-pseudonymization/dsf-pseudonymization-ttp/pom.xml index 8c6f8000c..01a0141e3 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.8.0 + 0.9.0 diff --git a/dsf-pseudonymization/pom.xml b/dsf-pseudonymization/pom.xml index c416cdc81..46ecd0075 100644 --- a/dsf-pseudonymization/pom.xml +++ b/dsf-pseudonymization/pom.xml @@ -8,7 +8,7 @@ org.highmed.dsf dsf-pom - 0.8.0 + 0.9.0 diff --git a/dsf-tools/dsf-tools-build-info-reader/pom.xml b/dsf-tools/dsf-tools-build-info-reader/pom.xml index 5bf0c9ce4..3e0a75b84 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.8.0 + 0.9.0 diff --git a/dsf-tools/dsf-tools-bundle-generator/pom.xml b/dsf-tools/dsf-tools-bundle-generator/pom.xml index 841048370..bde2f09ce 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.8.0 + 0.9.0 diff --git a/dsf-tools/dsf-tools-db-migration/pom.xml b/dsf-tools/dsf-tools-db-migration/pom.xml index 75776c53e..f1a208e7c 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.8.0 + 0.9.0 diff --git a/dsf-tools/dsf-tools-docker-secrets-reader/pom.xml b/dsf-tools/dsf-tools-docker-secrets-reader/pom.xml index 6a2365eee..a12295586 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.8.0 + 0.9.0 diff --git a/dsf-tools/dsf-tools-documentation-generator/pom.xml b/dsf-tools/dsf-tools-documentation-generator/pom.xml index 475c590d2..a950151f8 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.8.0 + 0.9.0 diff --git a/dsf-tools/dsf-tools-proxy-test/pom.xml b/dsf-tools/dsf-tools-proxy-test/pom.xml index 7a0988305..4bdfbdfd7 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.8.0 + 0.9.0 diff --git a/dsf-tools/dsf-tools-test-data-generator/pom.xml b/dsf-tools/dsf-tools-test-data-generator/pom.xml index c9390cb00..851ea7851 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.8.0 + 0.9.0 diff --git a/dsf-tools/pom.xml b/dsf-tools/pom.xml index cb029123f..d8ed10d06 100755 --- a/dsf-tools/pom.xml +++ b/dsf-tools/pom.xml @@ -8,7 +8,7 @@ org.highmed.dsf dsf-pom - 0.8.0 + 0.9.0 diff --git a/pom.xml b/pom.xml index 3527b7003..3d6d89de7 100755 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.highmed.dsf dsf-pom - 0.8.0 + 0.9.0 pom @@ -133,6 +133,12 @@ liquibase-core 4.16.1 + + + org.yaml + snakeyaml + 1.32 + org.postgresql postgresql @@ -211,7 +217,7 @@ com.fasterxml.jackson.core jackson-databind - 2.13.4 + 2.14.0-rc1 com.fasterxml.jackson.core @@ -331,12 +337,17 @@ ${hapi.fhir.version} - + org.apache.commons commons-compress 1.21 + + org.apache.commons + commons-text + 1.10.0 + org.apache.httpcomponents httpclient