" + RESOURCE_VERSION_PATTERN_STRING + "\\.\\d+\\.\\d+)";
+ Pattern PLUGIN_VERSION_PATTERN = Pattern.compile(PLUGIN_VERSION_PATTERN_STRING);
+
+ /**
+ * @return process plugin name, same as jar name excluding suffix -<version>.jar
+ */
+ String getName();
+
+ /**
+ * @return version of the process plugin, must match {@value #PLUGIN_VERSION_PATTERN_STRING}
+ */
+ String getVersion();
+
+ /**
+ * Placeholder #{version} in FHIR and BPMN files will be replaced with the returned value.
+ *
+ * @return version of FHIR and BPMN resources, must match {@value #RESOURCE_VERSION_PATTERN_STRING}
+ */
+ default String getResourceVersion()
+ {
+ if (getVersion() == null)
+ return null;
+
+ Matcher matcher = PLUGIN_VERSION_PATTERN.matcher(getVersion());
+ if (!matcher.matches())
+ return null;
+ else
+ return matcher.group("resourceVersion");
+ }
+
+ /**
+ * @return the release date of the process plugin
+ */
+ LocalDate getReleaseDate();
+
+ /**
+ * Placeholder #{date} in FHIR and BPMN files will be replaced with the returned value.
+ *
+ * @return the release date of FHIR resources and BPMN files
+ */
+ default LocalDate getResourceReleaseDate()
+ {
+ return getReleaseDate();
+ }
+
+ /**
+ * Return List.of("foo.bpmn"); for a foo.bpmn file located in the root folder of the process plugin
+ * jar. The returned files will be read via {@link ClassLoader#getResourceAsStream(String)}.
+ *
+ * Occurrences of #{version} will be replaced with the value of
+ * {@link #getResourceVersion()}
+ * Occurrences of #{date} will be replaced with the value of
+ * {@link #getResourceReleaseDate()}
+ * Occurrences of #{organization} will be replaced with the local organization DSF identifier
+ * value, or "null" if no local organization can be found in the allow list
+ * Other placeholders of the form #{property.name} will be replaced with values from equivalent
+ * environment variable, e.g. PROPERTY_NAME
+ *
+ * @return *.bpmn files inside the process plugin jar, paths relative to root folder of process plugin
+ * @see ClassLoader#getResourceAsStream(String)
+ */
+ List getProcessModels();
+
+ /**
+ * Return Map.of("testcom_process", List.of("foo.xml")); for a foo.xml file located in the root
+ * folder of the process plugin jar needed for a process called testcom_process. The returned files will be read via
+ * {@link ClassLoader#getResourceAsStream(String)}.
+ *
+ * Supported metadata resource types are ActivityDefinition, CodeSystem, Library, Measure, NamingSystem,
+ * Questionnaire, StructureDefinition, Task and ValueSet.
+ *
+ * Occurrences of #{version} will be replaced with the value of
+ * {@link #getResourceVersion()}
+ * Occurrences of #{date} will be replaced with the value of
+ * {@link #getResourceReleaseDate()}
+ * Occurrences of #{organization} will be replaced with the local organization DSF identifier
+ * value, or "null" if no local organization can be found in the allow list
+ * Other placeholders of the form #{property.name} will be replaced with values from equivalent
+ * environment variable, e.g. PROPERTY_NAME
+ *
+ * @return *.xml or *.json files inside the process plugin jar per process, paths relative to root folder of process
+ * plugin
+ * @see ClassLoader#getResourceAsStream(String)
+ */
+ Map> getFhirResourcesByProcessId();
+
+ /**
+ * List of {@link Configuration} annotated spring configuration classes.
+ *
+ * All services defined in {@link ProcessPluginApi} and {@link ProcessPluginApi} itself can be {@link Autowired}
+ * in {@link Configuration} classes.
+ *
+ * All implementations used for BPMN service tasks, message send tasks and throw events as well as task- and user
+ * task listeners need to be declared as spring {@link Bean}s with {@link Scope} "prototype".
+ * Other classes not directly used within BPMN activities should be declared with the default singleton scope.
+ *
+ * Configuration classes that defined private fields annotated with {@link Value} defining property placeholders,
+ * can be configured via environment variables. A field private boolean specialFunction;
+ * annotated with @Value("${org.test.process.special:false}") can be configured with the
+ * environment variable ORG_TEST_PROCESS_SPECIAL. To take advantage of the
+ * "dsf-tools-documentation-generator" maven plugin to generate a markdown file with configuration options for the
+ * plugin also add the {@link ProcessDocumentation} annotation.
+ *
+ * @return {@link Configuration} annotated classes, defining {@link Bean} annotated factory methods
+ * @see dev.dsf.bpe.v1.activity.AbstractServiceDelegate
+ * @see dev.dsf.bpe.v1.activity.AbstractTaskMessageSend
+ * @see dev.dsf.bpe.v1.activity.DefaultUserTaskListener
+ * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE
+ */
+ List> getSpringConfigurations();
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/ProcessPluginDeploymentStateListener.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/ProcessPluginDeploymentStateListener.java
new file mode 100644
index 000000000..c0457b898
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/ProcessPluginDeploymentStateListener.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1;
+
+import java.util.List;
+
+import org.springframework.context.annotation.Bean;
+
+/**
+ * Listener called after process plugin deployment with a list of deployed process-ids from this plugin. List contains
+ * all processes deployed in the bpe depending on the exclusion and retired config.
+ *
+ * Register a singleton {@link Bean} implementing this interface to execute custom code like connection tests if a
+ * process has been deployed.
+ */
+public interface ProcessPluginDeploymentStateListener
+{
+ void onProcessesDeployed(List processes);
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/config/ProxyConfig.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/config/ProxyConfig.java
new file mode 100644
index 000000000..b468011f0
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/config/ProxyConfig.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.config;
+
+import java.util.List;
+
+public interface ProxyConfig
+{
+ /**
+ * @return may be null
+ */
+ String getUrl();
+
+ /**
+ * @return true if a proxy url is configured and '*' is not set as a no-proxy url
+ */
+ boolean isEnabled();
+
+ /**
+ * @return may be null
+ */
+ String getUsername();
+
+ /**
+ * @return may be null
+ */
+ char[] getPassword();
+
+ /**
+ * @return never null, may be empty
+ */
+ List getNoProxyUrls();
+
+ /**
+ * Returns true if the given url is not null and the domain + port of the given
+ * url is configured as a no-proxy URL based on the environment configuration.
+ *
+ * Configured no-proxy URLs are matched exactly and against sub-domains. If a port is configured, only URLs with the
+ * same port (or default port) return a true result.
+ *
+ *
+ * No-Proxy URL examples
+ *
+ * | Configured |
+ * Given |
+ * Result |
+ *
+ *
+ * | foo.bar, test.com:8080 |
+ * https://foo.bar/fhir |
+ * true |
+ *
+ *
+ * | foo.bar, test.com:8080 |
+ * https://baz.foo.bar/test |
+ * true |
+ *
+ *
+ * | foo.bar, test.com:8080 |
+ * https://test.com:8080/fhir |
+ * true |
+ *
+ *
+ * | foo.bar, test.com:8080 |
+ * https://test.com/fhir |
+ * false |
+ *
+ *
+ * | foo.bar:443 |
+ * https://foo.bar/fhir |
+ * true |
+ *
+ *
+ *
+ * @param url
+ * may be null
+ * @return true if the given url is not null and is configured as a no-proxy url
+ */
+ boolean isNoProxyUrl(String url);
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/constants/BpmnExecutionVariables.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/constants/BpmnExecutionVariables.java
new file mode 100644
index 000000000..2f1d9c761
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/constants/BpmnExecutionVariables.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.constants;
+
+import dev.dsf.bpe.v1.variables.Target;
+
+/**
+ * Defines names of standard process engine variables used by the bpe
+ *
+ * @see dev.dsf.bpe.v1.variables.Variables
+ */
+public final class BpmnExecutionVariables
+{
+ private BpmnExecutionVariables()
+ {
+ }
+
+ /**
+ * Values from the target variable are used to configure
+ * {@link dev.dsf.bpe.v1.activity.AbstractTaskMessageSend} activities for sending Task resource messages
+ *
+ * @see dev.dsf.bpe.v1.variables.Variables#createTarget(String, String, String, String)
+ * @see dev.dsf.bpe.v1.variables.Variables#createTarget(String, String, String)
+ * @see dev.dsf.bpe.v1.variables.Variables#setTarget(dev.dsf.bpe.v1.variables.Target)
+ * @see dev.dsf.bpe.v1.variables.Variables#getTarget()
+ */
+ public static final String TARGET = "target";
+
+ /**
+ * The targets variable is typically used to iterate over {@link Target} variables in multi instance
+ * send/receive tasks or multi instance subprocesses
+ *
+ * @see dev.dsf.bpe.v1.variables.Variables#createTargets(java.util.List)
+ * @see dev.dsf.bpe.v1.variables.Variables#createTargets(dev.dsf.bpe.v1.variables.Target...)
+ * @see dev.dsf.bpe.v1.variables.Variables#setTargets(dev.dsf.bpe.v1.variables.Targets)
+ * @see dev.dsf.bpe.v1.variables.Variables#getTargets()
+ */
+ public static final String TARGETS = "targets";
+
+ /**
+ * Value of the correlationKey variable is used to correlated incoming Task resources to waiting multi
+ * instance process activities
+ *
+ * @see Target#getCorrelationKey()
+ */
+ public static final String CORRELATION_KEY = "correlationKey";
+
+ /**
+ * Value of the alternativeBusinessKey variable is used to correlated incoming Task resource to a
+ * waiting process instance if an alternative business-key was created for a communication target. See corresponding
+ * protected method in {@link dev.dsf.bpe.v1.activity.AbstractTaskMessageSend} on how to create and use
+ * an alternative business-key.
+ *
+ * @see dev.dsf.bpe.v1.activity.AbstractTaskMessageSend
+ */
+ public static final String ALTERNATIVE_BUSINESS_KEY = "alternativeBusinessKey";
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/constants/CodeSystems.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/constants/CodeSystems.java
new file mode 100644
index 000000000..c23167ffe
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/constants/CodeSystems.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.constants;
+
+import org.hl7.fhir.r4.model.Coding;
+
+/**
+ * Constants defining standard DSF CodeSystems
+ */
+public final class CodeSystems
+{
+ private CodeSystems()
+ {
+ }
+
+ public static final class BpmnMessage
+ {
+ private BpmnMessage()
+ {
+ }
+
+ public static final String URL = "http://dsf.dev/fhir/CodeSystem/bpmn-message";
+
+ public static final class Codes
+ {
+ private Codes()
+ {
+ }
+
+ public static final String MESSAGE_NAME = "message-name";
+ public static final String BUSINESS_KEY = "business-key";
+ public static final String CORRELATION_KEY = "correlation-key";
+ public static final String ERROR = "error";
+ }
+
+ public static final Coding messageName()
+ {
+ return new Coding(URL, Codes.MESSAGE_NAME, null);
+ }
+
+ public static final Coding businessKey()
+ {
+ return new Coding(URL, Codes.BUSINESS_KEY, null);
+ }
+
+ public static final Coding correlationKey()
+ {
+ return new Coding(URL, Codes.CORRELATION_KEY, null);
+ }
+
+ public static final Coding error()
+ {
+ return new Coding(URL, Codes.ERROR, null);
+ }
+ }
+
+ public static final class BpmnUserTask
+ {
+ private BpmnUserTask()
+ {
+ }
+
+ public static final String URL = "http://dsf.dev/fhir/CodeSystem/bpmn-user-task";
+
+ public static final class Codes
+ {
+ private Codes()
+ {
+ }
+
+ public static final String BUSINESS_KEY = "business-key";
+ public static final String USER_TASK_ID = "user-task-id";
+ }
+
+ public static final Coding businessKey()
+ {
+ return new Coding(URL, Codes.BUSINESS_KEY, null);
+ }
+
+ public static final Coding userTaskId()
+ {
+ return new Coding(URL, Codes.USER_TASK_ID, null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/constants/NamingSystems.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/constants/NamingSystems.java
new file mode 100644
index 000000000..6f804ca26
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/constants/NamingSystems.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.constants;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.hl7.fhir.r4.model.Endpoint;
+import org.hl7.fhir.r4.model.Identifier;
+import org.hl7.fhir.r4.model.Organization;
+import org.hl7.fhir.r4.model.Practitioner;
+import org.hl7.fhir.r4.model.Resource;
+import org.hl7.fhir.r4.model.Task;
+
+/**
+ * Constants defining standard DSF NamingSystems
+ */
+public final class NamingSystems
+{
+ private NamingSystems()
+ {
+ }
+
+ private static Optional findFirst(Supplier> identifierSupplier,
+ String identifierSystem)
+ {
+ Objects.requireNonNull(identifierSupplier, "identifierSupplier");
+ Objects.requireNonNull(identifierSystem, "identifierSystem");
+
+ List identifiers = identifierSupplier.get();
+ return identifiers == null ? Optional.empty()
+ : identifiers.stream().filter(i -> identifierSystem.equals(i.getSystem())).findFirst();
+ }
+
+ private static Optional findFirst(Optional resource,
+ Function> identifierFunction, String identifierSystem)
+ {
+ Objects.requireNonNull(resource, "resource");
+ Objects.requireNonNull(identifierFunction, "identifierFunction");
+ Objects.requireNonNull(identifierSystem, "identifierSystem");
+
+ return resource.map(identifierFunction).flatMap(findFirst(identifierSystem));
+ }
+
+ private static Function, Optional> findFirst(String identifierSystem)
+ {
+ Objects.requireNonNull(identifierSystem, "identifierSystem");
+
+ return ids -> ids.stream().filter(i -> identifierSystem.equals(i.getSystem())).findFirst();
+ }
+
+ public static final class OrganizationIdentifier
+ {
+ private OrganizationIdentifier()
+ {
+ }
+
+ public static final String SID = "http://dsf.dev/sid/organization-identifier";
+
+ public static Identifier withValue(String value)
+ {
+ return new Identifier().setSystem(SID).setValue(value);
+ }
+
+ public static Optional findFirst(Organization organization)
+ {
+ return organization == null ? Optional.empty() : NamingSystems.findFirst(organization::getIdentifier, SID);
+ }
+
+ public static Optional findFirst(Optional organization)
+ {
+ Objects.requireNonNull(organization, "organization");
+ return NamingSystems.findFirst(organization, Organization::getIdentifier, SID);
+ }
+ }
+
+ public static final class EndpointIdentifier
+ {
+ private EndpointIdentifier()
+ {
+ }
+
+ public static final String SID = "http://dsf.dev/sid/endpoint-identifier";
+
+ public static Identifier withValue(String value)
+ {
+ return new Identifier().setSystem(SID).setValue(value);
+ }
+
+ public static Optional findFirst(Endpoint endpoint)
+ {
+ return endpoint == null ? Optional.empty() : NamingSystems.findFirst(endpoint::getIdentifier, SID);
+ }
+
+ public static Optional findFirst(Optional endpoint)
+ {
+ Objects.requireNonNull(endpoint, "endpoint");
+ return NamingSystems.findFirst(endpoint, Endpoint::getIdentifier, SID);
+ }
+ }
+
+ public static final class PractitionerIdentifier
+ {
+ private PractitionerIdentifier()
+ {
+ }
+
+ public static final String SID = "http://dsf.dev/sid/practitioner-identifier";
+
+ public static Identifier withValue(String value)
+ {
+ return new Identifier().setSystem(SID).setValue(value);
+ }
+
+ public static Optional findFirst(Practitioner practitioner)
+ {
+ return practitioner == null ? Optional.empty() : NamingSystems.findFirst(practitioner::getIdentifier, SID);
+ }
+
+ public static Optional findFirst(Optional practitioner)
+ {
+ Objects.requireNonNull(practitioner, "practitioner");
+ return NamingSystems.findFirst(practitioner, Practitioner::getIdentifier, SID);
+ }
+ }
+
+ public static final class TaskIdentifier
+ {
+ private TaskIdentifier()
+ {
+ }
+
+ public static final String SID = "http://dsf.dev/sid/task-identifier";
+
+ public static Identifier withValue(String value)
+ {
+ return new Identifier().setSystem(SID).setValue(value);
+ }
+
+ public static Optional findFirst(Task task)
+ {
+ return task == null ? Optional.empty() : NamingSystems.findFirst(task::getIdentifier, SID);
+ }
+
+ public static Optional findFirst(Optional task)
+ {
+ Objects.requireNonNull(task, "task");
+ return NamingSystems.findFirst(task, Task::getIdentifier, SID);
+ }
+ }
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/documentation/ProcessDocumentation.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/documentation/ProcessDocumentation.java
new file mode 100644
index 000000000..4444ad763
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/documentation/ProcessDocumentation.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.documentation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+import dev.dsf.bpe.v1.ProcessPluginDefinition;
+
+/**
+ * Annotation for documenting DSF process plugin properties. Add this annotation in addition to {@link Value} to fields
+ * of your spring {@link Configuration} class in order to take advantage of the "dsf-tools-documentation-generator"
+ * maven plugin to generate a markdown file.
+ *
+ * Example:
+ *
+ *
+ * @ProcessDocumentation(description = "Set to `true` to enable a special function", processNames = "testorg_process")
+ * @Value("${org.test.process.special:false}")
+ * private boolean specialFunction;
+ *
+ *
+ * @see ProcessPluginDefinition#getSpringConfigurations()
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface ProcessDocumentation
+{
+ /**
+ * @return true if this property is required for processes listed in
+ * {@link ProcessDocumentation#processNames}
+ */
+ boolean required() default false;
+
+ /**
+ * @return an empty array if all processes use this property or an array of length {@literal >= 1} containing only
+ * specific processes that use this property, but not all
+ */
+ String[] processNames() default {};
+
+ /**
+ * @return description helping to configure this property
+ */
+ String description();
+
+ /**
+ * @return example value helping to configure this property
+ */
+ String example() default "";
+
+ /**
+ * @return recommendation helping to configure this property
+ */
+ String recommendation() default "";
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/EndpointProvider.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/EndpointProvider.java
new file mode 100644
index 000000000..ec2a5940a
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/EndpointProvider.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.service;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.Endpoint;
+import org.hl7.fhir.r4.model.Identifier;
+import org.hl7.fhir.r4.model.OrganizationAffiliation;
+
+import dev.dsf.bpe.v1.constants.NamingSystems.EndpointIdentifier;
+import dev.dsf.bpe.v1.constants.NamingSystems.OrganizationIdentifier;
+
+/**
+ * Provides access to {@link Endpoint} resources from the DSF FHIR server.
+ */
+public interface EndpointProvider
+{
+ /**
+ * @return Local DSF FHIR server base URL, e.g. https://foo.bar/fhir
+ */
+ String getLocalEndpointAddress();
+
+ /**
+ * @return {@link Endpoint} resource from the local DSF FHIR server associated with the configured base URL, empty
+ * {@link Optional} if no such resource exists
+ * @see #getLocalEndpointAddress()
+ */
+ Optional getLocalEndpoint();
+
+ /**
+ * @return DSF identifier of the {@link Endpoint} resource from the local DSF FHIR server associated with the
+ * configured base URL, empty {@link Optional} if no such resource exists or the {@link Endpoint} does not
+ * have a DSF identifier
+ * @see EndpointIdentifier
+ */
+ default Optional getLocalEndpointIdentifier()
+ {
+ return EndpointIdentifier.findFirst(getLocalEndpoint());
+ }
+
+ /**
+ * @return DSF identifier value of the {@link Endpoint} resource from the local DSF FHIR server associated with the
+ * configured base URL, empty {@link Optional} if no such resource exists or the {@link Endpoint} does not
+ * have a DSF identifier
+ * @see EndpointIdentifier
+ */
+ default Optional getLocalEndpointIdentifierValue()
+ {
+ return getLocalEndpointIdentifier().map(Identifier::getValue);
+ }
+
+ /**
+ * @param endpointIdentifier
+ * may be null
+ * @return Active {@link Endpoint} resource from the local DSF FHIR server with the given endpointIdentifier,
+ * empty {@link Optional} if no such resource exists or the given identifier is null
+ */
+ Optional getEndpoint(Identifier endpointIdentifier);
+
+ /**
+ * @param endpointIdentifierValue
+ * may be null
+ * @return Active {@link Endpoint} resource from the local DSF FHIR server with the given DSF
+ * endpointIdentifierValue, empty {@link Optional} if no such resource exists or the given identifier
+ * value is null
+ * @see EndpointIdentifier
+ */
+ default Optional getEndpoint(String endpointIdentifierValue)
+ {
+ return getEndpoint(
+ endpointIdentifierValue == null ? null : EndpointIdentifier.withValue(endpointIdentifierValue));
+ }
+
+ /**
+ * @param endpointIdentifier
+ * may be null
+ * @return Address (base URL) of the active {@link Endpoint} resource from the local DSF FHIR server with the given
+ * endpointIdentifier, empty {@link Optional} if no such resource exists or the given identifier is
+ * null
+ */
+ default Optional getEndpointAddress(Identifier endpointIdentifier)
+ {
+ return getEndpoint(endpointIdentifier).map(Endpoint::getAddress);
+ }
+
+ /**
+ * @param endpointIdentifierValue
+ * may be null
+ * @return Address (base URL) of the active {@link Endpoint} resource from the local DSF FHIR server with the given
+ * DSF endpointIdentifierValue, empty {@link Optional} if no such resource exists or the given
+ * identifier value is null
+ */
+ default Optional getEndpointAddress(String endpointIdentifierValue)
+ {
+ return getEndpointAddress(
+ endpointIdentifierValue == null ? null : EndpointIdentifier.withValue(endpointIdentifierValue));
+ }
+
+ /**
+ * @param parentOrganizationIdentifier
+ * may be null
+ * @param memberOrganizationIdentifier
+ * may be null
+ * @param memberOrganizationRole
+ * may be null
+ * @return Active {@link Endpoint} resource from the local DSF FHIR server associated with the given
+ * memberOrganizationIdentifier and memberOrganizationRole in a parent organization with the
+ * given parentOrganizationIdentifier, empty {@link Optional} if no such resource exists or one of
+ * the parameters is null; only considers Endpoints from active {@link OrganizationAffiliation}
+ * resources
+ */
+ Optional getEndpoint(Identifier parentOrganizationIdentifier, Identifier memberOrganizationIdentifier,
+ Coding memberOrganizationRole);
+
+ /**
+ * @param parentOrganizationIdentifierValue
+ * may be null
+ * @param memberOrganizationIdentifierValue
+ * may be null
+ * @param memberOrganizationRole
+ * may be null
+ * @return Active {@link Endpoint} resource from the local DSF FHIR server associated with the given DSF
+ * memberOrganizationIdentifierValue and memberOrganizationRole in a parent organization with
+ * the given DSF parentOrganizationIdentifierValue, empty {@link Optional} if no such resource exists
+ * or one of the parameters is null; only considers Endpoints from active
+ * {@link OrganizationAffiliation} resources
+ * @see OrganizationIdentifier
+ */
+ default Optional getEndpoint(String parentOrganizationIdentifierValue,
+ String memberOrganizationIdentifierValue, Coding memberOrganizationRole)
+ {
+ return getEndpoint(
+ parentOrganizationIdentifierValue == null ? null
+ : OrganizationIdentifier.withValue(parentOrganizationIdentifierValue),
+ memberOrganizationIdentifierValue == null ? null
+ : OrganizationIdentifier.withValue(memberOrganizationIdentifierValue),
+ memberOrganizationRole);
+ }
+
+ /**
+ * @param parentOrganizationIdentifier
+ * may be null
+ * @param memberOrganizationIdentifier
+ * may be null
+ * @param memberOrganizationRole
+ * may be null
+ * @return Address (base URL) of the active {@link Endpoint} resource from the local DSF FHIR server associated with
+ * the given memberOrganizationIdentifier and memberOrganizationRole in a parent organization
+ * with the given parentOrganizationIdentifier, empty {@link Optional} if no such resource exists or
+ * one of the parameters is null; only considers Endpoints from active
+ * {@link OrganizationAffiliation} resources
+ */
+ default Optional getEndpointAddress(Identifier parentOrganizationIdentifier,
+ Identifier memberOrganizationIdentifier, Coding memberOrganizationRole)
+ {
+ return getEndpoint(parentOrganizationIdentifier, memberOrganizationIdentifier, memberOrganizationRole)
+ .map(Endpoint::getAddress);
+ }
+
+ /**
+ * @param parentOrganizationIdentifierValue
+ * may be null
+ * @param memberOrganizationIdentifierValue
+ * may be null
+ * @param memberOrganizationRole
+ * may be null
+ * @return Address (base URL) of the active {@link Endpoint} resource from the local DSF FHIR server associated with
+ * the given DSF memberOrganizationIdentifierValue and memberOrganizationRole in a parent
+ * organization with the given DSF parentOrganizationIdentifierValue, empty {@link Optional} if no
+ * such resource exists or one of the parameters is null; only considers Endpoints from active
+ * {@link OrganizationAffiliation} resources
+ * @see OrganizationIdentifier
+ */
+ default Optional getEndpointAddress(String parentOrganizationIdentifierValue,
+ String memberOrganizationIdentifierValue, Coding memberOrganizationRole)
+ {
+ return getEndpointAddress(
+ parentOrganizationIdentifierValue == null ? null
+ : OrganizationIdentifier.withValue(parentOrganizationIdentifierValue),
+ memberOrganizationIdentifierValue == null ? null
+ : OrganizationIdentifier.withValue(memberOrganizationIdentifierValue),
+ memberOrganizationRole);
+ }
+
+ /**
+ * @param parentOrganizationIdentifier
+ * may be null
+ * @param memberOrganizationRole
+ * may be null
+ * @return Active {@link Endpoint} resources from the local DSF FHIR server associated with the given
+ * memberOrganizationRole in a parent organization with the given
+ * parentOrganizationIdentifier, empty {@link List} if no resources exist or one of the parameters is
+ * null; only considers Endpoints from active {@link OrganizationAffiliation} resources
+ */
+ List getEndpoints(Identifier parentOrganizationIdentifier, Coding memberOrganizationRole);
+
+ /**
+ * @param parentOrganizationIdentifierValue
+ * may be null
+ * @param memberOrganizationRole
+ * may be null
+ * @return Active {@link Endpoint} resources from the local DSF FHIR server associated with the given
+ * memberOrganizationRole in a parent organization with the given DSF
+ * parentOrganizationIdentifierValue, empty {@link List} if no resources exist or one of the
+ * parameters is null; only considers Endpoints from active {@link OrganizationAffiliation}
+ * resources
+ * @see OrganizationIdentifier
+ */
+ default List getEndpoints(String parentOrganizationIdentifierValue, Coding memberOrganizationRole)
+ {
+ return getEndpoints(parentOrganizationIdentifierValue == null ? null
+ : OrganizationIdentifier.withValue(parentOrganizationIdentifierValue), memberOrganizationRole);
+ }
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/FhirWebserviceClientProvider.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/FhirWebserviceClientProvider.java
new file mode 100644
index 000000000..69c510c7c
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/FhirWebserviceClientProvider.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.service;
+
+import dev.dsf.fhir.client.FhirWebserviceClient;
+
+public interface FhirWebserviceClientProvider
+{
+ FhirWebserviceClient getLocalWebserviceClient();
+
+ FhirWebserviceClient getWebserviceClient(String webserviceUrl);
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/MailService.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/MailService.java
new file mode 100644
index 000000000..10c72cafb
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/MailService.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.service;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.function.Consumer;
+
+import javax.mail.Message.RecipientType;
+import javax.mail.MessagingException;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+
+public interface MailService
+{
+ /**
+ * Sends a plain text mail to the BPE wide configured recipients.
+ *
+ * @param subject
+ * not null
+ * @param message
+ * not null
+ */
+ default void send(String subject, String message)
+ {
+ send(subject, message, (String) null);
+ }
+
+ /**
+ * Sends a plain text mail to the given address (to) if not null or the BPE wide configured
+ * recipients.
+ *
+ * @param subject
+ * not null
+ * @param message
+ * not null
+ * @param to
+ * BPE wide configured recipients if parameter is null
+ */
+ default void send(String subject, String message, String to)
+ {
+ send(subject, message, to == null ? null : Collections.singleton(to));
+ }
+
+ /**
+ * Sends a plain text mail to the given addresses (to) if not null and not empty or the BPE wide
+ * configured recipients.
+ *
+ * @param subject
+ * not null
+ * @param message
+ * not null
+ * @param to
+ * BPE wide configured recipients if parameter is null or empty
+ */
+ default void send(String subject, String message, Collection to)
+ {
+ try
+ {
+ MimeBodyPart body = new MimeBodyPart();
+ body.setText(message, StandardCharsets.UTF_8.displayName());
+
+ send(subject, body, to);
+ }
+ catch (MessagingException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Sends the given {@link MimeBodyPart} as content of a mail to the BPE wide configured recipients.
+ *
+ * @param subject
+ * not null
+ * @param body
+ * not null
+ */
+ default void send(String subject, MimeBodyPart body)
+ {
+ send(subject, body, (String) null);
+ }
+
+ /**
+ * Sends the given {@link MimeBodyPart} as content of a mail to the given address (to) if not
+ * null or the BPE wide configured recipients.
+ *
+ * @param subject
+ * not null
+ * @param body
+ * not null
+ * @param to
+ * BPE wide configured recipients if parameter is null
+ */
+ default void send(String subject, MimeBodyPart body, String to)
+ {
+ send(subject, body, to == null ? null : Collections.singleton(to));
+ }
+
+ /**
+ * Sends the given {@link MimeBodyPart} as content of a mail to the given addresses (to) if not
+ * null and not empty or the BPE wide configured recipients.
+ *
+ * @param subject
+ * not null
+ * @param body
+ * not null
+ * @param to
+ * BPE wide configured recipients if parameter is null or empty
+ */
+ default void send(String subject, MimeBodyPart body, Collection to)
+ {
+ if (to == null || to.isEmpty())
+ send(subject, body, (Consumer) null);
+ else
+ send(subject, body, m ->
+ {
+ try
+ {
+ m.setRecipients(RecipientType.TO, to.stream().map(t ->
+ {
+ try
+ {
+ return new InternetAddress(t);
+ }
+ catch (AddressException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }).toArray(InternetAddress[]::new));
+
+ m.saveChanges();
+ }
+ catch (MessagingException e)
+ {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ /**
+ * Sends the given {@link MimeBodyPart} as content of a mail to the BPE wide configured recipients, the
+ * messageModifier can be used to modify elements of the generated {@link MimeMessage} before it is send to
+ * the SMTP server.
+ *
+ * @param subject
+ * not null
+ * @param body
+ * not null
+ * @param messageModifier
+ * may be null
+ */
+ void send(String subject, MimeBodyPart body, Consumer messageModifier);
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/OrganizationProvider.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/OrganizationProvider.java
new file mode 100644
index 000000000..20bda7c0f
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/OrganizationProvider.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.service;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.Endpoint;
+import org.hl7.fhir.r4.model.Identifier;
+import org.hl7.fhir.r4.model.Organization;
+import org.hl7.fhir.r4.model.OrganizationAffiliation;
+
+import dev.dsf.bpe.v1.constants.NamingSystems.OrganizationIdentifier;
+
+/**
+ * Provides access to {@link Organization} resources from the DSF FHIR server.
+ */
+public interface OrganizationProvider
+{
+ /**
+ * Retrieves the local {@link Organization} resources by searching for the managing {@link Organization} of the
+ * local {@link Endpoint} resources. The local {@link Endpoint} resource is identified by the DSF FHIR server
+ * address configured for the DSF BPE server.
+ *
+ * @return Managing {@link Organization} for the {@link Endpoint} resource with address equal to the DSF FHIR server
+ * base address configured for this DSF BPE, empty {@link Optional} if no such resource exists
+ * @see #getRemoteOrganizations()
+ */
+ Optional getLocalOrganization();
+
+ /**
+ * @return DSF organization identifier from the local {@link Organization} resource, empty {@link Optional} if no
+ * such resource exists or the {@link Organization} does not have a DSF organization identifier
+ * @see #getLocalOrganization()
+ * @see OrganizationIdentifier
+ */
+ default Optional getLocalOrganizationIdentifier()
+ {
+ return OrganizationIdentifier.findFirst(getLocalOrganization());
+ }
+
+ /**
+ * @return DSF organization identifier value from the local {@link Organization} resource, empty {@link Optional} if
+ * no such resource exists or the {@link Organization} does not have a DSF organization identifier
+ * @see #getLocalOrganization()
+ * @see OrganizationIdentifier
+ */
+ default Optional getLocalOrganizationIdentifierValue()
+ {
+ return getLocalOrganizationIdentifier().map(Identifier::getValue);
+ }
+
+ /**
+ * @param organizationIdentifier
+ * may be null
+ * @return Active {@link Organization} with the given organizationIdentifier, empty {@link Optional} if no
+ * such resource exists or the given identifier is null
+ */
+ Optional getOrganization(Identifier organizationIdentifier);
+
+ /**
+ * @param organizationIdentifierValue
+ * may be null
+ * @return Active {@link Organization} with the given DSF organizationIdentifier, empty {@link Optional} if
+ * no such resource exists or the given identifier value is null
+ * @see OrganizationIdentifier
+ */
+ default Optional getOrganization(String organizationIdentifierValue)
+ {
+ return getOrganization(organizationIdentifierValue == null ? null
+ : OrganizationIdentifier.withValue(organizationIdentifierValue));
+ }
+
+ /**
+ * @param parentOrganizationIdentifier
+ * may be null
+ * @return Active Organizations configured as participatingOrganization for an active parent {@link Organization}
+ * with the given parentOrganizationIdentifier, empty {@link List} if no parent organization found,
+ * parent has no participating organizations configured via {@link OrganizationAffiliation} resources or the
+ * given identifier is null
+ */
+ List getOrganizations(Identifier parentOrganizationIdentifier);
+
+ /**
+ * @param parentOrganizationIdentifierValue
+ * may be null
+ * @return Active Organizations configured as participatingOrganization for an active parent {@link Organization}
+ * with the given DSF parentOrganizationIdentifierValue, empty {@link List} if no parent organization
+ * found, parent has no participating organizations configured via {@link OrganizationAffiliation} resources
+ * or the given identifier is null
+ * @see OrganizationIdentifier
+ */
+ default List getOrganizations(String parentOrganizationIdentifierValue)
+ {
+ return getOrganizations(parentOrganizationIdentifierValue == null ? null
+ : OrganizationIdentifier.withValue(parentOrganizationIdentifierValue));
+ }
+
+ /**
+ * @param parentOrganizationIdentifier
+ * may be null
+ * @param memberOrganizationRole
+ * may be null
+ * @return Active Organizations configured as participatingOrganization for an active parent {@link Organization}
+ * with the given parentOrganizationIdentifier and role equal to the given
+ * memberOrganizationRole, empty {@link List} if no parent organization found, parent has no
+ * participating organizations configured via {@link OrganizationAffiliation} resources with the given role
+ * or the given identifier is null
+ */
+ List getOrganizations(Identifier parentOrganizationIdentifier, Coding memberOrganizationRole);
+
+ /**
+ * @param parentOrganizationIdentifierValue
+ * may be null
+ * @param memberOrganizationRole
+ * may be null
+ * @return Active Organizations configured as participatingOrganization for an active parent {@link Organization}
+ * with the given parentOrganizationIdentifier and role equal to the given
+ * memberOrganizationRole, empty {@link List} if no parent organization found, parent has no
+ * participating organizations configured via {@link OrganizationAffiliation} resources with the given role
+ * or the given identifier is null
+ * @see OrganizationIdentifier
+ */
+ default List getOrganizations(String parentOrganizationIdentifierValue, Coding memberOrganizationRole)
+ {
+ return getOrganizations(parentOrganizationIdentifierValue == null ? null
+ : OrganizationIdentifier.withValue(parentOrganizationIdentifierValue), memberOrganizationRole);
+ }
+
+ /**
+ * @return All active {@link Organization} resources except the local {@link Organization}
+ * @see #getLocalOrganization()
+ */
+ List getRemoteOrganizations();
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/QuestionnaireResponseHelper.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/QuestionnaireResponseHelper.java
new file mode 100644
index 000000000..cf2d8da13
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/QuestionnaireResponseHelper.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.service;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.hl7.fhir.r4.model.Questionnaire;
+import org.hl7.fhir.r4.model.QuestionnaireResponse;
+import org.hl7.fhir.r4.model.Type;
+
+public interface QuestionnaireResponseHelper
+{
+ default Optional getFirstItemLeaveMatchingLinkId(
+ QuestionnaireResponse questionnaireResponse, String linkId)
+ {
+ return getItemLeavesMatchingLinkIdAsStream(questionnaireResponse, linkId).findFirst();
+ }
+
+ default List getItemLeavesMatchingLinkIdAsList(
+ QuestionnaireResponse questionnaireResponse, String linkId)
+ {
+ return getItemLeavesMatchingLinkIdAsStream(questionnaireResponse, linkId).collect(Collectors.toList());
+ }
+
+ Stream getItemLeavesMatchingLinkIdAsStream(
+ QuestionnaireResponse questionnaireResponse, String linkId);
+
+ default List getItemLeavesAsList(
+ QuestionnaireResponse questionnaireResponse)
+ {
+ return getItemLeavesAsStream(questionnaireResponse).collect(Collectors.toList());
+ }
+
+ Stream getItemLeavesAsStream(
+ QuestionnaireResponse questionnaireResponse);
+
+ Type transformQuestionTypeToAnswerType(Questionnaire.QuestionnaireItemComponent question);
+
+ void addItemLeafWithoutAnswer(QuestionnaireResponse questionnaireResponse, String linkId, String text);
+
+ void addItemLeafWithAnswer(QuestionnaireResponse questionnaireResponse, String linkId, String text, Type answer);
+
+ String getLocalVersionlessAbsoluteUrl(QuestionnaireResponse questionnaireResponse);
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/TaskHelper.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/TaskHelper.java
new file mode 100644
index 000000000..e563056a2
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/service/TaskHelper.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.service;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.StringType;
+import org.hl7.fhir.r4.model.Task;
+import org.hl7.fhir.r4.model.Task.ParameterComponent;
+import org.hl7.fhir.r4.model.Task.TaskOutputComponent;
+import org.hl7.fhir.r4.model.Type;
+
+public interface TaskHelper
+{
+ /**
+ * @param task
+ * may be null
+ * @return null if the given task is null
+ */
+ String getLocalVersionlessAbsoluteUrl(Task task);
+
+
+ /**
+ * Returns the first input parameter value from the given task with the given coding (system, code),
+ * if the value of the input parameter is of type 'string'.
+ *
+ * @param task
+ * may be null
+ * @param coding
+ * may be null
+ * @return {@link Optional#empty()} if the given task or coding is null
+ * @see ParameterComponent#getType()
+ * @see StringType
+ */
+ default Optional getFirstInputParameterStringValue(Task task, Coding coding)
+ {
+ return getInputParameterStringValues(task, coding).findFirst();
+ }
+
+ /**
+ * Returns the first input parameter value from the given task with the given system and code,
+ * if the value of the input parameter is of type 'string'.
+ *
+ * @param task
+ * may be null
+ * @param system
+ * may be null
+ * @param code
+ * may be null
+ * @return {@link Optional#empty()} if the given task is null
+ * @see ParameterComponent#getType()
+ * @see StringType
+ */
+ default Optional getFirstInputParameterStringValue(Task task, String system, String code)
+ {
+ return getInputParameterStringValues(task, system, code).findFirst();
+ }
+
+ /**
+ * Returns the first input parameter value from the given task with the given coding (system, code),
+ * if the value of the input parameter has the given expectedType.
+ *
+ * @param
+ * input parameter value type
+ * @param task
+ * may be null
+ * @param coding
+ * may be null
+ * @param expectedType
+ * not null
+ * @return {@link Optional#empty()} if the given task or coding is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ * @throws NullPointerException
+ * if the given expectedType is null
+ */
+ default Optional getFirstInputParameterValue(Task task, Coding coding, Class expectedType)
+ {
+ return getInputParameterValues(task, coding, expectedType).findFirst();
+ }
+
+ /**
+ * Returns the first input parameter value from the given task with the given system and code,
+ * if the value of the input parameter has the given expectedType.
+ *
+ * @param
+ * input parameter value type
+ * @param task
+ * may be null
+ * @param system
+ * may be null
+ * @param code
+ * may be null
+ * @param expectedType
+ * not null
+ * @return {@link Optional#empty()} if the given task is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ * @throws NullPointerException
+ * if the given expectedType is null
+ */
+ default Optional getFirstInputParameterValue(Task task, String system, String code,
+ Class expectedType)
+ {
+ return getInputParameterValues(task, system, code, expectedType).findFirst();
+ }
+
+ /**
+ * Returns the first input parameter from the given task with the given coding (system, code), if the
+ * value of the input parameter has the given expectedType and the input parameter has an extension with the
+ * given extensionUrl.
+ *
+ * @param task
+ * may be null
+ * @param coding
+ * may be null
+ * @param expectedType
+ * not null
+ * @param extensionUrl
+ * may be null
+ * @return {@link Optional#empty()} if the given task or coding is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ * @throws NullPointerException
+ * if the given expectedType is null
+ */
+ default Optional getFirstInputParameterWithExtension(Task task, Coding coding,
+ Class extends Type> expectedType, String extensionUrl)
+ {
+ return getInputParametersWithExtension(task, coding, expectedType, extensionUrl).findFirst();
+ }
+
+ /**
+ * Returns the first input parameter from the given task with the given system and code, if the
+ * value of the input parameter has the given expectedType and the input parameter has an extension with the
+ * given extensionUrl.
+ *
+ * @param task
+ * may be null
+ * @param system
+ * may be null
+ * @param code
+ * may be null
+ * @param expectedType
+ * not null
+ * @param extensionUrl
+ * may be null
+ * @return {@link Optional#empty()} if the given task is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ * @throws NullPointerException
+ * if the given expectedType is null
+ */
+ default Optional getFirstInputParameterWithExtension(Task task, String system, String code,
+ Class extends Type> expectedType, String extensionUrl)
+ {
+ return getInputParametersWithExtension(task, system, code, expectedType, extensionUrl).findFirst();
+ }
+
+ /**
+ * Returns the first input parameter from the given task with the given coding (system, code), if the
+ * value of the input parameter has the given expectedType.
+ *
+ * @param task
+ * may be null
+ * @param coding
+ * may be null
+ * @param expectedType
+ * not null
+ * @return {@link Optional#empty()} if the given task or coding is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ * @throws NullPointerException
+ * if the given expectedType is null
+ */
+ default Optional getFirstInputParameter(Task task, Coding coding,
+ Class extends Type> expectedType)
+ {
+ return getInputParameters(task, coding, expectedType).findFirst();
+ }
+
+ /**
+ * Returns the first input parameter from the given task with the given system and code, if the
+ * value of the input parameter has the given expectedType.
+ *
+ * @param task
+ * may be null
+ * @param system
+ * may be null
+ * @param code
+ * may be null
+ * @param expectedType
+ * not null
+ * @return {@link Optional#empty()} if the given task is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ * @throws NullPointerException
+ * if the given expectedType is null
+ */
+ default Optional getFirstInputParameter(Task task, String system, String code,
+ Class extends Type> expectedType)
+ {
+ return getInputParameters(task, system, code, expectedType).findFirst();
+ }
+
+
+ /**
+ * Returns input parameter values from the given task with the given coding (system, code), if the
+ * value of the input parameter is of type 'string'.
+ *
+ * @param task
+ * may be null
+ * @param coding
+ * may be null
+ * @return {@link Stream#empty()} if the given task or coding is null
+ * @see ParameterComponent#getType()
+ * @see StringType
+ */
+ Stream getInputParameterStringValues(Task task, Coding coding);
+
+ /**
+ * Returns input parameter values from the given task with the given system and code, if the
+ * value of the input parameter is of type 'string'.
+ *
+ * @param task
+ * may be null
+ * @param system
+ * may be null
+ * @param code
+ * may be null
+ * @return {@link Stream#empty()} if the given task is null
+ * @see ParameterComponent#getType()
+ * @see StringType
+ */
+ Stream getInputParameterStringValues(Task task, String system, String code);
+
+ /**
+ * Returns input parameter values from the given task with the given coding (system, code), if the
+ * value of the input parameter has the given expectedType.
+ *
+ * @param
+ * input parameter value type
+ * @param task
+ * may be null
+ * @param coding
+ * may be null
+ * @param expectedType
+ * not null
+ * @return {@link Stream#empty()} if the given task or coding is null
+ * @throws NullPointerException
+ * if the given expectedType is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ */
+ Stream getInputParameterValues(Task task, Coding coding, Class expectedType);
+
+ /**
+ * Returns input parameter values from the given task with the given system and code, if the
+ * value of the input parameter has the given expectedType.
+ *
+ * @param
+ * input parameter value type
+ * @param task
+ * may be null
+ * @param system
+ * may be null
+ * @param code
+ * may be null
+ * @param expectedType
+ * not null
+ * @return {@link Stream#empty()} if the given task is null
+ * @throws NullPointerException
+ * if the given expectedType is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ */
+ Stream getInputParameterValues(Task task, String system, String code, Class expectedType);
+
+ /**
+ * Returns input parameters from the given task with the given coding (system, code), if the value of
+ * the input parameter has the given expectedType and the input parameter has an extension with the given
+ * extensionUrl.
+ *
+ * @param task
+ * may be null
+ * @param coding
+ * may be null
+ * @param expectedType
+ * not null
+ * @param extensionUrl
+ * may be null
+ * @return {@link Stream#empty()} if the given task or coding is null
+ * @throws NullPointerException
+ * if the given expectedType is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ */
+ Stream getInputParametersWithExtension(Task task, Coding coding,
+ Class extends Type> expectedType, String extensionUrl);
+
+ /**
+ * Returns input parameters from the given task with the given system and code, if the value of
+ * the input parameter has the given expectedType and the input parameter has an extension with the given
+ * extensionUrl.
+ *
+ * @param task
+ * may be null
+ * @param system
+ * may be null
+ * @param code
+ * may be null
+ * @param expectedType
+ * not null
+ * @param extensionUrl
+ * may be null
+ * @return {@link Stream#empty()} if the given task is null
+ * @throws NullPointerException
+ * if the given expectedType is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ */
+ Stream getInputParametersWithExtension(Task task, String system, String code,
+ Class extends Type> expectedType, String extensionUrl);
+
+ /**
+ * Returns the input parameters from the given task with the given coding (system, code), if the value
+ * of the input parameter has the given expectedType.
+ *
+ * @param task
+ * may be null
+ * @param coding
+ * may be null
+ * @param expectedType
+ * not null
+ * @return {@link Stream#empty()} if the given task or coding is null
+ * @throws NullPointerException
+ * if the given expectedType is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ */
+ Stream getInputParameters(Task task, Coding coding, Class extends Type> expectedType);
+
+ /**
+ * Returns the input parameters from the given task with the given system and code, if the
+ * value of the input parameter has the given expectedType.
+ *
+ * @param task
+ * may be null
+ * @param system
+ * may be null
+ * @param code
+ * may be null
+ * @param expectedType
+ * not null
+ * @return {@link Stream#empty()} if the given task is null
+ * @throws NullPointerException
+ * if the given expectedType is null
+ * @see ParameterComponent#getType()
+ * @see Type
+ */
+ Stream getInputParameters(Task task, String system, String code,
+ Class extends Type> expectedType);
+
+
+ /**
+ * Creates an input parameter for the given value and coding.
+ *
+ * @param value
+ * may be null
+ * @param coding
+ * may be null
+ * @return not null
+ * @see ParameterComponent#setType(org.hl7.fhir.r4.model.CodeableConcept)
+ * @see ParameterComponent#setValue(Type)
+ */
+ ParameterComponent createInput(Type value, Coding coding);
+
+ /**
+ * Creates an input parameter for the given value, system and code.
+ *
+ * @param value
+ * may be null
+ * @param system
+ * may be null
+ * @param code
+ * may be null
+ * @return not null
+ * @see ParameterComponent#setType(org.hl7.fhir.r4.model.CodeableConcept)
+ * @see ParameterComponent#setValue(Type)
+ */
+ ParameterComponent createInput(Type value, String system, String code);
+
+
+ /**
+ * Creates an output parameter for the given value and coding.
+ *
+ * @param value
+ * may be null
+ * @param coding
+ * may be null
+ * @return not null
+ * @see TaskOutputComponent#setType(org.hl7.fhir.r4.model.CodeableConcept)
+ * @see TaskOutputComponent#setValue(Type)
+ */
+ TaskOutputComponent createOutput(Type value, Coding coding);
+
+ /**
+ * Creates an output parameter for the given value, system and code.
+ *
+ * @param value
+ * may be null
+ * @param system
+ * may be null
+ * @param code
+ * may be null
+ * @return not null
+ * @see TaskOutputComponent#setType(org.hl7.fhir.r4.model.CodeableConcept)
+ * @see TaskOutputComponent#setValue(Type)
+ */
+ TaskOutputComponent createOutput(Type value, String system, String code);
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/variables/Target.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/variables/Target.java
new file mode 100644
index 000000000..67f73b18a
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/variables/Target.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.variables;
+
+/**
+ * Specifies a communication target for FHIR Task resources.
+ *
+ * @see dev.dsf.bpe.v1.constants.BpmnExecutionVariables#TARGET
+ * @see Variables#createTarget(String, String, String, String)
+ * @see Variables#createTarget(String, String, String)
+ * @see Targets
+ */
+public interface Target
+{
+ /**
+ * @return not null
+ */
+ String getOrganizationIdentifierValue();
+
+ /**
+ * @return not null
+ */
+ String getEndpointIdentifierValue();
+
+ /**
+ * @return not null
+ */
+ String getEndpointUrl();
+
+ /**
+ * @return may be null
+ */
+ String getCorrelationKey();
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/variables/Targets.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/variables/Targets.java
new file mode 100644
index 000000000..c0115fcfa
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/bpe/v1/variables/Targets.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.bpe.v1.variables;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Specifies a list of communication targets for FHIR Task resources.
+ *
+ * @see dev.dsf.bpe.v1.constants.BpmnExecutionVariables#TARGETS
+ * @see Variables#createTargets(List)
+ * @see Variables#createTargets(Target...)
+ * @see Target
+ */
+public interface Targets
+{
+ /**
+ * @return not null
+ */
+ List getEntries();
+
+ /**
+ * Removes targets base on the given {@link Target}s endpoint identifier value.
+ *
+ * @param target
+ * @return new {@link Targets} object
+ * @see Target#getEndpointIdentifierValue()
+ */
+ Targets removeByEndpointIdentifierValue(Target target);
+
+ /**
+ * Removes targets base on the given endpoint identifier value.
+ *
+ * @param targetEndpointIdentifierValue
+ * @return new {@link Targets} object
+ */
+ Targets removeByEndpointIdentifierValue(String targetEndpointIdentifierValue);
+
+ /**
+ * Removes targets base on the given endpoint identifier values.
+ *
+ * @param targetEndpointIdentifierValues
+ * @return new {@link Targets} object
+ */
+ Targets removeAllByEndpointIdentifierValue(Collection targetEndpointIdentifierValues);
+
+ /**
+ * @return true if the entries list is empty
+ */
+ boolean isEmpty();
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentials.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentials.java
new file mode 100644
index 000000000..1e26e7713
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentials.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.common.auth;
+
+import java.util.Map;
+
+public interface DsfOpenIdCredentials
+{
+ String getUserId();
+
+ Map getAccessToken();
+
+ /**
+ * @return empty when authentication via bearer token
+ */
+ Map getIdToken();
+
+ /**
+ * @param key
+ * not null
+ * @return null if no {@link Long} entry with the given key in id-token
+ */
+ Long getLongClaim(String key);
+
+ /**
+ * @param key
+ * not null
+ * @param defaultValue
+ * @return defaultValue if no {@link String} entry with the given key in id-token
+ */
+ String getStringClaimOrDefault(String key, String defaultValue);
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/DsfRole.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/DsfRole.java
new file mode 100644
index 000000000..703fcddca
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/DsfRole.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.common.auth.conf;
+
+public interface DsfRole
+{
+ String name();
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/Identity.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/Identity.java
new file mode 100644
index 000000000..7f0c68830
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/Identity.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.common.auth.conf;
+
+import java.security.Principal;
+import java.security.cert.X509Certificate;
+import java.util.Optional;
+import java.util.Set;
+
+import org.hl7.fhir.r4.model.Organization;
+
+public interface Identity extends Principal
+{
+ String ORGANIZATION_IDENTIFIER_SYSTEM = "http://dsf.dev/sid/organization-identifier";
+
+ boolean isLocalIdentity();
+
+ /**
+ * @return never null
+ */
+ Organization getOrganization();
+
+ Optional getOrganizationIdentifierValue();
+
+ Set getDsfRoles();
+
+ boolean hasDsfRole(DsfRole role);
+
+ /**
+ * @return {@link Optional#empty()} if login via OIDC
+ */
+ Optional getCertificate();
+
+ String getDisplayName();
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/OrganizationIdentity.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/OrganizationIdentity.java
new file mode 100644
index 000000000..d0fcfe29f
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/OrganizationIdentity.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.common.auth.conf;
+
+public interface OrganizationIdentity extends Identity
+{
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentity.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentity.java
new file mode 100644
index 000000000..41b4b8d05
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentity.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.common.auth.conf;
+
+import java.util.Optional;
+import java.util.Set;
+
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.Practitioner;
+
+import dev.dsf.common.auth.DsfOpenIdCredentials;
+
+public interface PractitionerIdentity extends Identity
+{
+ String PRACTITIONER_IDENTIFIER_SYSTEM = "http://dsf.dev/sid/practitioner-identifier";
+
+ /**
+ * @return never null
+ */
+ Practitioner getPractitioner();
+
+ /**
+ * @return never null
+ */
+ Set getPractionerRoles();
+
+ /**
+ * @return {@link Optional#empty()} if login via client certificate
+ */
+ Optional getCredentials();
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/All.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/All.java
new file mode 100644
index 000000000..06476a755
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/All.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization.process;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.Extension;
+import org.hl7.fhir.r4.model.OrganizationAffiliation;
+
+import dev.dsf.common.auth.conf.Identity;
+import dev.dsf.common.auth.conf.OrganizationIdentity;
+import dev.dsf.common.auth.conf.PractitionerIdentity;
+
+public class All implements Recipient, Requester
+{
+ private final boolean localIdentity;
+
+ private final String practitionerRoleSystem;
+ private final String practitionerRoleCode;
+
+ public All(boolean localIdentity, String practitionerRoleSystem, String practitionerRoleCode)
+ {
+ this.localIdentity = localIdentity;
+
+ this.practitionerRoleSystem = practitionerRoleSystem;
+ this.practitionerRoleCode = practitionerRoleCode;
+ }
+
+ private boolean needsPractitionerRole()
+ {
+ return practitionerRoleSystem != null && practitionerRoleCode != null;
+ }
+
+ @Override
+ public boolean isRequesterAuthorized(Identity requester, Stream requesterAffiliations)
+ {
+ return isAuthorized(requester);
+ }
+
+ @Override
+ public boolean isRecipientAuthorized(Identity recipient, Stream recipientAffiliations)
+ {
+ return isAuthorized(recipient);
+ }
+
+ private boolean isAuthorized(Identity identity)
+ {
+ return identity != null && identity.getOrganization() != null && identity.getOrganization().getActive()
+ && identity.isLocalIdentity() == localIdentity
+ && ((needsPractitionerRole() && hasPractitionerRole(getPractitionerRoles(identity)))
+ || (!needsPractitionerRole() && identity instanceof OrganizationIdentity));
+ }
+
+ private Set getPractitionerRoles(Identity identity)
+ {
+ if (identity instanceof PractitionerIdentity p)
+ return p.getPractionerRoles();
+ else
+ return Collections.emptySet();
+ }
+
+ private boolean hasPractitionerRole(Set practitionerRoles)
+ {
+ return practitionerRoles.stream().anyMatch(
+ c -> practitionerRoleSystem.equals(c.getSystem()) && practitionerRoleCode.equals(c.getCode()));
+ }
+
+ @Override
+ public Extension toRecipientExtension()
+ {
+ return new Extension().setUrl(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT)
+ .setValue(toCoding(false));
+ }
+
+ @Override
+ public Extension toRequesterExtension()
+ {
+ return new Extension().setUrl(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_REQUESTER)
+ .setValue(toCoding(needsPractitionerRole()));
+ }
+
+ private Coding toCoding(boolean needsPractitionerRole)
+ {
+ Coding coding = getProcessAuthorizationCode();
+
+ if (needsPractitionerRole)
+ coding.addExtension().setUrl(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PRACTITIONER)
+ .setValue(new Coding(practitionerRoleSystem, practitionerRoleCode, null));
+
+ return coding;
+ }
+
+ @Override
+ public Coding getProcessAuthorizationCode()
+ {
+ if (localIdentity)
+ {
+ if (needsPractitionerRole())
+ return new Coding(ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM,
+ ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL_PRACTITIONER, null);
+ else
+ return new Coding(ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM,
+ ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL, null);
+ }
+ else
+ {
+ return new Coding(ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM,
+ ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ALL, null);
+ }
+ }
+
+ @Override
+ public boolean requesterMatches(Extension requesterExtension)
+ {
+ return matches(requesterExtension, ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_REQUESTER)
+ && hasMatchingPractitionerExtension(requesterExtension.getValue().getExtension());
+ }
+
+ @Override
+ public boolean recipientMatches(Extension recipientExtension)
+ {
+ return matches(recipientExtension, ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT);
+ }
+
+ private boolean matches(Extension extension, String url)
+ {
+ return extension != null && url.equals(extension.getUrl()) && extension.hasValue()
+ && extension.getValue() instanceof Coding value && matches(value);
+ }
+
+ private boolean hasMatchingPractitionerExtension(List extensions)
+ {
+ return needsPractitionerRole() ? extensions.stream().anyMatch(this::practitionerExtensionMatches)
+ : extensions.stream().noneMatch(this::practitionerExtensionMatches);
+ }
+
+ private boolean practitionerExtensionMatches(Extension extension)
+ {
+ return ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PRACTITIONER.equals(extension.getUrl())
+ && extension.hasValue() && extension.getValue() instanceof Coding value
+ && practitionerRoleMatches(value);
+ }
+
+ private boolean practitionerRoleMatches(Coding coding)
+ {
+ return coding != null && coding.hasSystem() && coding.hasCode()
+ && practitionerRoleSystem.equals(coding.getSystem()) && practitionerRoleCode.equals(coding.getCode());
+ }
+
+ @Override
+ public boolean matches(Coding processAuthorizationCode)
+ {
+ if (localIdentity)
+ if (needsPractitionerRole())
+ return processAuthorizationCode != null
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM
+ .equals(processAuthorizationCode.getSystem())
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL_PRACTITIONER
+ .equals(processAuthorizationCode.getCode());
+ else
+ return processAuthorizationCode != null
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM
+ .equals(processAuthorizationCode.getSystem())
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL
+ .equals(processAuthorizationCode.getCode());
+ else
+ return processAuthorizationCode != null
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM
+ .equals(processAuthorizationCode.getSystem())
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ALL
+ .equals(processAuthorizationCode.getCode());
+ }
+
+ public static Optional fromRequester(Coding coding, Predicate practitionerRoleExists)
+ {
+ if (coding != null && coding.hasSystem()
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM.equals(coding.getSystem())
+ && coding.hasCode())
+ {
+ if (ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL.equals(coding.getCode()))
+ return Optional.of(new All(true, null, null));
+ else if (ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ALL.equals(coding.getCode()))
+ return Optional.of(new All(false, null, null));
+ else if (ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL_PRACTITIONER
+ .equals(coding.getCode()))
+ return fromPractitionerRequester(coding, practitionerRoleExists);
+ }
+
+ return Optional.empty();
+ }
+
+ private static Optional fromPractitionerRequester(Coding coding,
+ Predicate practitionerRoleExists)
+ {
+ if (coding != null && coding.hasExtension())
+ {
+ List practitionerRoles = coding.getExtension().stream().filter(Extension::hasUrl).filter(
+ e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PRACTITIONER.equals(e.getUrl()))
+ .collect(Collectors.toList());
+ if (practitionerRoles.size() == 1)
+ {
+ Extension practitionerRole = practitionerRoles.get(0);
+ if (practitionerRole.hasValue() && practitionerRole.getValue() instanceof Coding value
+ && value.hasSystem() && value.hasCode() && practitionerRoleExists.test(coding))
+ {
+ return Optional.of(new All(true, value.getSystem(), value.getCode()));
+ }
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ public static Optional fromRecipient(Coding coding)
+ {
+ if (coding != null && coding.hasSystem()
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM.equals(coding.getSystem())
+ && coding.hasCode()
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL.equals(coding.getCode()))
+ {
+ return Optional.of(new All(true, null, null));
+ // remote not allowed for recipient
+ }
+
+ return Optional.empty();
+ }
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Organization.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Organization.java
new file mode 100644
index 000000000..1e6f3cfb2
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Organization.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization.process;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.Extension;
+import org.hl7.fhir.r4.model.Identifier;
+import org.hl7.fhir.r4.model.OrganizationAffiliation;
+import org.hl7.fhir.r4.model.Reference;
+
+import dev.dsf.common.auth.conf.Identity;
+import dev.dsf.common.auth.conf.OrganizationIdentity;
+import dev.dsf.common.auth.conf.PractitionerIdentity;
+import dev.dsf.fhir.authorization.read.ReadAccessHelper;
+
+public class Organization implements Recipient, Requester
+{
+ private final String organizationIdentifier;
+ private final boolean localIdentity;
+
+ private final String practitionerRoleSystem;
+ private final String practitionerRoleCode;
+
+ public Organization(boolean localIdentity, String organizationIdentifier, String practitionerRoleSystem,
+ String practitionerRoleCode)
+ {
+ Objects.requireNonNull(organizationIdentifier, "organizationIdentifier");
+ if (organizationIdentifier.isBlank())
+ throw new IllegalArgumentException("organizationIdentifier blank");
+
+ this.localIdentity = localIdentity;
+ this.organizationIdentifier = organizationIdentifier;
+
+ this.practitionerRoleSystem = practitionerRoleSystem;
+ this.practitionerRoleCode = practitionerRoleCode;
+ }
+
+ private boolean needsPractitionerRole()
+ {
+ return practitionerRoleSystem != null && practitionerRoleCode != null;
+ }
+
+ @Override
+ public boolean isRequesterAuthorized(Identity requester, Stream requesterAffiliations)
+ {
+ return isAuthorized(requester);
+ }
+
+ @Override
+ public boolean isRecipientAuthorized(Identity recipient, Stream recipientAffiliations)
+ {
+ return isAuthorized(recipient);
+ }
+
+ private boolean isAuthorized(Identity identity)
+ {
+ return identity != null && identity.getOrganization() != null && identity.getOrganization().getActive()
+ && identity.isLocalIdentity() == localIdentity && hasOrganizationIdentifier(identity.getOrganization())
+ && ((needsPractitionerRole() && hasPractitionerRole(getPractitionerRoles(identity)))
+ || (!needsPractitionerRole() && identity instanceof OrganizationIdentity));
+ }
+
+ private boolean hasOrganizationIdentifier(org.hl7.fhir.r4.model.Organization organization)
+ {
+ return organization.getIdentifier().stream().filter(Identifier::hasSystem).filter(Identifier::hasValue)
+ .filter(i -> ReadAccessHelper.ORGANIZATION_IDENTIFIER_SYSTEM.equals(i.getSystem()))
+ .anyMatch(i -> organizationIdentifier.equals(i.getValue()));
+ }
+
+ private Set getPractitionerRoles(Identity identity)
+ {
+ if (identity instanceof PractitionerIdentity p)
+ return p.getPractionerRoles();
+ else
+ return Collections.emptySet();
+ }
+
+ private boolean hasPractitionerRole(Set practitionerRoles)
+ {
+ return practitionerRoles.stream().anyMatch(
+ c -> practitionerRoleSystem.equals(c.getSystem()) && practitionerRoleCode.equals(c.getCode()));
+ }
+
+ @Override
+ public Extension toRecipientExtension()
+ {
+ return new Extension().setUrl(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT)
+ .setValue(toCoding(false));
+ }
+
+ @Override
+ public Extension toRequesterExtension()
+ {
+ return new Extension().setUrl(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_REQUESTER)
+ .setValue(toCoding(needsPractitionerRole()));
+ }
+
+ private Coding toCoding(boolean needsPractitionerRole)
+ {
+ Identifier organization = new Reference().getIdentifier()
+ .setSystem(ProcessAuthorizationHelper.ORGANIZATION_IDENTIFIER_SYSTEM).setValue(organizationIdentifier);
+
+ Coding coding = getProcessAuthorizationCode();
+
+ if (needsPractitionerRole)
+ {
+ Extension extension = coding.addExtension()
+ .setUrl(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER);
+ extension.addExtension(
+ ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_ORGANIZATION,
+ organization);
+ extension.addExtension(
+ ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_PRACTITIONER_ROLE,
+ new Coding(practitionerRoleSystem, practitionerRoleCode, null));
+ }
+ else
+ {
+ coding.addExtension().setUrl(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION)
+ .setValue(organization);
+ }
+
+ return coding;
+ }
+
+ @Override
+ public Coding getProcessAuthorizationCode()
+ {
+ if (localIdentity)
+ {
+ if (needsPractitionerRole())
+ return new Coding(ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM,
+ ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION_PRACTITIONER, null);
+ else
+ return new Coding(ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM,
+ ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION, null);
+ }
+ else
+ return new Coding(ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM,
+ ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ORGANIZATION, null);
+ }
+
+ @Override
+ public boolean requesterMatches(Extension requesterExtension)
+ {
+ return matches(requesterExtension, ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_REQUESTER,
+ needsPractitionerRole());
+ }
+
+ @Override
+ public boolean recipientMatches(Extension recipientExtension)
+ {
+ return matches(recipientExtension, ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT, false);
+ }
+
+ private boolean matches(Extension extension, String url, boolean needsPractitionerRole)
+ {
+ return extension != null && url.equals(extension.getUrl()) && extension.hasValue()
+ && extension.getValue() instanceof Coding value && matches(value) && value.hasExtension()
+ && hasMatchingOrganizationExtension(value.getExtension(), needsPractitionerRole);
+ }
+
+ private boolean hasMatchingOrganizationExtension(List extensions, boolean needsPractitionerRole)
+ {
+ return extensions.stream().anyMatch(organizationExtensionMatches(needsPractitionerRole));
+ }
+
+ private Predicate organizationExtensionMatches(boolean needsPractitionerRole)
+ {
+ if (needsPractitionerRole)
+ {
+ return extension -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER
+ .equals(extension.getUrl()) && !extension.hasValue()
+ && hasMatchingSubOrganizationExtension(extension.getExtension())
+ && hasMatchingPractitionerExtension(extension.getExtension());
+ }
+ else
+ {
+ return extension -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION
+ .equals(extension.getUrl()) && extension.hasValue()
+ && extension.getValue() instanceof Identifier value && organizationIdentifierMatches(value);
+ }
+ }
+
+ private boolean organizationIdentifierMatches(Identifier identifier)
+ {
+ return identifier != null && identifier.hasSystem() && identifier.hasValue()
+ && ProcessAuthorizationHelper.ORGANIZATION_IDENTIFIER_SYSTEM.equals(identifier.getSystem())
+ && organizationIdentifier.equals(identifier.getValue());
+ }
+
+ private boolean hasMatchingSubOrganizationExtension(List extensions)
+ {
+ return extensions.stream().anyMatch(this::subOrganizationExtensionMatches);
+ }
+
+ private boolean subOrganizationExtensionMatches(Extension extension)
+ {
+ return ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_ORGANIZATION
+ .equals(extension.getUrl()) && extension.hasValue() && extension.getValue() instanceof Identifier value
+ && organizationIdentifierMatches(value);
+ }
+
+ private boolean hasMatchingPractitionerExtension(List extensions)
+ {
+ return extensions.stream().anyMatch(this::practitionerExtensionMatches);
+ }
+
+ private boolean practitionerExtensionMatches(Extension extension)
+ {
+ return ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_PRACTITIONER_ROLE
+ .equals(extension.getUrl()) && extension.hasValue() && extension.getValue() instanceof Coding value
+ && practitionerRoleMatches(value);
+ }
+
+ private boolean practitionerRoleMatches(Coding coding)
+ {
+ return coding != null && coding.hasSystem() && coding.hasCode()
+ && practitionerRoleSystem.equals(coding.getSystem()) && practitionerRoleCode.equals(coding.getCode());
+ }
+
+ @Override
+ public boolean matches(Coding processAuthorizationCode)
+ {
+ if (localIdentity)
+ if (needsPractitionerRole())
+ return processAuthorizationCode != null
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM
+ .equals(processAuthorizationCode.getSystem())
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION_PRACTITIONER
+ .equals(processAuthorizationCode.getCode());
+ else
+ return processAuthorizationCode != null
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM
+ .equals(processAuthorizationCode.getSystem())
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION
+ .equals(processAuthorizationCode.getCode());
+ else
+ return processAuthorizationCode != null
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM
+ .equals(processAuthorizationCode.getSystem())
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ORGANIZATION
+ .equals(processAuthorizationCode.getCode());
+ }
+
+ public static Optional fromRequester(Coding coding, Predicate practitionerRoleExists,
+ Predicate organizationWithIdentifierExists)
+ {
+ if (coding != null && coding.hasSystem()
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM.equals(coding.getSystem())
+ && coding.hasCode())
+ {
+ if (ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION.equals(coding.getCode()))
+ return from(true, coding, organizationWithIdentifierExists).map(r -> (Requester) r);
+ else if (ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ORGANIZATION
+ .equals(coding.getCode()))
+ return from(false, coding, organizationWithIdentifierExists).map(r -> (Requester) r);
+ else if (ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION_PRACTITIONER
+ .equals(coding.getCode()))
+ return fromPractitionerRequester(coding, practitionerRoleExists, organizationWithIdentifierExists);
+ }
+
+ return Optional.empty();
+ }
+
+ public static Optional fromRecipient(Coding coding,
+ Predicate organizationWithIdentifierExists)
+ {
+ if (coding != null && coding.hasSystem()
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM.equals(coding.getSystem())
+ && coding.hasCode()
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION.equals(coding.getCode()))
+ {
+ return from(true, coding, organizationWithIdentifierExists).map(r -> (Recipient) r);
+ }
+
+ return Optional.empty();
+ }
+
+ private static Optional super Organization> from(boolean localIdentity, Coding coding,
+ Predicate organizationWithIdentifierExists)
+ {
+ if (coding != null && coding.hasExtension())
+ {
+ List organizations = coding.getExtension().stream().filter(Extension::hasUrl).filter(
+ e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION.equals(e.getUrl()))
+ .collect(Collectors.toList());
+ if (organizations.size() == 1)
+ {
+ Extension organization = organizations.get(0);
+ if (organization.hasValue() && organization.getValue() instanceof Identifier identifier
+ && ProcessAuthorizationHelper.ORGANIZATION_IDENTIFIER_SYSTEM.equals(identifier.getSystem())
+ && organizationWithIdentifierExists.test(identifier))
+ {
+ return Optional.of(new Organization(localIdentity, identifier.getValue(), null, null));
+ }
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ private static Optional fromPractitionerRequester(Coding coding,
+ Predicate practitionerRoleExists, Predicate organizationWithIdentifierExists)
+ {
+ if (coding != null && coding.hasExtension())
+ {
+ List organizationPractitioners = coding.getExtension().stream().filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER
+ .equals(e.getUrl()))
+ .collect(Collectors.toList());
+ if (organizationPractitioners.size() == 1)
+ {
+ Extension organizationPractitioner = organizationPractitioners.get(0);
+ List organizations = organizationPractitioner.getExtension().stream()
+ .filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_ORGANIZATION
+ .equals(e.getUrl()))
+ .collect(Collectors.toList());
+ List practitionerRoles = organizationPractitioner.getExtension().stream()
+ .filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_PRACTITIONER_ROLE
+ .equals(e.getUrl()))
+ .collect(Collectors.toList());
+ if (organizations.size() == 1 && practitionerRoles.size() == 1)
+ {
+ Extension organization = organizations.get(0);
+ Extension practitionerRole = practitionerRoles.get(0);
+
+ if (organization.hasValue() && organization.getValue() instanceof Identifier organizationIdentifier
+ && practitionerRole.hasValue()
+ && practitionerRole.getValue() instanceof Coding practitionerRoleCoding
+ && ProcessAuthorizationHelper.ORGANIZATION_IDENTIFIER_SYSTEM
+ .equals(organizationIdentifier.getSystem())
+ && organizationWithIdentifierExists.test(organizationIdentifier)
+ && practitionerRoleExists.test(practitionerRoleCoding))
+ {
+ return Optional.of(new Organization(true, organizationIdentifier.getValue(),
+ practitionerRoleCoding.getSystem(), practitionerRoleCoding.getCode()));
+ }
+ }
+ }
+ }
+
+ return Optional.empty();
+ }
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelper.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelper.java
new file mode 100644
index 000000000..f1490301b
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelper.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization.process;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.hl7.fhir.r4.model.ActivityDefinition;
+import org.hl7.fhir.r4.model.CanonicalType;
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.Identifier;
+
+public interface ProcessAuthorizationHelper
+{
+ String PROCESS_AUTHORIZATION_SYSTEM = "http://dsf.dev/fhir/CodeSystem/process-authorization";
+ String PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION = "LOCAL_ORGANIZATION";
+ String PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION_PRACTITIONER = "LOCAL_ORGANIZATION_PRACTITIONER";
+ String PROCESS_AUTHORIZATION_VALUE_REMOTE_ORGANIZATION = "REMOTE_ORGANIZATION";
+ String PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE = "LOCAL_ROLE";
+ String PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE_PRACTITIONER = "LOCAL_ROLE_PRACTITIONER";
+ String PROCESS_AUTHORIZATION_VALUE_REMOTE_ROLE = "REMOTE_ROLE";
+ String PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL = "LOCAL_ALL";
+ String PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL_PRACTITIONER = "LOCAL_ALL_PRACTITIONER";
+ String PROCESS_AUTHORIZATION_VALUE_REMOTE_ALL = "REMOTE_ALL";
+
+ String ORGANIZATION_IDENTIFIER_SYSTEM = "http://dsf.dev/sid/organization-identifier";
+
+ String EXTENSION_PROCESS_AUTHORIZATION = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization";
+ String EXTENSION_PROCESS_AUTHORIZATION_MESSAGE_NAME = "message-name";
+ String EXTENSION_PROCESS_AUTHORIZATION_TASK_PROFILE = "task-profile";
+ String EXTENSION_PROCESS_AUTHORIZATION_REQUESTER = "requester";
+ String EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT = "recipient";
+
+ String EXTENSION_PROCESS_AUTHORIZATION_PRACTITIONER = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization-practitioner";
+
+ String EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization-organization";
+
+ String EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization-organization-practitioner";
+ String EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_ORGANIZATION = "organization";
+ String EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_PRACTITIONER_ROLE = "practitioner-role";
+
+ String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization-parent-organization-role";
+ String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION = "parent-organization";
+ String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE = "organization-role";
+
+ String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization-parent-organization-role-practitioner";
+ String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER_PARENT_ORGANIZATION = EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION;
+ String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER_ORGANIZATION_ROLE = EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE;
+ String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER_PRACTITIONER_ROLE = "practitioner-role";
+
+
+ ActivityDefinition add(ActivityDefinition activityDefinition, String messageName, String taskProfile,
+ Requester requester, Recipient recipient);
+
+ ActivityDefinition add(ActivityDefinition activityDefinition, String messageName, String taskProfile,
+ Collection extends Requester> requesters, Collection extends Recipient> recipients);
+
+ boolean isValid(ActivityDefinition activityDefinition, Predicate profileExists,
+ Predicate practitionerRoleExists, Predicate organizationWithIdentifierExists,
+ Predicate organizationRoleExists);
+
+ default Stream getRequesters(ActivityDefinition activityDefinition, String processUrl,
+ String processVersion, String messageName, String taskProfile)
+ {
+ return getRequesters(activityDefinition, processUrl, processVersion, messageName,
+ Collections.singleton(taskProfile));
+ }
+
+ Stream getRequesters(ActivityDefinition activityDefinition, String processUrl, String processVersion,
+ String messageName, Collection taskProfiles);
+
+ default Stream getRecipients(ActivityDefinition activityDefinition, String processUrl,
+ String processVersion, String messageName, String taskProfiles)
+ {
+ return getRecipients(activityDefinition, processUrl, processVersion, messageName,
+ Collections.singleton(taskProfiles));
+ }
+
+ Stream getRecipients(ActivityDefinition activityDefinition, String processUrl, String processVersion,
+ String messageName, Collection taskProfiles);
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelperImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelperImpl.java
new file mode 100644
index 000000000..eccceabd8
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelperImpl.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization.process;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.hl7.fhir.r4.model.ActivityDefinition;
+import org.hl7.fhir.r4.model.CanonicalType;
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.Extension;
+import org.hl7.fhir.r4.model.Identifier;
+import org.hl7.fhir.r4.model.StringType;
+
+public class ProcessAuthorizationHelperImpl implements ProcessAuthorizationHelper
+{
+ @Override
+ public ActivityDefinition add(ActivityDefinition activityDefinition, String messageName, String taskProfile,
+ Requester requester, Recipient recipient)
+ {
+ Objects.requireNonNull(activityDefinition, "activityDefinition");
+ Objects.requireNonNull(messageName, "messageName");
+ if (messageName.isBlank())
+ throw new IllegalArgumentException("messageName blank");
+ Objects.requireNonNull(taskProfile, "taskProfile");
+ if (taskProfile.isBlank())
+ throw new IllegalArgumentException("taskProfile blank");
+ Objects.requireNonNull(requester, "requester");
+ Objects.requireNonNull(recipient, "recipient");
+
+ Extension extension = getExtensionByMessageNameAndTaskProfile(activityDefinition, messageName, taskProfile);
+ if (!hasAuthorization(extension, requester))
+ extension.addExtension(requester.toRequesterExtension());
+ if (!hasAuthorization(extension, recipient))
+ extension.addExtension(recipient.toRecipientExtension());
+
+ return activityDefinition;
+ }
+
+ @Override
+ public ActivityDefinition add(ActivityDefinition activityDefinition, String messageName, String taskProfile,
+ Collection extends Requester> requesters, Collection extends Recipient> recipients)
+ {
+ Objects.requireNonNull(activityDefinition, "activityDefinition");
+ Objects.requireNonNull(messageName, "messageName");
+ if (messageName.isBlank())
+ throw new IllegalArgumentException("messageName blank");
+ Objects.requireNonNull(taskProfile, "taskProfile");
+ if (taskProfile.isBlank())
+ throw new IllegalArgumentException("taskProfile blank");
+ Objects.requireNonNull(requesters, "requesters");
+ if (requesters.isEmpty())
+ throw new IllegalArgumentException("requesters empty");
+ Objects.requireNonNull(recipients, "recipients");
+ if (recipients.isEmpty())
+ throw new IllegalArgumentException("recipients empty");
+
+ Extension extension = getExtensionByMessageNameAndTaskProfile(activityDefinition, messageName, taskProfile);
+ requesters.stream().filter(r -> !hasAuthorization(extension, r))
+ .forEach(r -> extension.addExtension(r.toRequesterExtension()));
+ recipients.stream().filter(r -> !hasAuthorization(extension, r))
+ .forEach(r -> extension.addExtension(r.toRecipientExtension()));
+
+ return activityDefinition;
+ }
+
+ private Extension getExtensionByMessageNameAndTaskProfile(ActivityDefinition a, String messageName,
+ String taskProfile)
+ {
+ return a.getExtension().stream().filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION.equals(e.getUrl()))
+ .filter(Extension::hasExtension)
+ .filter(e -> hasMessageName(e, messageName) && hasTaskProfileExact(e, taskProfile)).findFirst()
+ .orElseGet(() ->
+ {
+ Extension e = newExtension(messageName, taskProfile);
+ a.addExtension(e);
+ return e;
+ });
+ }
+
+ private boolean hasMessageName(Extension processAuthorization, String messageName)
+ {
+ return processAuthorization.getExtension().stream().filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_MESSAGE_NAME.equals(e.getUrl()))
+ .filter(Extension::hasValue).filter(e -> e.getValue() instanceof StringType)
+ .map(e -> (StringType) e.getValue()).anyMatch(s -> messageName.equals(s.getValueAsString()));
+ }
+
+ private boolean hasTaskProfileExact(Extension processAuthorization, String taskProfile)
+ {
+ return processAuthorization.getExtension().stream().filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_TASK_PROFILE.equals(e.getUrl()))
+ .filter(Extension::hasValue).filter(e -> e.getValue() instanceof CanonicalType)
+ .map(e -> (CanonicalType) e.getValue()).anyMatch(c -> taskProfile.equals(c.getValueAsString()));
+ }
+
+ private Extension newExtension(String messageName, String taskProfile)
+ {
+ Extension e = new Extension(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION);
+ e.addExtension(newMessageName(messageName));
+ e.addExtension(newTaskProfile(taskProfile));
+
+ return e;
+ }
+
+ private Extension newMessageName(String messageName)
+ {
+ return new Extension(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_MESSAGE_NAME)
+ .setValue(new StringType(messageName));
+ }
+
+ private Extension newTaskProfile(String taskProfile)
+ {
+ return new Extension(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_TASK_PROFILE)
+ .setValue(new CanonicalType(taskProfile));
+ }
+
+ private boolean hasAuthorization(Extension processAuthorization, Requester authorization)
+ {
+ return processAuthorization.getExtension().stream().anyMatch(authorization::requesterMatches);
+ }
+
+ private boolean hasAuthorization(Extension processAuthorization, Recipient authorization)
+ {
+ return processAuthorization.getExtension().stream().anyMatch(authorization::recipientMatches);
+ }
+
+ @Override
+ public boolean isValid(ActivityDefinition activityDefinition, Predicate profileExists,
+ Predicate practitionerRoleExists, Predicate organizationWithIdentifierExists,
+ Predicate organizationRoleExists)
+ {
+ if (activityDefinition == null)
+ return false;
+
+ List processAuthorizations = activityDefinition.getExtension().stream().filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION.equals(e.getUrl()))
+ .collect(Collectors.toList());
+
+ if (processAuthorizations.isEmpty())
+ return false;
+
+ return processAuthorizations.stream()
+ .map(e -> isProcessAuthorizationValid(e, profileExists, practitionerRoleExists,
+ organizationWithIdentifierExists, organizationRoleExists))
+ .allMatch(v -> v) && messageNamesUnique(processAuthorizations);
+ }
+
+ private boolean messageNamesUnique(List processAuthorizations)
+ {
+ return processAuthorizations.size() == processAuthorizations.stream().flatMap(e -> e.getExtension().stream()
+ .filter(mn -> EXTENSION_PROCESS_AUTHORIZATION_MESSAGE_NAME.equals(mn.getUrl())).map(Extension::getValue)
+ .map(v -> (StringType) v).map(StringType::getValueAsString).findFirst().stream()).distinct().count();
+ }
+
+ private boolean isProcessAuthorizationValid(Extension processAuthorization, Predicate profileExists,
+ Predicate practitionerRoleExists, Predicate organizationWithIdentifierExists,
+ Predicate organizationRoleExists)
+ {
+ if (processAuthorization == null
+ || !ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION.equals(processAuthorization.getUrl())
+ || !processAuthorization.hasExtension())
+ return false;
+
+ List messageNames = new ArrayList<>(), taskProfiles = new ArrayList<>(),
+ requesters = new ArrayList<>(), recipients = new ArrayList<>();
+ for (Extension extension : processAuthorization.getExtension())
+ {
+ if (extension.hasUrl())
+ {
+ switch (extension.getUrl())
+ {
+ case EXTENSION_PROCESS_AUTHORIZATION_MESSAGE_NAME:
+ messageNames.add(extension);
+ break;
+ case EXTENSION_PROCESS_AUTHORIZATION_TASK_PROFILE:
+ taskProfiles.add(extension);
+ break;
+ case EXTENSION_PROCESS_AUTHORIZATION_REQUESTER:
+ requesters.add(extension);
+ break;
+ case EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT:
+ recipients.add(extension);
+ break;
+ }
+ }
+ }
+
+ if (messageNames.size() != 1 || taskProfiles.size() != 1 || requesters.isEmpty() || recipients.isEmpty())
+ return false;
+
+ return isMessageNameValid(messageNames.get(0)) && isTaskProfileValid(taskProfiles.get(0), profileExists)
+ && isRequestersValid(requesters, practitionerRoleExists, organizationWithIdentifierExists,
+ organizationRoleExists)
+ && isRecipientsValid(recipients, organizationWithIdentifierExists, organizationRoleExists);
+ }
+
+ private boolean isMessageNameValid(Extension messageName)
+ {
+ if (messageName == null || !ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_MESSAGE_NAME
+ .equals(messageName.getUrl()))
+ return false;
+
+ return messageName.hasValue() && messageName.getValue() instanceof StringType value
+ && !value.getValueAsString().isBlank();
+ }
+
+ private boolean isTaskProfileValid(Extension taskProfile, Predicate profileExists)
+ {
+ if (taskProfile == null || !ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_TASK_PROFILE
+ .equals(taskProfile.getUrl()))
+ return false;
+
+ return taskProfile.hasValue() && taskProfile.getValue() instanceof CanonicalType value
+ && profileExists.test(value);
+ }
+
+ private boolean isRequestersValid(List requesters, Predicate practitionerRoleExists,
+ Predicate organizationWithIdentifierExists, Predicate organizationRoleExists)
+ {
+ return requesters.stream().allMatch(r -> isRequesterValid(r, practitionerRoleExists,
+ organizationWithIdentifierExists, organizationRoleExists));
+ }
+
+ private boolean isRequesterValid(Extension requester, Predicate practitionerRoleExists,
+ Predicate organizationWithIdentifierExists, Predicate organizationRoleExists)
+ {
+ if (requester == null
+ || !ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_REQUESTER.equals(requester.getUrl()))
+ return false;
+
+ if (requester.hasValue() && requester.getValue() instanceof Coding value)
+ {
+ return requesterFrom(value, practitionerRoleExists, organizationWithIdentifierExists,
+ organizationRoleExists).isPresent();
+ }
+
+ return false;
+ }
+
+ private Optional requesterFrom(Coding coding, Predicate practitionerRoleExists,
+ Predicate organizationWithIdentifierExists, Predicate organizatioRoleExists)
+ {
+ switch (coding.getCode())
+ {
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL:
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL_PRACTITIONER:
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ALL:
+ return All.fromRequester(coding, practitionerRoleExists);
+
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION:
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION_PRACTITIONER:
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ORGANIZATION:
+ return Organization.fromRequester(coding, practitionerRoleExists, organizationWithIdentifierExists);
+
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE:
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE_PRACTITIONER:
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ROLE:
+ return Role.fromRequester(coding, practitionerRoleExists, organizationWithIdentifierExists,
+ organizatioRoleExists);
+ }
+
+ return Optional.empty();
+ }
+
+ private boolean isRecipientsValid(List recipients,
+ Predicate organizationWithIdentifierExists, Predicate organizationRoleExists)
+ {
+ return recipients.stream()
+ .allMatch(r -> isRecipientValid(r, organizationWithIdentifierExists, organizationRoleExists));
+ }
+
+ private boolean isRecipientValid(Extension recipient, Predicate organizationWithIdentifierExists,
+ Predicate organizationRoleExists)
+ {
+ if (recipient == null
+ || !ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT.equals(recipient.getUrl()))
+ return false;
+
+ if (recipient.hasValue() && recipient.getValue() instanceof Coding value)
+ {
+ return recipientFrom(value, organizationWithIdentifierExists, organizationRoleExists).isPresent();
+ }
+
+ return false;
+ }
+
+ private Optional recipientFrom(Coding coding, Predicate organizationWithIdentifierExists,
+ Predicate organizationRoleExists)
+ {
+ return switch (coding.getCode())
+ {
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL -> All.fromRecipient(coding);
+
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION ->
+ Organization.fromRecipient(coding, organizationWithIdentifierExists);
+
+ case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE ->
+ Role.fromRecipient(coding, organizationWithIdentifierExists, organizationRoleExists);
+
+ default -> Optional.empty();
+ };
+ }
+
+ @Override
+ public Stream getRequesters(ActivityDefinition activityDefinition, String processUrl,
+ String processVersion, String messageName, Collection taskProfiles)
+ {
+ Optional authorizationExtension = getAuthorizationExtension(activityDefinition, processUrl,
+ processVersion, messageName, taskProfiles);
+
+ if (authorizationExtension.isEmpty())
+ return Stream.empty();
+ else
+ return authorizationExtension.get().getExtension().stream().filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_REQUESTER
+ .equals(e.getUrl()))
+ .filter(Extension::hasValue).filter(e -> e.getValue() instanceof Coding)
+ .map(e -> (Coding) e.getValue())
+ .flatMap(coding -> requesterFrom(coding, _ -> true, _ -> true, _ -> true).stream());
+ }
+
+ @Override
+ public Stream getRecipients(ActivityDefinition activityDefinition, String processUrl,
+ String processVersion, String messageName, Collection taskProfiles)
+ {
+ Optional authorizationExtension = getAuthorizationExtension(activityDefinition, processUrl,
+ processVersion, messageName, taskProfiles);
+
+ if (authorizationExtension.isEmpty())
+ return Stream.empty();
+ else
+ return authorizationExtension.get().getExtension().stream().filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT
+ .equals(e.getUrl()))
+ .filter(Extension::hasValue).filter(e -> e.getValue() instanceof Coding)
+ .map(e -> (Coding) e.getValue())
+ .flatMap(coding -> recipientFrom(coding, _ -> true, _ -> true).stream());
+ }
+
+ private Optional getAuthorizationExtension(ActivityDefinition activityDefinition, String processUrl,
+ String processVersion, String messageName, Collection taskProfiles)
+ {
+ if (activityDefinition == null || processUrl == null || processUrl.isBlank() || processVersion == null
+ || processVersion.isBlank() || messageName == null || messageName.isBlank() || taskProfiles == null)
+ return Optional.empty();
+
+ if (!processUrl.equals(activityDefinition.getUrl()) || !processVersion.equals(activityDefinition.getVersion()))
+ return Optional.empty();
+
+ Optional authorizationExtension = activityDefinition.getExtension().stream()
+ .filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION.equals(e.getUrl()))
+ .filter(Extension::hasExtension)
+ .filter(e -> hasMessageName(e, messageName) && hasTaskProfile(e, taskProfiles)).findFirst();
+ return authorizationExtension;
+ }
+
+ private boolean hasTaskProfile(Extension processAuthorization, Collection taskProfiles)
+ {
+ return taskProfiles.stream()
+ .anyMatch(taskProfile -> hasTaskProfileNotVersionSpecific(processAuthorization, taskProfile));
+ }
+
+ private boolean hasTaskProfileNotVersionSpecific(Extension processAuthorization, String taskProfile)
+ {
+ return processAuthorization.getExtension().stream().filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_TASK_PROFILE.equals(e.getUrl()))
+ .filter(Extension::hasValue).filter(e -> e.getValue() instanceof CanonicalType)
+ .map(e -> (CanonicalType) e.getValue())
+
+ // match if task profile is equal to value in activity definition
+ // or match if task profile is not version specific but value in activity definition is and non version
+ // specific profiles are same -> client does not care about version of task resource, may result in
+ // validation errors
+ .anyMatch(c -> taskProfile.equals(c.getValueAsString())
+ || taskProfile.equals(getBase(c.getValueAsString())));
+ }
+
+ private static String getBase(String canonicalUrl)
+ {
+ if (canonicalUrl.contains("|"))
+ {
+ String[] split = canonicalUrl.split("\\|");
+ return split[0];
+ }
+ else
+ return canonicalUrl;
+ }
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Recipient.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Recipient.java
new file mode 100644
index 000000000..9dea1d959
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Recipient.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization.process;
+
+import java.util.Collection;
+import java.util.stream.Stream;
+
+import org.hl7.fhir.r4.model.Extension;
+import org.hl7.fhir.r4.model.OrganizationAffiliation;
+
+import dev.dsf.common.auth.conf.Identity;
+
+public interface Recipient extends WithAuthorization
+{
+ static Recipient localAll()
+ {
+ return new All(true, null, null);
+ }
+
+ static Recipient localOrganization(String organizationIdentifier)
+ {
+ return new Organization(true, organizationIdentifier, null, null);
+ }
+
+ static Recipient localRole(String parentOrganizationIdentifier, String roleSystem, String roleCode)
+ {
+ return new Role(true, parentOrganizationIdentifier, roleSystem, roleCode, null, null);
+ }
+
+ boolean recipientMatches(Extension recipientExtension);
+
+ boolean isRecipientAuthorized(Identity recipientUser, Stream recipientAffiliations);
+
+ default boolean isRecipientAuthorized(Identity recipientUser,
+ Collection recipientAffiliations)
+ {
+ return isRecipientAuthorized(recipientUser,
+ recipientAffiliations == null ? null : recipientAffiliations.stream());
+ }
+
+ Extension toRecipientExtension();
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Requester.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Requester.java
new file mode 100644
index 000000000..bdba54a8a
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Requester.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization.process;
+
+import java.util.Collection;
+import java.util.stream.Stream;
+
+import org.hl7.fhir.r4.model.Extension;
+import org.hl7.fhir.r4.model.OrganizationAffiliation;
+
+import dev.dsf.common.auth.conf.Identity;
+
+public interface Requester extends WithAuthorization
+{
+ static Requester localAll()
+ {
+ return all(true, null, null);
+ }
+
+ static Requester localAllPractitioner(String practitionerRoleSystem, String practitionerRoleCode)
+ {
+ return all(true, practitionerRoleSystem, practitionerRoleCode);
+ }
+
+ static Requester remoteAll()
+ {
+ return all(false, null, null);
+ }
+
+ static Requester all(boolean localIdentity, String userRoleSystem, String userRoleCode)
+ {
+ return new All(localIdentity, userRoleSystem, userRoleCode);
+ }
+
+ static Requester localOrganization(String organizationIdentifier)
+ {
+ return organization(true, organizationIdentifier, null, null);
+ }
+
+ static Requester localOrganizationPractitioner(String organizationIdentifier, String practitionerRoleSystem,
+ String practitionerRoleCode)
+ {
+ return organization(true, organizationIdentifier, practitionerRoleSystem, practitionerRoleCode);
+ }
+
+ static Requester remoteOrganization(String organizationIdentifier)
+ {
+ return organization(false, organizationIdentifier, null, null);
+ }
+
+ static Requester organization(boolean localIdentity, String organizationIdentifier, String practitionerRoleSystem,
+ String practitionerRoleCode)
+ {
+ return new Organization(localIdentity, organizationIdentifier, practitionerRoleSystem, practitionerRoleCode);
+ }
+
+ static Requester localRole(String parentOrganizationIdentifier, String organizatioRoleSystem,
+ String organizatioRoleCode)
+ {
+ return role(true, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode, null, null);
+ }
+
+ static Requester localRolePractitioner(String parentOrganizationIdentifier, String organizatioRoleSystem,
+ String organizatioRoleCode, String practitionerRoleSystem, String practitionerRoleCode)
+ {
+ return role(true, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode,
+ practitionerRoleSystem, practitionerRoleCode);
+ }
+
+ static Requester remoteRole(String parentOrganizationIdentifier, String organizatioRoleSystem,
+ String organizatioRoleCode)
+ {
+ return role(false, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode, null, null);
+ }
+
+ static Requester role(boolean localIdentity, String parentOrganizationIdentifier, String organizatioRoleSystem,
+ String organizatioRoleCode, String practitionerRoleSystem, String practitionerRoleCode)
+ {
+ return new Role(localIdentity, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode,
+ practitionerRoleSystem, practitionerRoleCode);
+ }
+
+ boolean requesterMatches(Extension requesterExtension);
+
+ boolean isRequesterAuthorized(Identity requesterUser, Stream requesterAffiliations);
+
+ default boolean isRequesterAuthorized(Identity requesterUser,
+ Collection requesterAffiliations)
+ {
+ return isRequesterAuthorized(requesterUser,
+ requesterAffiliations == null ? null : requesterAffiliations.stream());
+ }
+
+ Extension toRequesterExtension();
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Role.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Role.java
new file mode 100644
index 000000000..48e7af68f
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/Role.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization.process;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.hl7.fhir.r4.model.CodeableConcept;
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.Extension;
+import org.hl7.fhir.r4.model.Identifier;
+import org.hl7.fhir.r4.model.OrganizationAffiliation;
+import org.hl7.fhir.r4.model.Reference;
+
+import dev.dsf.common.auth.conf.Identity;
+import dev.dsf.common.auth.conf.OrganizationIdentity;
+import dev.dsf.common.auth.conf.PractitionerIdentity;
+import dev.dsf.fhir.authorization.read.ReadAccessHelper;
+
+public class Role implements Recipient, Requester
+{
+ private final boolean localIdentity;
+ private final String parentOrganizationIdentifier;
+ private final String organizationRoleSystem;
+ private final String organizationRoleCode;
+
+ private final String practitionerRoleSystem;
+ private final String practitionerRoleCode;
+
+ public Role(boolean localIdentity, String parentOrganizationIdentifier, String organizatioRoleSystem,
+ String organizationRoleCode, String practitionerRoleSystem, String practitionerRoleCode)
+ {
+ Objects.requireNonNull(parentOrganizationIdentifier, "parentOrganizationIdentifier");
+ if (parentOrganizationIdentifier.isBlank())
+ throw new IllegalArgumentException("parentOrganizationIdentifier blank");
+ Objects.requireNonNull(organizatioRoleSystem, "organizatioRoleSystem");
+ if (organizatioRoleSystem.isBlank())
+ throw new IllegalArgumentException("organizatioRoleSystem blank");
+ Objects.requireNonNull(organizationRoleCode, "organizationRoleCode");
+ if (organizationRoleCode.isBlank())
+ throw new IllegalArgumentException("organizationRoleCode blank");
+
+ this.localIdentity = localIdentity;
+ this.parentOrganizationIdentifier = parentOrganizationIdentifier;
+ this.organizationRoleSystem = organizatioRoleSystem;
+ this.organizationRoleCode = organizationRoleCode;
+
+ this.practitionerRoleSystem = practitionerRoleSystem;
+ this.practitionerRoleCode = practitionerRoleCode;
+ }
+
+ private boolean needsPractitionerRole()
+ {
+ return practitionerRoleSystem != null && practitionerRoleCode != null;
+ }
+
+ @Override
+ public boolean isRequesterAuthorized(Identity requester, Stream requesterAffiliations)
+ {
+ return isAuthorized(requester, requesterAffiliations);
+ }
+
+ @Override
+ public boolean isRecipientAuthorized(Identity recipient, Stream recipientAffiliations)
+ {
+ return isAuthorized(recipient, recipientAffiliations);
+ }
+
+ private boolean isAuthorized(Identity identity, Stream affiliations)
+ {
+ return identity != null && identity.getOrganization() != null && identity.getOrganization().getActive()
+ && identity.isLocalIdentity() == localIdentity && affiliations != null
+ && hasParentOrganizationMemberRole(identity.getOrganization(), affiliations)
+ && ((needsPractitionerRole() && hasPractitionerRole(getPractitionerRoles(identity)))
+ || (!needsPractitionerRole() && identity instanceof OrganizationIdentity));
+ }
+
+ private boolean hasParentOrganizationMemberRole(org.hl7.fhir.r4.model.Organization recipientOrganization,
+ Stream affiliations)
+ {
+ return affiliations
+
+ // check affiliation active
+ .filter(OrganizationAffiliation::getActive)
+
+ // check parent-organization identifier
+ .filter(OrganizationAffiliation::hasOrganization).filter(a -> a.getOrganization().hasIdentifier())
+ .filter(a -> a.getOrganization().getIdentifier().hasSystem())
+ .filter(a -> a.getOrganization().getIdentifier().hasValue())
+ .filter(a -> ReadAccessHelper.ORGANIZATION_IDENTIFIER_SYSTEM
+ .equals(a.getOrganization().getIdentifier().getSystem()))
+ .filter(a -> parentOrganizationIdentifier.equals(a.getOrganization().getIdentifier().getValue()))
+
+ // check member identifier
+ .filter(OrganizationAffiliation::hasParticipatingOrganization)
+ .filter(a -> a.getParticipatingOrganization().hasIdentifier())
+ .filter(a -> a.getParticipatingOrganization().getIdentifier().hasSystem())
+ .filter(a -> a.getParticipatingOrganization().getIdentifier().hasValue()).filter(a ->
+ {
+ final Identifier memberIdentifier = a.getParticipatingOrganization().getIdentifier();
+ return recipientOrganization.getIdentifier().stream().filter(Identifier::hasSystem)
+ .filter(Identifier::hasValue)
+ .anyMatch(i -> i.getSystem().equals(memberIdentifier.getSystem())
+ && i.getValue().equals(memberIdentifier.getValue()));
+ })
+
+ // check role
+ .filter(OrganizationAffiliation::hasCode).flatMap(a -> a.getCode().stream())
+ .filter(CodeableConcept::hasCoding).flatMap(c -> c.getCoding().stream()).filter(Coding::hasSystem)
+ .filter(Coding::hasCode).anyMatch(
+ c -> c.getSystem().equals(organizationRoleSystem) && c.getCode().equals(organizationRoleCode));
+ }
+
+ private Set getPractitionerRoles(Identity identity)
+ {
+ if (identity instanceof PractitionerIdentity p)
+ return p.getPractionerRoles();
+ else
+ return Collections.emptySet();
+ }
+
+ private boolean hasPractitionerRole(Set practitionerRoles)
+ {
+ return practitionerRoles.stream().anyMatch(
+ c -> practitionerRoleSystem.equals(c.getSystem()) && practitionerRoleCode.equals(c.getCode()));
+ }
+
+ @Override
+ public Extension toRecipientExtension()
+ {
+ return new Extension().setUrl(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT)
+ .setValue(toCoding(false));
+ }
+
+ @Override
+ public Extension toRequesterExtension()
+ {
+ return new Extension().setUrl(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_REQUESTER)
+ .setValue(toCoding(needsPractitionerRole()));
+ }
+
+ private Coding toCoding(boolean needsPractitionerRole)
+ {
+ Identifier parentOrganization = new Reference().getIdentifier()
+ .setSystem(ProcessAuthorizationHelper.ORGANIZATION_IDENTIFIER_SYSTEM)
+ .setValue(parentOrganizationIdentifier);
+ Extension parentOrganizationExt = new Extension(
+ ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION,
+ parentOrganization);
+
+ Coding organizationRole = new Coding(organizationRoleSystem, organizationRoleCode, null);
+ Extension organizationRoleExt = new Extension(
+ ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE,
+ organizationRole);
+
+ Coding coding = getProcessAuthorizationCode();
+
+ if (needsPractitionerRole)
+ {
+ Extension practitionerRoleExt = new Extension(
+ ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER_PRACTITIONER_ROLE,
+ new Coding(practitionerRoleSystem, practitionerRoleCode, null));
+
+ coding.addExtension().setUrl(
+ ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER)
+ .addExtension(parentOrganizationExt).addExtension(organizationRoleExt)
+ .addExtension(practitionerRoleExt);
+ }
+ else
+ {
+ coding.addExtension()
+ .setUrl(ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE)
+ .addExtension(parentOrganizationExt).addExtension(organizationRoleExt);
+ }
+
+ return coding;
+ }
+
+ @Override
+ public Coding getProcessAuthorizationCode()
+ {
+ if (localIdentity)
+ {
+ if (needsPractitionerRole())
+ return new Coding(ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM,
+ ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE_PRACTITIONER, null);
+ else
+ return new Coding(ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM,
+ ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE, null);
+ }
+ else
+ return new Coding(ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM,
+ ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ROLE, null);
+ }
+
+ @Override
+ public boolean requesterMatches(Extension requesterExtension)
+ {
+ return matches(requesterExtension, ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_REQUESTER,
+ needsPractitionerRole());
+ }
+
+ @Override
+ public boolean recipientMatches(Extension recipientExtension)
+ {
+ return matches(recipientExtension, ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT, false);
+ }
+
+ private boolean matches(Extension extension, String url, boolean needsPractitionerRole)
+ {
+ return extension != null && url.equals(extension.getUrl()) && extension.hasValue()
+ && extension.getValue() instanceof Coding value && matches(value) && value.hasExtension()
+ && hasMatchingParentOrganizationRoleExtension(value.getExtension(), needsPractitionerRole);
+ }
+
+ private boolean hasMatchingParentOrganizationRoleExtension(List extension, boolean needsPractitionerRole)
+ {
+ return extension.stream().anyMatch(parentOrganizationRoleExtensionMatches(needsPractitionerRole));
+ }
+
+ private Predicate parentOrganizationRoleExtensionMatches(boolean needsPractitionerRole)
+ {
+ if (needsPractitionerRole)
+ {
+ return extension -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER
+ .equals(extension.getUrl()) && extension.hasExtension()
+ && hasMatchingParentOrganizationExtension(extension.getExtension())
+ && hasMatchingOrganizationRoleExtension(extension.getExtension())
+ && hasMatchingPractitionerRoleExtension(extension.getExtension());
+ }
+ else
+ {
+ return extension -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE
+ .equals(extension.getUrl()) && extension.hasExtension()
+ && hasMatchingParentOrganizationExtension(extension.getExtension())
+ && hasMatchingOrganizationRoleExtension(extension.getExtension());
+ }
+ }
+
+ private boolean hasMatchingParentOrganizationExtension(List extensions)
+ {
+ return extensions.stream().anyMatch(this::parentOrganizationExtensionMatches);
+ }
+
+ private boolean parentOrganizationExtensionMatches(Extension extension)
+ {
+ return ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION
+ .equals(extension.getUrl()) && extension.hasValue() && extension.getValue() instanceof Identifier value
+ && parentOrganizationIdentifierMatches(value);
+ }
+
+ private boolean parentOrganizationIdentifierMatches(Identifier identifier)
+ {
+ return identifier != null && identifier.hasSystem() && identifier.hasValue()
+ && ProcessAuthorizationHelper.ORGANIZATION_IDENTIFIER_SYSTEM.equals(identifier.getSystem())
+ && parentOrganizationIdentifier.equals(identifier.getValue());
+ }
+
+ private boolean hasMatchingOrganizationRoleExtension(List extensions)
+ {
+ return extensions.stream().anyMatch(this::organizationRoleExtensionMatches);
+ }
+
+ private boolean organizationRoleExtensionMatches(Extension extension)
+ {
+ return ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE
+ .equals(extension.getUrl()) && extension.hasValue() && extension.getValue() instanceof Coding value
+ && organizationRoleMatches(value);
+ }
+
+ private boolean organizationRoleMatches(Coding coding)
+ {
+ return coding != null && coding.hasSystem() && coding.hasCode()
+ && organizationRoleSystem.equals(coding.getSystem()) && organizationRoleCode.equals(coding.getCode());
+ }
+
+ private boolean hasMatchingPractitionerRoleExtension(List extensions)
+ {
+ return extensions.stream().anyMatch(this::practitionerRoleExtensionMatches);
+ }
+
+ private boolean practitionerRoleExtensionMatches(Extension extension)
+ {
+ return ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER_PRACTITIONER_ROLE
+ .equals(extension.getUrl()) && extension.hasValue() && extension.getValue() instanceof Coding value
+ && practitionerRoleMatches(value);
+ }
+
+ private boolean practitionerRoleMatches(Coding coding)
+ {
+ return coding != null && coding.hasSystem() && coding.hasCode()
+ && practitionerRoleSystem.equals(coding.getSystem()) && practitionerRoleCode.equals(coding.getCode());
+ }
+
+ @Override
+ public boolean matches(Coding processAuthorizationCode)
+ {
+ if (localIdentity)
+ if (needsPractitionerRole())
+ return processAuthorizationCode != null
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM
+ .equals(processAuthorizationCode.getSystem())
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE_PRACTITIONER
+ .equals(processAuthorizationCode.getCode());
+ else
+ return processAuthorizationCode != null
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM
+ .equals(processAuthorizationCode.getSystem())
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE
+ .equals(processAuthorizationCode.getCode());
+ else
+ return processAuthorizationCode != null
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM
+ .equals(processAuthorizationCode.getSystem())
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ROLE
+ .equals(processAuthorizationCode.getCode());
+ }
+
+ public static Optional fromRequester(Coding coding, Predicate practitionerRoleExists,
+ Predicate organizationWithIdentifierExists, Predicate organizationRoleExists)
+ {
+ if (coding != null && coding.hasSystem()
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM.equals(coding.getSystem())
+ && coding.hasCode())
+ {
+ if (ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE.equals(coding.getCode()))
+ return from(true, coding, organizationWithIdentifierExists, organizationRoleExists)
+ .map(r -> (Requester) r);
+ else if (ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_REMOTE_ROLE.equals(coding.getCode()))
+ return from(false, coding, organizationWithIdentifierExists, organizationRoleExists)
+ .map(r -> (Requester) r);
+ else if (ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE_PRACTITIONER
+ .equals(coding.getCode()))
+ return fromPractitionerRequester(coding, practitionerRoleExists, organizationWithIdentifierExists,
+ organizationRoleExists);
+ }
+
+ return Optional.empty();
+ }
+
+ public static Optional fromRecipient(Coding coding,
+ Predicate organizationWithIdentifierExists, Predicate organizationRoleExists)
+ {
+ if (coding != null && coding.hasSystem()
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_SYSTEM.equals(coding.getSystem())
+ && coding.hasCode()
+ && ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE.equals(coding.getCode()))
+ {
+ return from(true, coding, organizationWithIdentifierExists, organizationRoleExists).map(r -> (Recipient) r);
+ }
+
+ return Optional.empty();
+ }
+
+ private static Optional from(boolean localIdentity, Coding coding,
+ Predicate organizationWithIdentifierExists, Predicate organizationRoleExists)
+ {
+ if (coding != null && coding.hasExtension())
+ {
+ List parentOrganizationRoles = coding.getExtension().stream().filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE
+ .equals(e.getUrl()))
+ .collect(Collectors.toList());
+
+ if (parentOrganizationRoles.size() == 1)
+ {
+ Extension parentOrganizationRole = parentOrganizationRoles.get(0);
+ List parentOrganizations = parentOrganizationRole.getExtension().stream()
+ .filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION
+ .equals(e.getUrl()))
+ .collect(Collectors.toList());
+ List organizationRoles = parentOrganizationRole.getExtension().stream()
+ .filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE
+ .equals(e.getUrl()))
+ .collect(Collectors.toList());
+
+ if (parentOrganizations.size() == 1 && organizationRoles.size() == 1)
+ {
+ Extension parentOrganization = parentOrganizations.get(0);
+ Extension organizationRole = organizationRoles.get(0);
+
+ if (parentOrganization.hasValue()
+ && parentOrganization.getValue() instanceof Identifier parentOrganizationIdentifier
+ && organizationRole.hasValue()
+ && organizationRole.getValue() instanceof Coding organizationRoleCoding
+ && ProcessAuthorizationHelper.ORGANIZATION_IDENTIFIER_SYSTEM
+ .equals(parentOrganizationIdentifier.getSystem())
+ && organizationWithIdentifierExists.test(parentOrganizationIdentifier)
+ && organizationRoleExists.test(organizationRoleCoding))
+ {
+ return Optional.of(new Role(localIdentity, parentOrganizationIdentifier.getValue(),
+ organizationRoleCoding.getSystem(), organizationRoleCoding.getCode(), null, null));
+ }
+ }
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ private static Optional fromPractitionerRequester(Coding coding,
+ Predicate practitionerRoleExists, Predicate organizationWithIdentifierExists,
+ Predicate organizationRoleExists)
+ {
+ if (coding != null && coding.hasExtension())
+ {
+ List parentOrganizationRolePractitioners = coding.getExtension().stream()
+ .filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER
+ .equals(e.getUrl()))
+ .collect(Collectors.toList());
+
+ if (parentOrganizationRolePractitioners.size() == 1)
+ {
+ Extension parentOrganizationRolePractitioner = parentOrganizationRolePractitioners.get(0);
+ List parentOrganizations = parentOrganizationRolePractitioner.getExtension().stream()
+ .filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION
+ .equals(e.getUrl()))
+ .collect(Collectors.toList());
+ List organizationRoles = parentOrganizationRolePractitioner.getExtension().stream()
+ .filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE
+ .equals(e.getUrl()))
+ .collect(Collectors.toList());
+ List practitionerRoles = parentOrganizationRolePractitioner.getExtension().stream()
+ .filter(Extension::hasUrl)
+ .filter(e -> ProcessAuthorizationHelper.EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER_PRACTITIONER_ROLE
+ .equals(e.getUrl()))
+ .collect(Collectors.toList());
+
+ if (parentOrganizations.size() == 1 && organizationRoles.size() == 1 && practitionerRoles.size() == 1)
+ {
+ Extension parentOrganization = parentOrganizations.get(0);
+ Extension organizationRole = organizationRoles.get(0);
+ Extension practitionerRole = practitionerRoles.get(0);
+
+ if (parentOrganization.hasValue()
+ && parentOrganization.getValue() instanceof Identifier parentOrganizationIdentifier
+ && organizationRole.hasValue()
+ && organizationRole.getValue() instanceof Coding organizationRoleCoding
+ && practitionerRole.hasValue()
+ && practitionerRole.getValue() instanceof Coding practitionerRoleCoding
+ && ProcessAuthorizationHelper.ORGANIZATION_IDENTIFIER_SYSTEM
+ .equals(parentOrganizationIdentifier.getSystem())
+ && organizationWithIdentifierExists.test(parentOrganizationIdentifier)
+ && organizationRoleExists.test(organizationRoleCoding)
+ && practitionerRoleExists.test(practitionerRoleCoding))
+ {
+ return Optional.of(new Role(true, parentOrganizationIdentifier.getValue(),
+ organizationRoleCoding.getSystem(), organizationRoleCoding.getCode(),
+ practitionerRoleCoding.getSystem(), practitionerRoleCoding.getCode()));
+ }
+ }
+ }
+ }
+
+ return Optional.empty();
+ }
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/WithAuthorization.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/WithAuthorization.java
new file mode 100644
index 000000000..dc19ae599
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/process/WithAuthorization.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization.process;
+
+import org.hl7.fhir.r4.model.Coding;
+
+public interface WithAuthorization
+{
+ Coding getProcessAuthorizationCode();
+
+ boolean matches(Coding processAuthorizationCode);
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/read/ReadAccessHelper.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/read/ReadAccessHelper.java
new file mode 100644
index 000000000..6050033c2
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/authorization/read/ReadAccessHelper.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.authorization.read;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+import org.hl7.fhir.r4.model.Coding;
+import org.hl7.fhir.r4.model.Identifier;
+import org.hl7.fhir.r4.model.Organization;
+import org.hl7.fhir.r4.model.OrganizationAffiliation;
+import org.hl7.fhir.r4.model.Resource;
+
+/**
+ * Helper with methods to configure read access to FHIR resources.
+ */
+public interface ReadAccessHelper
+{
+ String READ_ACCESS_TAG_SYSTEM = "http://dsf.dev/fhir/CodeSystem/read-access-tag";
+ String READ_ACCESS_TAG_VALUE_LOCAL = "LOCAL";
+ String READ_ACCESS_TAG_VALUE_ORGANIZATION = "ORGANIZATION";
+ String READ_ACCESS_TAG_VALUE_ROLE = "ROLE";
+ String READ_ACCESS_TAG_VALUE_ALL = "ALL";
+
+ String ORGANIZATION_IDENTIFIER_SYSTEM = "http://dsf.dev/sid/organization-identifier";
+
+ String EXTENSION_READ_ACCESS_ORGANIZATION = "http://dsf.dev/fhir/StructureDefinition/extension-read-access-organization";
+
+ String EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE = "http://dsf.dev/fhir/StructureDefinition/extension-read-access-parent-organization-role";
+ String EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION = "parent-organization";
+ String EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE = "organization-role";
+
+ /**
+ * Adds LOCAL tag. Removes ALL tag if present.
+ *
+ * @param
+ * the resource type
+ * @param resource
+ * may be null
+ * @return null if given resource is null
+ * @see #addAll(Resource)
+ */
+ R addLocal(R resource);
+
+ /**
+ * Adds ORGANIZATION tag for the given organization. Adds LOCAL tag if not present, removes ALL tag if present.
+ *
+ * @param
+ * the resource type
+ * @param resource
+ * may be null
+ * @param organizationIdentifier
+ * not null
+ * @return null if given resource is null
+ * @see #addLocal(Resource)
+ * @see #addOrganization(Resource, Organization)
+ */
+ R addOrganization(R resource, String organizationIdentifier);
+
+ /**
+ * Adds ORGANIZATION tag for the given organization. Adds LOCAL tag if not present, removes ALL tag if present.
+ *
+ * @param
+ * the resource type
+ * @param resource
+ * may be null
+ * @param organization
+ * not null
+ * @return null if given resource is null
+ * @throws NullPointerException
+ * if given organization is null
+ * @throws IllegalArgumentException
+ * if given organization does not have valid identifier
+ * @see #addLocal(Resource)
+ * @see #addOrganization(Resource, String)
+ */
+ R addOrganization(R resource, Organization organization);
+
+ /**
+ * Adds ROLE tag for the given affiliation. Adds LOCAL tag if not present, removes ALL tag if present.
+ *
+ * @param
+ * the resource type
+ * @param resource
+ * may be null
+ * @param consortiumIdentifier
+ * not null
+ * @param roleSystem
+ * not null
+ * @param roleCode
+ * not null
+ * @return null if given resource is null
+ * @see #addLocal(Resource)
+ * @see #addRole(Resource, OrganizationAffiliation)
+ */
+ R addRole(R resource, String consortiumIdentifier, String roleSystem, String roleCode);
+
+ /**
+ * Adds ROLE tag for the given affiliation. Adds LOCAL tag if not present, removes ALL tag if present.
+ *
+ * @param
+ * the resource type
+ * @param resource
+ * may be null
+ * @param affiliation
+ * not null
+ * @return null if given resource is null
+ * @throws NullPointerException
+ * if given affiliation is null
+ * @throws IllegalArgumentException
+ * if given affiliation does not have valid consortium identifier or organization role (only one
+ * role supported)
+ * @see #addLocal(Resource)
+ * @see #addRole(Resource, String, String, String)
+ */
+ R addRole(R resource, OrganizationAffiliation affiliation);
+
+ /**
+ * Adds All tag. Removes LOCAL, ORGANIZATION and ROLE tags if present.
+ *
+ * @param
+ * the resource type
+ * @param resource
+ * may be null
+ * @return null if given resource is null
+ * @see #addLocal(Resource)
+ * @see #addOrganization(Resource, String)
+ * @see #addRole(Resource, String, String, String)
+ */
+ R addAll(R resource);
+
+ boolean hasLocal(Resource resource);
+
+ boolean hasOrganization(Resource resource, String organizationIdentifier);
+
+ boolean hasOrganization(Resource resource, Organization organization);
+
+ boolean hasAnyOrganization(Resource resource);
+
+ boolean hasRole(Resource resource, String consortiumIdentifier, String roleSystem, String roleCode);
+
+ boolean hasRole(Resource resource, OrganizationAffiliation affiliation);
+
+ boolean hasRole(Resource resource, List affiliations);
+
+ boolean hasAnyRole(Resource resource);
+
+ boolean hasAll(Resource resource);
+
+ /**
+ * Resource with access tags valid if:
+ *
+ * 1 LOCAL tag and n {ORGANIZATION, ROLE} tags {@code (n >= 0)}
+ * or
+ * 1 ALL tag
+ *
+ * All tags {LOCAL, ORGANIZATION, ROLE, ALL} valid
+ *
+ * Does not check if referenced organizations or roles exist
+ *
+ * @param resource
+ * may be null
+ * @return false if given resource is null or resource not valid
+ */
+ boolean isValid(Resource resource);
+
+ /**
+ * Resource with access tags valid if:
+ *
+ * 1 LOCAL tag and n {ORGANIZATION, ROLE} tags {@code (n >= 0)}
+ * or
+ * 1 ALL tag
+ *
+ * All tags {LOCAL, ORGANIZATION, ROLE, ALL} valid
+ *
+ * @param resource
+ * may be null
+ * @param organizationWithIdentifierExists
+ * not null
+ * @param roleExists
+ * not null
+ * @return false if given resource is null or resource not valid
+ */
+ boolean isValid(Resource resource, Predicate organizationWithIdentifierExists,
+ Predicate roleExists);
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java
new file mode 100644
index 000000000..1f5a5cc4b
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.client;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+import org.hl7.fhir.r4.model.Bundle;
+import org.hl7.fhir.r4.model.CapabilityStatement;
+import org.hl7.fhir.r4.model.IdType;
+import org.hl7.fhir.r4.model.Resource;
+import org.hl7.fhir.r4.model.StructureDefinition;
+
+import jakarta.ws.rs.core.MediaType;
+
+public interface BasicFhirWebserviceClient extends PreferReturnResource
+{
+ void delete(Class extends Resource> resourceClass, String id);
+
+ void deleteConditionaly(Class extends Resource> resourceClass, Map> criteria);
+
+ void deletePermanently(Class extends Resource> resourceClass, String id);
+
+ Resource read(String resourceTypeName, String id);
+
+ /**
+ * @param
+ * @param resourceType
+ * not null
+ * @param id
+ * not null
+ * @return
+ */
+ R read(Class resourceType, String id);
+
+ /**
+ * Uses If-None-Match and If-Modified-Since Headers based on the version and lastUpdated values in oldValue
+ * to check if the resource has been modified.
+ *
+ * @param
+ * @param oldValue
+ * not null
+ * @return oldValue (same object) if server send 304 - Not Modified, else value returned from server
+ */
+ R read(R oldValue);
+
+ boolean exists(Class resourceType, String id);
+
+ /**
+ * @param id
+ * not null
+ * @param mediaType
+ * not null
+ * @return {@link InputStream} needs to be closed
+ */
+ InputStream readBinary(String id, MediaType mediaType);
+
+ /**
+ * @param resourceTypeName
+ * not null
+ * @param id
+ * not null
+ * @param version
+ * not null
+ * @return {@link Resource}
+ */
+ Resource read(String resourceTypeName, String id, String version);
+
+ R read(Class resourceType, String id, String version);
+
+ boolean exists(Class resourceType, String id, String version);
+
+ /**
+ * @param id
+ * not null
+ * @param version
+ * not null
+ * @param mediaType
+ * not null
+ * @return {@link InputStream} needs to be closed
+ */
+ InputStream readBinary(String id, String version, MediaType mediaType);
+
+ boolean exists(IdType resourceTypeIdVersion);
+
+ Bundle search(Class extends Resource> resourceType, Map> parameters);
+
+ Bundle searchWithStrictHandling(Class extends Resource> resourceType, Map> parameters);
+
+ CapabilityStatement getConformance();
+
+ StructureDefinition generateSnapshot(String url);
+
+ StructureDefinition generateSnapshot(StructureDefinition differential);
+
+ default Bundle history()
+ {
+ return history(null);
+ }
+
+ default Bundle history(int page, int count)
+ {
+ return history(null, page, count);
+ }
+
+ default Bundle history(Class extends Resource> resourceType)
+ {
+ return history(resourceType, null);
+ }
+
+ default Bundle history(Class extends Resource> resourceType, int page, int count)
+ {
+ return history(resourceType, null, page, count);
+ }
+
+ default Bundle history(Class extends Resource> resourceType, String id)
+ {
+ return history(resourceType, id, Integer.MIN_VALUE, Integer.MIN_VALUE);
+ }
+
+ Bundle history(Class extends Resource> resourceType, String id, int page, int count);
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/FhirWebserviceClient.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/FhirWebserviceClient.java
new file mode 100644
index 000000000..ab08defa2
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/FhirWebserviceClient.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.client;
+
+public interface FhirWebserviceClient extends BasicFhirWebserviceClient, RetryClient
+{
+ String getBaseUrl();
+
+ PreferReturnOutcomeWithRetry withOperationOutcomeReturn();
+
+ PreferReturnMinimalWithRetry withMinimalReturn();
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnMinimal.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnMinimal.java
new file mode 100644
index 000000000..4bceee1ae
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnMinimal.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.client;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+import org.hl7.fhir.r4.model.Bundle;
+import org.hl7.fhir.r4.model.IdType;
+import org.hl7.fhir.r4.model.Resource;
+
+import jakarta.ws.rs.core.MediaType;
+
+public interface PreferReturnMinimal
+{
+ IdType create(Resource resource);
+
+ IdType createConditionaly(Resource resource, String ifNoneExistCriteria);
+
+ IdType createBinary(InputStream in, MediaType mediaType, String securityContextReference);
+
+ IdType update(Resource resource);
+
+ IdType updateConditionaly(Resource resource, Map> criteria);
+
+ IdType updateBinary(String id, InputStream in, MediaType mediaType, String securityContextReference);
+
+ Bundle postBundle(Bundle bundle);
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalWithRetry.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalWithRetry.java
new file mode 100644
index 000000000..1760da0c7
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalWithRetry.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.client;
+
+public interface PreferReturnMinimalWithRetry extends PreferReturnMinimal, RetryClient
+{
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnOutcome.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnOutcome.java
new file mode 100644
index 000000000..cb65eb1e3
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnOutcome.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.client;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+import org.hl7.fhir.r4.model.Bundle;
+import org.hl7.fhir.r4.model.OperationOutcome;
+import org.hl7.fhir.r4.model.Resource;
+
+import jakarta.ws.rs.core.MediaType;
+
+public interface PreferReturnOutcome
+{
+ OperationOutcome create(Resource resource);
+
+ OperationOutcome createConditionaly(Resource resource, String ifNoneExistCriteria);
+
+ OperationOutcome createBinary(InputStream in, MediaType mediaType, String securityContextReference);
+
+
+ OperationOutcome update(Resource resource);
+
+ OperationOutcome updateConditionaly(Resource resource, Map> criteria);
+
+ OperationOutcome updateBinary(String id, InputStream in, MediaType mediaType, String securityContextReference);
+
+
+ Bundle postBundle(Bundle bundle);
+}
\ No newline at end of file
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeWithRetry.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeWithRetry.java
new file mode 100644
index 000000000..63e184a37
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeWithRetry.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.client;
+
+public interface PreferReturnOutcomeWithRetry extends PreferReturnOutcome, RetryClient
+{
+}
diff --git a/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnResource.java b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnResource.java
new file mode 100644
index 000000000..a02f3a764
--- /dev/null
+++ b/dsf-bpe/dsf-bpe-process-api-v1-base/src/main/java/dev/dsf/fhir/client/PreferReturnResource.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018-2025 Heilbronn University of Applied Sciences
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dev.dsf.fhir.client;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+import org.hl7.fhir.r4.model.Binary;
+import org.hl7.fhir.r4.model.Bundle;
+import org.hl7.fhir.r4.model.Resource;
+
+import jakarta.ws.rs.core.MediaType;
+
+public interface PreferReturnResource
+{
+