diff --git a/.gitignore b/.gitignore index c90a78c3f..686ced3ca 100755 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,11 @@ ### # dsf-bpe ignores ### +dsf-bpe/dsf-bpe-server-jetty/api/v1/*.jar +dsf-bpe/dsf-bpe-server-jetty/api/v2/*.jar dsf-bpe/dsf-bpe-server-jetty/conf/config.properties +dsf-bpe/dsf-bpe-server-jetty/docker/api/v1/*.jar +dsf-bpe/dsf-bpe-server-jetty/docker/api/v2/*.jar dsf-bpe/dsf-bpe-server-jetty/docker/dsf_bpe.jar dsf-bpe/dsf-bpe-server-jetty/docker/dsf_status_client.jar dsf-bpe/dsf-bpe-server-jetty/docker/lib/*.jar diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml new file mode 100644 index 000000000..d27cfd40c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/pom.xml @@ -0,0 +1,66 @@ + + 4.0.0 + + dsf-bpe-process-api-v1-impl + + + dev.dsf + dsf-bpe-pom + 2.0.0-SNAPSHOT + + + + + dev.dsf + dsf-bpe-process-api + + + dev.dsf + dsf-bpe-process-api-v1 + + + + org.glassfish.jersey.core + jersey-client + + + org.glassfish.jersey.inject + jersey-hk2 + + + org.glassfish.jersey.media + jersey-media-jaxb + + + org.glassfish.jersey.media + jersey-media-json-jackson + + + org.glassfish.jersey.connectors + jersey-apache-connector + + + commons-logging + commons-logging + + + + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${hapi.fhir.version.v1} + + + + org.apache.logging.log4j + log4j-slf4j2-impl + test + + + org.mockito + mockito-core + test + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/ProcessPluginApiImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/ProcessPluginApiImpl.java similarity index 98% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/ProcessPluginApiImpl.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/ProcessPluginApiImpl.java index 0f4c5c3e3..8ad20b9e2 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/ProcessPluginApiImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/ProcessPluginApiImpl.java @@ -16,7 +16,7 @@ import dev.dsf.bpe.v1.service.QuestionnaireResponseHelper; import dev.dsf.bpe.v1.service.TaskHelper; import dev.dsf.bpe.v1.variables.Variables; -import dev.dsf.bpe.variables.VariablesImpl; +import dev.dsf.bpe.v1.variables.VariablesImpl; import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelper; import dev.dsf.fhir.authorization.read.ReadAccessHelper; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/config/ProxyConfigDelegate.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/config/ProxyConfigDelegate.java similarity index 82% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/config/ProxyConfigDelegate.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/config/ProxyConfigDelegate.java index 8da1d0ace..b9cf3d258 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/config/ProxyConfigDelegate.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/config/ProxyConfigDelegate.java @@ -4,9 +4,9 @@ public class ProxyConfigDelegate implements ProxyConfig { - private final dev.dsf.common.config.ProxyConfig delegate; + private final dev.dsf.bpe.api.config.ProxyConfig delegate; - public ProxyConfigDelegate(dev.dsf.common.config.ProxyConfig delegate) + public ProxyConfigDelegate(dev.dsf.bpe.api.config.ProxyConfig delegate) { this.delegate = delegate; } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/listener/AbstractListener.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/listener/AbstractListener.java similarity index 98% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/listener/AbstractListener.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/listener/AbstractListener.java index 5b45b603f..e8be4669e 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/listener/AbstractListener.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/listener/AbstractListener.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.listener; +package dev.dsf.bpe.v1.listener; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/listener/ContinueListener.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/listener/ContinueListener.java similarity index 93% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/listener/ContinueListener.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/listener/ContinueListener.java index a64fe6948..3f160d899 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/listener/ContinueListener.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/listener/ContinueListener.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.listener; +package dev.dsf.bpe.v1.listener; import java.util.function.Function; @@ -8,7 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import dev.dsf.bpe.subscription.TaskHandler; +import dev.dsf.bpe.api.Constants; import dev.dsf.bpe.v1.constants.CodeSystems.BpmnMessage; public class ContinueListener extends AbstractListener implements ExecutionListener @@ -23,8 +23,8 @@ public ContinueListener(String serverBaseUrl, Function getSpringServiceConfigClass() + { + return ApiServiceConfig.class; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginFactoryImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginFactoryImpl.java new file mode 100644 index 000000000..e742af88e --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginFactoryImpl.java @@ -0,0 +1,49 @@ +package dev.dsf.bpe.v1.plugin; + +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.camunda.bpm.engine.impl.variable.serializer.TypedValueSerializer; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import dev.dsf.bpe.api.listener.ListenerFactory; +import dev.dsf.bpe.api.plugin.AbstractProcessPluginFactory; +import dev.dsf.bpe.api.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessPluginFactory; +import dev.dsf.bpe.v1.ProcessPluginDefinition; +import dev.dsf.bpe.v1.activity.DefaultUserTaskListener; + +public class ProcessPluginFactoryImpl extends AbstractProcessPluginFactory implements ProcessPluginFactory +{ + public static final int API_VERSION = 1; + + public ProcessPluginFactoryImpl(ClassLoader apiClassLoader, ApplicationContext apiApplicationContext, + ConfigurableEnvironment environment) + { + super(API_VERSION, apiClassLoader, apiApplicationContext, environment, ProcessPluginDefinition.class, + DefaultUserTaskListener.class); + } + + @Override + protected ProcessPlugin createProcessPlugin(Object processPluginDefinition, boolean draft, Path jarFile, + URLClassLoader pluginClassLoader) + { + return new ProcessPluginImpl((ProcessPluginDefinition) processPluginDefinition, API_VERSION, draft, jarFile, + pluginClassLoader, environment, apiApplicationContext); + } + + @Override + @SuppressWarnings("rawtypes") + public Stream getSerializer() + { + return apiApplicationContext.getBeansOfType(TypedValueSerializer.class).values().stream(); + } + + @Override + public ListenerFactory getListenerFactory() + { + return apiApplicationContext.getBean(ListenerFactory.class); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginImpl.java new file mode 100644 index 000000000..d45c4506b --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginImpl.java @@ -0,0 +1,258 @@ +package dev.dsf.bpe.v1.plugin; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.camunda.bpm.engine.variable.value.PrimitiveValue; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.ActivityDefinition; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.MetadataResource; +import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskStatus; +import org.hl7.fhir.r4.model.ValueSet; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import dev.dsf.bpe.api.plugin.AbstractProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessPluginDeploymentListener; +import dev.dsf.bpe.api.plugin.ProcessPluginFhirConfig; +import dev.dsf.bpe.v1.ProcessPluginApi; +import dev.dsf.bpe.v1.ProcessPluginDefinition; +import dev.dsf.bpe.v1.ProcessPluginDeploymentStateListener; +import dev.dsf.bpe.v1.constants.CodeSystems; +import dev.dsf.bpe.v1.constants.NamingSystems.OrganizationIdentifier; +import dev.dsf.bpe.v1.constants.NamingSystems.TaskIdentifier; +import dev.dsf.bpe.v1.variables.FhirResourceValues; + +public class ProcessPluginImpl extends AbstractProcessPlugin implements ProcessPlugin +{ + private final ProcessPluginDefinition processPluginDefinition; + private final ProcessPluginApi processPluginApi; + + public ProcessPluginImpl(ProcessPluginDefinition processPluginDefinition, int processPluginApiVersion, + boolean draft, Path jarFile, ClassLoader classLoader, ConfigurableEnvironment environment, + ApplicationContext apiApplicationContext) + { + super(ProcessPluginDefinition.class, processPluginApiVersion, draft, jarFile, classLoader, environment, + apiApplicationContext, ApiServicesSpringConfiguration.class); + + this.processPluginDefinition = processPluginDefinition; + processPluginApi = apiApplicationContext.getBean(ProcessPluginApi.class); + } + + @Override + protected ProcessPluginFhirConfig createFhirConfig() + { + BiFunction parseResource = (String filename, String content) -> + { + if (filename.endsWith(JSON_SUFFIX)) + return newJsonParser().parseResource(content); + else if (filename.endsWith(XML_SUFFIX)) + return newXmlParser().parseResource(content); + else + throw new IllegalArgumentException("FHIR resource filename not ending in .json or .xml"); + }; + + Function encodeResource = resource -> + { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = new OutputStreamWriter(out, StandardCharsets.UTF_8)) + { + newJsonParser().encodeResourceToWriter((IBaseResource) resource, w); + return out.toByteArray(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + }; + + Function> getResourceName = resource -> Optional + .ofNullable(resource instanceof Resource r ? r.getResourceType().name() : null); + + Predicate hasMetadataResourceUrl = resource -> resource instanceof MetadataResource m && m.hasUrl(); + Predicate hasMetadataResourceVersion = resource -> resource instanceof MetadataResource m + && m.hasVersion(); + + Function> getMetadataResourceVersion = resource -> Optional + .ofNullable(resource instanceof MetadataResource m ? m.getVersion() : null); + + Function> getActivityDefinitionUrl = a -> Optional + .ofNullable(a.hasUrlElement() && a.getUrlElement().hasValue() ? a.getUrlElement().getValue() : null); + + Function> getTaskInstantiatesCanonical = resource -> Optional + .ofNullable(resource instanceof Task t && t.hasInstantiatesCanonicalElement() + && t.getInstantiatesCanonicalElement().hasValue() + ? t.getInstantiatesCanonicalElement().getValue() + : null); + + Function> getTaskIdentifierValue = t -> TaskIdentifier + .findFirst(t) + .map(i -> new ProcessPluginFhirConfig.Identifier( + i.hasSystem() ? Optional.of(i.getSystem()) : Optional.empty(), + i.hasValue() ? Optional.of(i.getValue()) : Optional.empty())); + + Predicate isTaskStatusDraft = t -> t.hasStatusElement() && t.getStatusElement().hasValue() + && TaskStatus.DRAFT.equals(t.getStatus()); + + Function> getRequester = t -> t.hasRequester() + ? Optional.ofNullable(t.getRequester()).map(r -> + { + Identifier i = r.getIdentifier(); + return new ProcessPluginFhirConfig.Reference( + Optional.ofNullable(i.getSystemElement()).filter(e -> e.hasValue()).map(e -> e.getValue()), + Optional.ofNullable(i.getValueElement()).filter(e -> e.hasValue()).map(e -> e.getValue()), + Optional.ofNullable(r.getTypeElement()).filter(e -> e.hasValue()).map(e -> e.getValue())); + }) + : Optional.empty(); + + Function> getRecipient = t -> t.hasRestriction() + && t.getRestriction().hasRecipient() && t.getRestriction().getRecipient().size() == 1 + ? Optional.ofNullable(t.getRestriction().getRecipientFirstRep()).map(r -> + { + Identifier i = r.getIdentifier(); + return new ProcessPluginFhirConfig.Reference( + Optional.ofNullable(i.getSystemElement()).filter(e -> e.hasValue()) + .map(e -> e.getValue()), + Optional.ofNullable(i.getValueElement()).filter(e -> e.hasValue()) + .map(e -> e.getValue()), + Optional.ofNullable(r.getTypeElement()).filter(e -> e.hasValue()) + .map(e -> e.getValue())); + }) + : Optional.empty(); + + Predicate hasTaskInputMessageName = t -> t + .getInput().stream().filter( + i -> i.getType().getCoding().stream() + .anyMatch(c -> CodeSystems.BpmnMessage.URL.equals(c.getSystem()) + && CodeSystems.BpmnMessage.Codes.MESSAGE_NAME.equals(c.getCode()))) + .count() == 1; + + return new ProcessPluginFhirConfig<>(ActivityDefinition.class, CodeSystem.class, Library.class, Measure.class, + NamingSystem.class, Questionnaire.class, StructureDefinition.class, Task.class, ValueSet.class, + OrganizationIdentifier.SID, TaskIdentifier.SID, TaskStatus.DRAFT.toCode(), CodeSystems.BpmnMessage.URL, + CodeSystems.BpmnMessage.Codes.MESSAGE_NAME, parseResource, encodeResource, getResourceName, + hasMetadataResourceUrl, hasMetadataResourceVersion, getMetadataResourceVersion, + getActivityDefinitionUrl, NamingSystem::hasName, getTaskInstantiatesCanonical, getTaskIdentifierValue, + isTaskStatusDraft, getRequester, getRecipient, Task::hasInput, hasTaskInputMessageName, + Task::hasOutput); + } + + private IParser newXmlParser() + { + return newParser(FhirContext::newXmlParser); + } + + private IParser newJsonParser() + { + return newParser(FhirContext::newJsonParser); + } + + private IParser newParser(Function parserFactor) + { + IParser p = parserFactor.apply(processPluginApi.getFhirContext()); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + + return p; + } + + @Override + protected List> getDefinitionSpringConfigurations() + { + return processPluginDefinition.getSpringConfigurations(); + } + + @Override + protected String getDefinitionName() + { + return processPluginDefinition.getName(); + } + + @Override + protected String getDefinitionVersion() + { + return processPluginDefinition.getVersion(); + } + + @Override + protected String getDefinitionResourceVersion() + { + return processPluginDefinition.getResourceVersion(); + } + + @Override + protected LocalDate getDefinitionReleaseDate() + { + return processPluginDefinition.getReleaseDate(); + } + + @Override + protected LocalDate getDefinitionResourceReleaseDate() + { + return processPluginDefinition.getResourceReleaseDate(); + } + + @Override + protected Map> getDefinitionFhirResourcesByProcessId() + { + return processPluginDefinition.getFhirResourcesByProcessId(); + } + + @Override + protected List getDefinitionProcessModels() + { + return processPluginDefinition.getProcessModels(); + } + + @Override + public PrimitiveValue createFhirTaskVariable(String taskJson) + { + Task task = newJsonParser().parseResource(Task.class, taskJson); + return FhirResourceValues.create(task); + } + + @Override + public PrimitiveValue createFhirQuestionnaireResponseVariable(String questionnaireResponseJson) + { + QuestionnaireResponse questionnaireResponse = newJsonParser().parseResource(QuestionnaireResponse.class, + questionnaireResponseJson); + return FhirResourceValues.create(questionnaireResponse); + } + + @Override + public ProcessPluginDeploymentListener getProcessPluginDeploymentListener() + { + return allActiveProcesses -> + { + List activePluginProcesses = getActivePluginProcesses(allActiveProcesses); + + getApplicationContext().getBeansOfType(ProcessPluginDeploymentStateListener.class).values().stream() + .forEach(l -> handleProcessPluginDeploymentStateListenerError( + () -> l.onProcessesDeployed(activePluginProcesses), + ProcessPluginDeploymentStateListener.class, l.getClass())); + }; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/AbstractResourceProvider.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/AbstractResourceProvider.java similarity index 100% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/AbstractResourceProvider.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/AbstractResourceProvider.java diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/EndpointProviderImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/EndpointProviderImpl.java similarity index 100% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/EndpointProviderImpl.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/EndpointProviderImpl.java diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirClientProviderImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/FhirWebserviceClientProviderImpl.java similarity index 59% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirClientProviderImpl.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/FhirWebserviceClientProviderImpl.java index 8bf9cd6da..47ad742e9 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirClientProviderImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/FhirWebserviceClientProviderImpl.java @@ -1,31 +1,24 @@ -package dev.dsf.bpe.client; +package dev.dsf.bpe.v1.service; -import java.net.URI; import java.security.KeyStore; import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import ca.uhn.fhir.context.FhirContext; -import dev.dsf.common.config.ProxyConfig; +import dev.dsf.bpe.api.config.ProxyConfig; +import dev.dsf.bpe.api.service.BuildInfoProvider; import dev.dsf.fhir.client.FhirWebserviceClient; import dev.dsf.fhir.client.FhirWebserviceClientJersey; -import dev.dsf.fhir.client.WebsocketClient; -import dev.dsf.fhir.client.WebsocketClientTyrus; import dev.dsf.fhir.service.ReferenceCleaner; -import dev.dsf.tools.build.BuildInfoReader; -public class FhirClientProviderImpl implements FhirClientProvider, InitializingBean +public class FhirWebserviceClientProviderImpl implements FhirWebserviceClientProvider, InitializingBean { - private static final Logger logger = LoggerFactory.getLogger(FhirClientProviderImpl.class); private static final String USER_AGENT_VALUE = "DSF/"; private final Map webserviceClientsByUrl = new HashMap<>(); - private final Map websocketClientsBySubscriptionId = new HashMap<>(); private final FhirContext fhirContext; private final ReferenceCleaner referenceCleaner; @@ -43,21 +36,14 @@ public class FhirClientProviderImpl implements FhirClientProvider, InitializingB private final int remoteWebserviceConnectTimeout; private final boolean remoteWebserviceLogRequests; - private final String localWebsocketUrl; - private final KeyStore localWebsocketTrustStore; - private final KeyStore localWebsocketKeyStore; - private final char[] localWebsocketKeyStorePassword; - private final ProxyConfig proxyConfig; - private final BuildInfoReader buildInfoReader; + private final BuildInfoProvider buildInfoProvider; - public FhirClientProviderImpl(FhirContext fhirContext, ReferenceCleaner referenceCleaner, + public FhirWebserviceClientProviderImpl(FhirContext fhirContext, ReferenceCleaner referenceCleaner, String localWebserviceBaseUrl, int localWebserviceReadTimeout, int localWebserviceConnectTimeout, boolean localWebserviceLogRequests, KeyStore webserviceTrustStore, KeyStore webserviceKeyStore, char[] webserviceKeyStorePassword, int remoteWebserviceReadTimeout, int remoteWebserviceConnectTimeout, - boolean remoteWebserviceLogRequests, String localWebsocketUrl, KeyStore localWebsocketTrustStore, - KeyStore localWebsocketKeyStore, char[] localWebsocketKeyStorePassword, ProxyConfig proxyConfig, - BuildInfoReader buildInfoReader) + boolean remoteWebserviceLogRequests, ProxyConfig proxyConfig, BuildInfoProvider buildInfoProvider) { this.fhirContext = fhirContext; this.referenceCleaner = referenceCleaner; @@ -75,13 +61,8 @@ public FhirClientProviderImpl(FhirContext fhirContext, ReferenceCleaner referenc this.remoteWebserviceConnectTimeout = remoteWebserviceConnectTimeout; this.remoteWebserviceLogRequests = remoteWebserviceLogRequests; - this.localWebsocketUrl = localWebsocketUrl; - this.localWebsocketTrustStore = localWebsocketTrustStore; - this.localWebsocketKeyStore = localWebsocketKeyStore; - this.localWebsocketKeyStorePassword = localWebsocketKeyStorePassword; - this.proxyConfig = proxyConfig; - this.buildInfoReader = buildInfoReader; + this.buildInfoProvider = buildInfoProvider; } @Override @@ -100,13 +81,9 @@ public void afterPropertiesSet() throws Exception throw new IllegalArgumentException("remoteReadTimeout < 0"); if (remoteWebserviceConnectTimeout < 0) throw new IllegalArgumentException("remoteConnectTimeout < 0"); - Objects.requireNonNull(localWebsocketUrl, "localWebsocketUrl"); - Objects.requireNonNull(localWebsocketTrustStore, "localWebsocketTrustStore"); - Objects.requireNonNull(localWebsocketKeyStore, "localWebsocketKeyStore"); - Objects.requireNonNull(localWebsocketKeyStorePassword, "localWebsocketKeyStorePassword"); Objects.requireNonNull(proxyConfig, "proxyConfig"); - Objects.requireNonNull(buildInfoReader, "buildInfoReader"); + Objects.requireNonNull(buildInfoProvider, "buildInfoReader"); } public String getLocalBaseUrl() @@ -131,12 +108,12 @@ private FhirWebserviceClient getClient(String webserviceUrl) client = new FhirWebserviceClientJersey(webserviceUrl, webserviceTrustStore, webserviceKeyStore, webserviceKeyStorePassword, null, proxyUrl, proxyUsername, proxyPassword, localWebserviceConnectTimeout, localWebserviceReadTimeout, localWebserviceLogRequests, - USER_AGENT_VALUE + buildInfoReader.getProjectVersion(), fhirContext, referenceCleaner); + USER_AGENT_VALUE + buildInfoProvider.getProjectVersion(), fhirContext, referenceCleaner); else client = new FhirWebserviceClientJersey(webserviceUrl, webserviceTrustStore, webserviceKeyStore, webserviceKeyStorePassword, null, proxyUrl, proxyUsername, proxyPassword, remoteWebserviceConnectTimeout, remoteWebserviceReadTimeout, remoteWebserviceLogRequests, - USER_AGENT_VALUE + buildInfoReader.getProjectVersion(), fhirContext, referenceCleaner); + USER_AGENT_VALUE + buildInfoProvider.getProjectVersion(), fhirContext, referenceCleaner); webserviceClientsByUrl.put(webserviceUrl, client); return client; @@ -165,45 +142,4 @@ public FhirWebserviceClient getWebserviceClient(String webserviceUrl) return newClient; } } - - @Override - public WebsocketClient getLocalWebsocketClient(Runnable reconnector, String subscriptionId) - { - if (!websocketClientsBySubscriptionId.containsKey(subscriptionId)) - { - WebsocketClientTyrus client = createWebsocketClient(reconnector, subscriptionId); - websocketClientsBySubscriptionId.put(subscriptionId, client); - return client; - } - - return websocketClientsBySubscriptionId.get(subscriptionId); - } - - protected WebsocketClientTyrus createWebsocketClient(Runnable reconnector, String subscriptionId) - { - return new WebsocketClientTyrus(reconnector, URI.create(localWebsocketUrl), localWebsocketTrustStore, - localWebsocketKeyStore, localWebsocketKeyStorePassword, - proxyConfig.isEnabled(localWebsocketUrl) ? proxyConfig.getUrl() : null, - proxyConfig.isEnabled(localWebsocketUrl) ? proxyConfig.getUsername() : null, - proxyConfig.isEnabled(localWebsocketUrl) ? proxyConfig.getPassword() : null, - USER_AGENT_VALUE + buildInfoReader.getProjectVersion(), subscriptionId); - } - - @Override - public void disconnectAll() - { - for (WebsocketClient c : websocketClientsBySubscriptionId.values()) - { - try - { - c.disconnect(); - } - catch (Exception e) - { - logger.debug("Error while disconnecting websocket client", e); - logger.warn("Error while disconnecting websocket client: {} - {}", e.getClass().getName(), - e.getMessage()); - } - } - } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/MailServiceImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/MailServiceImpl.java similarity index 81% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/MailServiceImpl.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/MailServiceImpl.java index 2fb0304ca..c3a526bdf 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/MailServiceImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/MailServiceImpl.java @@ -8,11 +8,13 @@ import org.springframework.beans.factory.InitializingBean; +import dev.dsf.bpe.api.service.BpeMailService; + public class MailServiceImpl implements MailService, InitializingBean { - private final MailService delegate; + private final BpeMailService delegate; - public MailServiceImpl(MailService delegate) + public MailServiceImpl(BpeMailService delegate) { this.delegate = delegate; } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/OrganizationProviderImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/OrganizationProviderImpl.java similarity index 100% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/OrganizationProviderImpl.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/OrganizationProviderImpl.java diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/QuestionnaireResponseHelperImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/QuestionnaireResponseHelperImpl.java similarity index 97% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/QuestionnaireResponseHelperImpl.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/QuestionnaireResponseHelperImpl.java index 0cbdc4090..64773dc77 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/QuestionnaireResponseHelperImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/QuestionnaireResponseHelperImpl.java @@ -100,6 +100,6 @@ public Type transformQuestionTypeToAnswerType(Questionnaire.QuestionnaireItemCom public String getLocalVersionlessAbsoluteUrl(QuestionnaireResponse questionnaireResponse) { return questionnaireResponse.getIdElement().toVersionless() - .withServerBase(serverBaseUrl, ResourceType.Task.name()).getValue(); + .withServerBase(serverBaseUrl, ResourceType.QuestionnaireResponse.name()).getValue(); } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/TaskHelperImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/TaskHelperImpl.java similarity index 100% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/TaskHelperImpl.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/service/TaskHelperImpl.java diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/spring/ApiServiceConfig.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/spring/ApiServiceConfig.java new file mode 100644 index 000000000..8873ece3b --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/spring/ApiServiceConfig.java @@ -0,0 +1,200 @@ +package dev.dsf.bpe.v1.spring; + +import java.security.KeyStore; +import java.util.Locale; +import java.util.UUID; + +import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.databind.ObjectMapper; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.i18n.HapiLocalizer; +import dev.dsf.bpe.api.config.ClientConfig; +import dev.dsf.bpe.api.listener.ListenerFactory; +import dev.dsf.bpe.api.listener.ListenerFactoryImpl; +import dev.dsf.bpe.api.service.BpeMailService; +import dev.dsf.bpe.api.service.BuildInfoProvider; +import dev.dsf.bpe.v1.ProcessPluginApi; +import dev.dsf.bpe.v1.ProcessPluginApiImpl; +import dev.dsf.bpe.v1.config.ProxyConfig; +import dev.dsf.bpe.v1.config.ProxyConfigDelegate; +import dev.dsf.bpe.v1.listener.ContinueListener; +import dev.dsf.bpe.v1.listener.EndListener; +import dev.dsf.bpe.v1.listener.StartListener; +import dev.dsf.bpe.v1.plugin.ProcessPluginFactoryImpl; +import dev.dsf.bpe.v1.service.EndpointProvider; +import dev.dsf.bpe.v1.service.EndpointProviderImpl; +import dev.dsf.bpe.v1.service.FhirWebserviceClientProvider; +import dev.dsf.bpe.v1.service.FhirWebserviceClientProviderImpl; +import dev.dsf.bpe.v1.service.MailService; +import dev.dsf.bpe.v1.service.MailServiceImpl; +import dev.dsf.bpe.v1.service.OrganizationProvider; +import dev.dsf.bpe.v1.service.OrganizationProviderImpl; +import dev.dsf.bpe.v1.service.QuestionnaireResponseHelper; +import dev.dsf.bpe.v1.service.QuestionnaireResponseHelperImpl; +import dev.dsf.bpe.v1.service.TaskHelper; +import dev.dsf.bpe.v1.service.TaskHelperImpl; +import dev.dsf.bpe.v1.variables.FhirResourceSerializer; +import dev.dsf.bpe.v1.variables.FhirResourcesListSerializer; +import dev.dsf.bpe.v1.variables.ObjectMapperFactory; +import dev.dsf.bpe.v1.variables.TargetSerializer; +import dev.dsf.bpe.v1.variables.TargetsSerializer; +import dev.dsf.bpe.v1.variables.VariablesImpl; +import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelper; +import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelperImpl; +import dev.dsf.fhir.authorization.read.ReadAccessHelper; +import dev.dsf.fhir.authorization.read.ReadAccessHelperImpl; +import dev.dsf.fhir.service.ReferenceCleaner; +import dev.dsf.fhir.service.ReferenceCleanerImpl; +import dev.dsf.fhir.service.ReferenceExtractor; +import dev.dsf.fhir.service.ReferenceExtractorImpl; + +@Configuration +public class ApiServiceConfig +{ + @Autowired + private ClientConfig environmentConfig; + + @Autowired + private dev.dsf.bpe.api.config.ProxyConfig proxyConfig; + + @Autowired + private BuildInfoProvider buildInfoProvider; + + @Autowired + private BpeMailService bpeMailService; + + @Bean + public ProcessPluginApi processPluginApiV1() + { + ProxyConfig proxyConfig = new ProxyConfigDelegate(this.proxyConfig); + + FhirWebserviceClientProvider clientProvider = clientProvider(); + EndpointProvider endpointProvider = new EndpointProviderImpl(clientProvider, + environmentConfig.getFhirServerBaseUrl()); + FhirContext fhirContext = fhirContext(); + MailService mailService = new MailServiceImpl(bpeMailService); + ObjectMapper objectMapper = objectMapper(); + OrganizationProvider organizationProvider = new OrganizationProviderImpl(clientProvider, + environmentConfig.getFhirServerBaseUrl()); + + ProcessAuthorizationHelper processAuthorizationHelper = new ProcessAuthorizationHelperImpl(); + QuestionnaireResponseHelper questionnaireResponseHelper = new QuestionnaireResponseHelperImpl( + environmentConfig.getFhirServerBaseUrl()); + ReadAccessHelper readAccessHelper = new ReadAccessHelperImpl(); + TaskHelper taskHelper = new TaskHelperImpl(environmentConfig.getFhirServerBaseUrl()); + + return new ProcessPluginApiImpl(proxyConfig, endpointProvider, fhirContext, clientProvider, mailService, + objectMapper, organizationProvider, processAuthorizationHelper, questionnaireResponseHelper, + readAccessHelper, taskHelper); + } + + @Bean + public FhirWebserviceClientProvider clientProvider() + { + char[] keyStorePassword = UUID.randomUUID().toString().toCharArray(); + KeyStore webserviceKeyStore = environmentConfig.getWebserviceKeyStore(keyStorePassword); + KeyStore webserviceTrustStore = environmentConfig.getWebserviceTrustStore(); + + return new FhirWebserviceClientProviderImpl(fhirContext(), referenceCleaner(), + environmentConfig.getFhirServerBaseUrl(), environmentConfig.getWebserviceClientLocalReadTimeout(), + environmentConfig.getWebserviceClientLocalConnectTimeout(), + environmentConfig.getWebserviceClientLocalVerbose(), webserviceTrustStore, webserviceKeyStore, + keyStorePassword, environmentConfig.getWebserviceClientRemoteReadTimeout(), + environmentConfig.getWebserviceClientRemoteConnectTimeout(), + environmentConfig.getWebserviceClientRemoteVerbose(), proxyConfig, buildInfoProvider); + } + + @Bean + public ReferenceCleaner referenceCleaner() + { + return new ReferenceCleanerImpl(referenceExtractor()); + } + + @Bean + public ReferenceExtractor referenceExtractor() + { + return new ReferenceExtractorImpl(); + } + + @Bean + public FhirContext fhirContext() + { + // workaround for https://github.com/hapifhir/hapi-fhir/issues/5205 + StreamReadConstraints.overrideDefaultStreamReadConstraints( + StreamReadConstraints.builder().maxStringLength(Integer.MAX_VALUE).build()); + + FhirContext context = FhirContext.forR4(); + HapiLocalizer localizer = new HapiLocalizer() + { + @Override + public Locale getLocale() + { + return Locale.ROOT; + } + }; + context.setLocalizer(localizer); + return context; + } + + @Bean + public ObjectMapper objectMapper() + { + return ObjectMapperFactory.createObjectMapper(fhirContext()); + } + + @Bean + public FhirResourceSerializer fhirResourceSerializer() + { + return new FhirResourceSerializer(fhirContext()); + } + + @Bean + public FhirResourcesListSerializer fhirResourcesListSerializer() + { + return new FhirResourcesListSerializer(objectMapper()); + } + + @Bean + public TargetSerializer targetSerializer() + { + return new TargetSerializer(objectMapper()); + } + + @Bean + public TargetsSerializer targetsSerializer() + { + return new TargetsSerializer(objectMapper()); + } + + @Bean + public ExecutionListener startListener() + { + return new StartListener(environmentConfig.getFhirServerBaseUrl(), VariablesImpl::new); + } + + @Bean + public ExecutionListener endListener() + { + return new EndListener(environmentConfig.getFhirServerBaseUrl(), VariablesImpl::new, + clientProvider().getLocalWebserviceClient()); + } + + @Bean + public ExecutionListener continueListener() + { + return new ContinueListener(environmentConfig.getFhirServerBaseUrl(), VariablesImpl::new); + } + + @Bean + public ListenerFactory listenerFactory() + { + return new ListenerFactoryImpl(ProcessPluginFactoryImpl.API_VERSION, startListener(), endListener(), + continueListener()); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceJacksonDeserializer.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceJacksonDeserializer.java similarity index 97% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceJacksonDeserializer.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceJacksonDeserializer.java index 8cc1aee74..3671b72c7 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceJacksonDeserializer.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceJacksonDeserializer.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.io.IOException; import java.util.Objects; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceJacksonSerializer.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceJacksonSerializer.java similarity index 96% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceJacksonSerializer.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceJacksonSerializer.java index d107d7e6a..c40b74ba2 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceJacksonSerializer.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceJacksonSerializer.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.io.IOException; import java.util.Objects; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceSerializer.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceSerializer.java similarity index 96% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceSerializer.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceSerializer.java index 4c1b45ee4..b3ad24e8a 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceSerializer.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceSerializer.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; @@ -15,7 +15,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; -import dev.dsf.bpe.variables.FhirResourceValues.FhirResourceValue; +import dev.dsf.bpe.v1.variables.FhirResourceValues.FhirResourceValue; public class FhirResourceSerializer extends PrimitiveValueSerializer implements InitializingBean { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceValues.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceValues.java similarity index 97% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceValues.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceValues.java index 8adaf566c..badba8c2f 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourceValues.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourceValues.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.util.Map; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourcesList.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourcesList.java similarity index 97% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourcesList.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourcesList.java index 150904d70..a4c93d881 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourcesList.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourcesList.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.util.ArrayList; import java.util.Arrays; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourcesListSerializer.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourcesListSerializer.java similarity index 94% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourcesListSerializer.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourcesListSerializer.java index 9f27ffd1e..d692409e8 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourcesListSerializer.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourcesListSerializer.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -11,7 +11,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; -import dev.dsf.bpe.variables.FhirResourcesListValues.FhirResourcesListValue; +import dev.dsf.bpe.v1.variables.FhirResourcesListValues.FhirResourcesListValue; public class FhirResourcesListSerializer extends PrimitiveValueSerializer implements InitializingBean diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourcesListValues.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourcesListValues.java similarity index 98% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourcesListValues.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourcesListValues.java index 467039354..fc102fb4f 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/FhirResourcesListValues.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/FhirResourcesListValues.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.util.Collection; import java.util.List; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/KeyDeserializer.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/KeyDeserializer.java similarity index 96% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/KeyDeserializer.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/KeyDeserializer.java index f8ae8545e..29f99afa4 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/KeyDeserializer.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/KeyDeserializer.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.io.IOException; import java.security.Key; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/KeySerializer.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/KeySerializer.java similarity index 95% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/KeySerializer.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/KeySerializer.java index ab2d5bcd1..6d80040cb 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/KeySerializer.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/KeySerializer.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.io.IOException; import java.security.Key; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/ObjectMapperFactory.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/ObjectMapperFactory.java similarity index 97% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/ObjectMapperFactory.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/ObjectMapperFactory.java index 2f34d738a..4afb9af79 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/ObjectMapperFactory.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/ObjectMapperFactory.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import org.hl7.fhir.r4.model.Resource; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetImpl.java similarity index 96% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetImpl.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetImpl.java index a082c0db2..91d6b9786 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetImpl.java @@ -1,11 +1,9 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import dev.dsf.bpe.v1.variables.Target; - public class TargetImpl implements Target { private final String organizationIdentifierValue; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetSerializer.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetSerializer.java similarity index 93% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetSerializer.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetSerializer.java index ee2787e60..24e254b8c 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetSerializer.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetSerializer.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.io.IOException; import java.util.Objects; @@ -11,8 +11,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import dev.dsf.bpe.v1.variables.Target; -import dev.dsf.bpe.variables.TargetValues.TargetValue; +import dev.dsf.bpe.v1.variables.TargetValues.TargetValue; public class TargetSerializer extends PrimitiveValueSerializer implements InitializingBean { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetValues.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetValues.java similarity index 97% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetValues.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetValues.java index de91ea92f..537183871 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetValues.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetValues.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.util.Map; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetsImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetsImpl.java similarity index 94% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetsImpl.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetsImpl.java index 74e1220a8..843c591e2 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetsImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetsImpl.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.util.ArrayList; import java.util.Collection; @@ -10,9 +10,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import dev.dsf.bpe.v1.variables.Target; -import dev.dsf.bpe.v1.variables.Targets; - public class TargetsImpl implements Targets { private final List entries = new ArrayList<>(); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetsSerializer.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetsSerializer.java similarity index 93% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetsSerializer.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetsSerializer.java index 334448842..a8e788f6e 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetsSerializer.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetsSerializer.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.io.IOException; import java.util.Objects; @@ -11,8 +11,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import dev.dsf.bpe.v1.variables.Targets; -import dev.dsf.bpe.variables.TargetsValues.TargetsValue; +import dev.dsf.bpe.v1.variables.TargetsValues.TargetsValue; public class TargetsSerializer extends PrimitiveValueSerializer implements InitializingBean { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetsValues.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetsValues.java similarity index 97% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetsValues.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetsValues.java index 0c571ab55..d9b7c8444 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/TargetsValues.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/TargetsValues.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.util.Map; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/VariablesImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/VariablesImpl.java similarity index 92% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/VariablesImpl.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/VariablesImpl.java index 941e7e752..79303ef0d 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/variables/VariablesImpl.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/bpe/v1/variables/VariablesImpl.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.variables; +package dev.dsf.bpe.v1.variables; import java.util.ArrayList; import java.util.Collections; @@ -15,15 +15,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import dev.dsf.bpe.listener.ListenerVariables; -import dev.dsf.bpe.subscription.QuestionnaireResponseHandler; +import dev.dsf.bpe.api.Constants; import dev.dsf.bpe.v1.constants.BpmnExecutionVariables; -import dev.dsf.bpe.v1.variables.Target; -import dev.dsf.bpe.v1.variables.Targets; -import dev.dsf.bpe.v1.variables.Variables; -import dev.dsf.bpe.variables.FhirResourceValues.FhirResourceValue; -import dev.dsf.bpe.variables.FhirResourcesListValues.FhirResourcesListValue; -import dev.dsf.bpe.variables.TargetValues.TargetValue; +import dev.dsf.bpe.v1.listener.ListenerVariables; +import dev.dsf.bpe.v1.variables.FhirResourceValues.FhirResourceValue; +import dev.dsf.bpe.v1.variables.FhirResourcesListValues.FhirResourcesListValue; +import dev.dsf.bpe.v1.variables.TargetValues.TargetValue; public class VariablesImpl implements Variables, ListenerVariables { @@ -230,7 +227,7 @@ public List getCurrentTasks() @Override public void updateTask(Task task) { - logger.trace("updateTask- Task.id: {}", task == null ? "null" : task.getIdElement().getIdPart()); + logger.trace("updateTask - Task.id: {}", task == null ? "null" : task.getIdElement().getIdPart()); if (task != null) { @@ -246,13 +243,35 @@ public void updateTask(Task task) setResourceList(TASKS_PREFIX + instanceId, tasks); else logger.warn("Given task {} not part of tasks list '{}', ignoring task", - task.getIdElement().getIdPart().toString(), instanceId); + task.getIdElement().getIdPart(), instanceId); } } else logger.warn("Given task is null"); } + @Override + public QuestionnaireResponse getLatestReceivedQuestionnaireResponse() + { + return (QuestionnaireResponse) getResource(Constants.QUESTIONNAIRE_RESPONSE_VARIABLE); + } + + @Override + public void setVariable(String variableName, TypedValue value) + { + Objects.requireNonNull(variableName, "variableName"); + + execution.setVariable(variableName, value); + } + + @Override + public Object getVariable(String variableName) + { + Objects.requireNonNull(variableName, "variableName"); + + return execution.getVariable(variableName); + } + @Override public void onStart(Task task) { @@ -294,26 +313,4 @@ public void onEnd() tasks.removeAll(getCurrentTasks()); setResourceList(TASKS_PREFIX + instanceId, tasks); } - - @Override - public QuestionnaireResponse getLatestReceivedQuestionnaireResponse() - { - return (QuestionnaireResponse) getResource(QuestionnaireResponseHandler.QUESTIONNAIRE_RESPONSE_VARIABLE); - } - - @Override - public void setVariable(String variableName, TypedValue value) - { - Objects.requireNonNull(variableName, "variableName"); - - execution.setVariable(variableName, value); - } - - @Override - public Object getVariable(String variableName) - { - Objects.requireNonNull(variableName, "variableName"); - - return execution.getVariable(variableName); - } } diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/adapter/FhirAdapter.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/adapter/FhirAdapter.java new file mode 100644 index 000000000..76a7ae03c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/adapter/FhirAdapter.java @@ -0,0 +1,156 @@ +package dev.dsf.fhir.adapter; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Set; +import java.util.function.Supplier; + +import org.hl7.fhir.r4.model.BaseResource; +import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.IdType; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.api.Constants; +import dev.dsf.fhir.service.ReferenceCleaner; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; + +@Provider +@Consumes({ Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML, + Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON }) +@Produces({ Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML, + Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON }) +public class FhirAdapter implements MessageBodyReader, MessageBodyWriter +{ + private final FhirContext fhirContext; + private final ReferenceCleaner referenceCleaner; + + public FhirAdapter(FhirContext fhirContext, ReferenceCleaner referenceCleaner) + { + this.fhirContext = fhirContext; + this.referenceCleaner = referenceCleaner; + } + + private IParser getParser(MediaType mediaType, Supplier parserFactor) + { + /* Parsers are not guaranteed to be thread safe */ + IParser p = parserFactor.get(); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + + if (mediaType != null) + { + if ("true".equals(mediaType.getParameters().getOrDefault("pretty", "false"))) + p.setPrettyPrint(true); + + switch (mediaType.getParameters().getOrDefault("summary", "false")) + { + case "true" -> p.setSummaryMode(true); + case "text" -> p.setEncodeElements(Set.of("*.text", "*.id", "*.meta", "*.(mandatory)")); + case "data" -> p.setSuppressNarratives(true); + } + } + + return p; + } + + private IParser getParser(MediaType mediaType) + { + return switch (mediaType.getType() + "/" + mediaType.getSubtype()) + { + case Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML -> + getParser(mediaType, fhirContext::newXmlParser); + case Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON -> + getParser(mediaType, fhirContext::newJsonParser); + default -> throw new IllegalStateException("MediaType " + mediaType.toString() + " not supported"); + }; + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + return type != null && BaseResource.class.isAssignableFrom(type); + } + + @Override + public void writeTo(BaseResource t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException + { + getParser(mediaType).encodeResourceToWriter(t, new OutputStreamWriter(entityStream)); + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + return type != null && BaseResource.class.isAssignableFrom(type); + } + + @Override + public BaseResource readFrom(Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException + { + return fixResource(getParser(mediaType).parseResource(type, new InputStreamReader(entityStream))); + } + + private BaseResource fixResource(BaseResource resource) + { + return switch (resource) + { + case Bundle b -> fixBundle(b); + case Binary b -> fixBinary(b); + default -> resource; + }; + } + + private BaseResource fixBundle(Bundle resource) + { + // HAPI FHIR parser adds contained resources to bundle references + resource = referenceCleaner.cleanReferenceResourcesIfBundle(resource); + + if (resource.hasIdElement() && resource.getIdElement().hasIdPart() + && !resource.getIdElement().hasVersionIdPart() && resource.hasMeta() + && resource.getMeta().hasVersionId()) + { + // Bugfix HAPI 5.1.0 is removing version information from bundle.id + IdType fixedId = new IdType(resource.getResourceType().name(), resource.getIdElement().getIdPart(), + resource.getMeta().getVersionId()); + resource.setIdElement(fixedId); + } + + // Bugfix HAPI 5.1.0 is removing version information from bundle.id + resource.getEntry().stream().filter(e -> e.hasResource() && e.getResource() instanceof Bundle) + .map(e -> (Bundle) e.getResource()).forEach(this::fixResource); + + return resource; + } + + private BaseResource fixBinary(Binary resource) + { + if (resource.hasIdElement() && resource.getIdElement().hasIdPart() + && !resource.getIdElement().hasVersionIdPart() && resource.hasMeta() + && resource.getMeta().hasVersionId()) + { + // Bugfix HAPI 5.1.0 is removing version information from binary.id + IdType fixedId = new IdType(resource.getResourceType().name(), resource.getIdElement().getIdPart(), + resource.getMeta().getVersionId()); + resource.setIdElement(fixedId); + } + + return resource; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/authorization/read/ReadAccessHelperImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/authorization/read/ReadAccessHelperImpl.java new file mode 100644 index 000000000..5996978a5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/authorization/read/ReadAccessHelperImpl.java @@ -0,0 +1,401 @@ +package dev.dsf.fhir.authorization.read; + +import java.util.Arrays; +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.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.Organization; +import org.hl7.fhir.r4.model.OrganizationAffiliation; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; + +public class ReadAccessHelperImpl implements ReadAccessHelper +{ + private static final List READ_ACCESS_TAG_VALUES = Arrays.asList(READ_ACCESS_TAG_VALUE_LOCAL, + READ_ACCESS_TAG_VALUE_ORGANIZATION, READ_ACCESS_TAG_VALUE_ROLE, READ_ACCESS_TAG_VALUE_ALL); + + private Predicate matchesTagValue(String value) + { + return c -> c != null && READ_ACCESS_TAG_SYSTEM.equals(c.getSystem()) && c.hasCode() + && c.getCode().equals(value); + } + + @Override + public R addLocal(R resource) + { + if (resource == null) + return null; + + resource.getMeta().getTag().removeIf(matchesTagValue(READ_ACCESS_TAG_VALUE_ALL)); + resource.getMeta().addTag().setSystem(READ_ACCESS_TAG_SYSTEM).setCode(READ_ACCESS_TAG_VALUE_LOCAL); + + return resource; + } + + @Override + public R addOrganization(R resource, String organizationIdentifier) + { + if (resource == null) + return null; + + Objects.requireNonNull(organizationIdentifier, "organizationIdentifier"); + + if (resource.getMeta().getTag().stream().noneMatch(matchesTagValue(READ_ACCESS_TAG_VALUE_LOCAL))) + addLocal(resource); + + resource.getMeta().addTag().setSystem(READ_ACCESS_TAG_SYSTEM).setCode(READ_ACCESS_TAG_VALUE_ORGANIZATION) + .addExtension().setUrl(EXTENSION_READ_ACCESS_ORGANIZATION) + .setValue(new Identifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue(organizationIdentifier)); + + return resource; + } + + @Override + public R addOrganization(R resource, Organization organization) + { + if (resource == null) + return null; + + Objects.requireNonNull(organization, "organization"); + + if (!organization.hasIdentifier()) + throw new IllegalArgumentException("organization has no identifier"); + + Optional identifierValue = organization.getIdentifier().stream().filter(Identifier::hasSystem) + .filter(i -> ORGANIZATION_IDENTIFIER_SYSTEM.equals(i.getSystem())).filter(Identifier::hasValue) + .map(Identifier::getValue).filter(v -> !v.isBlank()).findFirst(); + + return addOrganization(resource, identifierValue.orElseThrow(() -> new IllegalArgumentException( + "organization has no non blank identifier value with system " + ORGANIZATION_IDENTIFIER_SYSTEM))); + } + + @Override + public R addRole(R resource, String parentOrganizationIdentifier, String roleSystem, + String roleCode) + { + if (resource == null) + return null; + + Objects.requireNonNull(parentOrganizationIdentifier, "parentOrganizationIdentifier"); + Objects.requireNonNull(roleSystem, "roleSystem"); + Objects.requireNonNull(roleCode, "roleCode"); + + if (resource.getMeta().getTag().stream().noneMatch(matchesTagValue(READ_ACCESS_TAG_VALUE_LOCAL))) + addLocal(resource); + + Extension ex = resource.getMeta().addTag().setSystem(READ_ACCESS_TAG_SYSTEM).setCode(READ_ACCESS_TAG_VALUE_ROLE) + .addExtension().setUrl(EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE); + ex.addExtension().setUrl(EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION).setValue( + new Identifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue(parentOrganizationIdentifier)); + ex.addExtension().setUrl(EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE) + .setValue(new Coding().setSystem(roleSystem).setCode(roleCode)); + return resource; + } + + @Override + public R addRole(R resource, OrganizationAffiliation affiliation) + { + if (resource == null) + return null; + + Objects.requireNonNull(affiliation, "affiliation"); + if (!affiliation.hasOrganization()) + throw new IllegalArgumentException("affiliation has no parent-organization reference"); + if (!affiliation.getOrganization().hasIdentifier()) + throw new IllegalArgumentException("affiliation has no parent-organization reference with identifier"); + if (!affiliation.getOrganization().getIdentifier().hasSystem() + || !ORGANIZATION_IDENTIFIER_SYSTEM.equals(affiliation.getOrganization().getIdentifier().getSystem())) + throw new IllegalArgumentException( + "affiliation has no parent-organization reference with identifier system " + + ORGANIZATION_IDENTIFIER_SYSTEM); + if (!affiliation.getOrganization().getIdentifier().hasValue() + || affiliation.getOrganization().getIdentifier().getValue().isBlank()) + throw new IllegalArgumentException( + "affiliation has no parent-organization reference with non blank identifier value"); + + String parentOrganizationIdentifier = affiliation.getOrganization().getIdentifier().getValue(); + + if (!affiliation.hasCode() || affiliation.getCode().size() != 1 || !affiliation.getCodeFirstRep().hasCoding() + || affiliation.getCodeFirstRep().getCoding().size() != 1 + || !affiliation.getCodeFirstRep().getCodingFirstRep().hasCode() + || !affiliation.getCodeFirstRep().getCodingFirstRep().hasSystem()) + throw new IllegalArgumentException("affiliation has no single member role with code and system"); + + String roleSystem = affiliation.getCodeFirstRep().getCodingFirstRep().getSystem(); + String roleCode = affiliation.getCodeFirstRep().getCodingFirstRep().getCode(); + + return addRole(resource, parentOrganizationIdentifier, roleSystem, roleCode); + } + + @Override + public R addAll(R resource) + { + if (resource == null) + return null; + + resource.getMeta().getTag() + .removeIf(matchesTagValue(READ_ACCESS_TAG_VALUE_LOCAL) + .or(matchesTagValue(READ_ACCESS_TAG_VALUE_ORGANIZATION)) + .or(matchesTagValue(READ_ACCESS_TAG_VALUE_ROLE))); + + resource.getMeta().addTag().setSystem(READ_ACCESS_TAG_SYSTEM).setCode(READ_ACCESS_TAG_VALUE_ALL); + return resource; + } + + @Override + public boolean hasLocal(Resource resource) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + return resource.getMeta().getTag(READ_ACCESS_TAG_SYSTEM, READ_ACCESS_TAG_VALUE_LOCAL) != null; + } + + @Override + public boolean hasOrganization(Resource resource, String organizationIdentifier) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + Stream extensions = getTagExtensions(resource, READ_ACCESS_TAG_SYSTEM, + READ_ACCESS_TAG_VALUE_ORGANIZATION, EXTENSION_READ_ACCESS_ORGANIZATION); + + return extensions.filter(Extension::hasValue).map(Extension::getValue).filter(v -> v instanceof Identifier) + .map(v -> (Identifier) v).filter(Identifier::hasValue) + .anyMatch(i -> Objects.equals(i.getValue(), organizationIdentifier)); + } + + @Override + public boolean hasOrganization(Resource resource, Organization organization) + { + if (resource == null || organization == null) + return false; + + return organization.hasIdentifier() && organization.getIdentifier().stream().filter(Identifier::hasSystem) + .filter(i -> ORGANIZATION_IDENTIFIER_SYSTEM.equals(i.getSystem())).filter(Identifier::hasValue) + .map(Identifier::getValue).anyMatch(identifier -> hasOrganization(resource, identifier)); + } + + private Stream getTagExtensions(Resource resource, String tagSystem, String tagCode, String extensionUrl) + { + return resource.getMeta().getTag().stream().filter(c -> Objects.equals(c.getSystem(), tagSystem)) + .filter(c -> Objects.equals(c.getCode(), tagCode)).filter(Coding::hasExtension) + .flatMap(c -> c.getExtension().stream()).filter(e -> Objects.equals(e.getUrl(), extensionUrl)); + } + + @Override + public boolean hasAnyOrganization(Resource resource) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + return resource.getMeta().getTag(READ_ACCESS_TAG_SYSTEM, READ_ACCESS_TAG_VALUE_ORGANIZATION) != null; + } + + @Override + public boolean hasRole(Resource resource, String parentOrganizationIdentifier, String roleSystem, String roleCode) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + Stream extensions = getTagExtensions(resource, READ_ACCESS_TAG_SYSTEM, READ_ACCESS_TAG_VALUE_ROLE, + EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE); + + return extensions.filter(Extension::hasExtension) + .anyMatch(matches(parentOrganizationIdentifier, roleSystem, roleCode)); + } + + @Override + public boolean hasRole(Resource resource, List affiliations) + { + if (affiliations == null || affiliations.isEmpty()) + return false; + + return affiliations.stream().anyMatch(affiliation -> hasRole(resource, affiliation)); + } + + private Predicate matches(String parentOrganizationIdentifier, String roleSystem, + String roleCode) + { + return extensions -> + { + boolean cor = extensions.getExtension().stream().filter(Extension::hasUrl) + .filter(e -> Objects.equals(e.getUrl(), + EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION)) + .filter(Extension::hasValue).map(Extension::getValue).filter(v -> v instanceof Identifier) + .map(v -> (Identifier) v).filter(Identifier::hasSystem).filter(Identifier::hasValue) + .anyMatch(i -> ORGANIZATION_IDENTIFIER_SYSTEM.equals(i.getSystem()) + && Objects.equals(i.getValue(), parentOrganizationIdentifier)); + boolean role = extensions.getExtension().stream().filter(Extension::hasUrl) + .filter(e -> Objects.equals(e.getUrl(), + EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE)) + .filter(Extension::hasValue).map(Extension::getValue).filter(v -> v instanceof Coding) + .map(v -> (Coding) v) + .anyMatch(c -> Objects.equals(c.getSystem(), roleSystem) && Objects.equals(c.getCode(), roleCode)); + return cor && role; + }; + } + + @Override + public boolean hasRole(Resource resource, OrganizationAffiliation affiliation) + { + if (resource == null || affiliation == null || !affiliation.hasOrganization() || !affiliation.hasCode()) + return false; + + Reference parentOrganizationRef = affiliation.getOrganization(); + if (!parentOrganizationRef.hasIdentifier()) + return false; + Identifier parentOrganizationIdentifier = parentOrganizationRef.getIdentifier(); + if (!parentOrganizationIdentifier.hasValue()) + return false; + + String parentOrganizationIdentifierValue = parentOrganizationRef.getIdentifier().getValue(); + + return affiliation.getCode().stream().filter(CodeableConcept::hasCoding).flatMap(c -> c.getCoding().stream()) + .filter(Coding::hasSystem).filter(Coding::hasCode) + .anyMatch(c -> hasRole(resource, parentOrganizationIdentifierValue, c.getSystem(), c.getCode())); + } + + @Override + public boolean hasAnyRole(Resource resource) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + return resource.getMeta().getTag(READ_ACCESS_TAG_SYSTEM, READ_ACCESS_TAG_VALUE_ROLE) != null; + } + + @Override + public boolean hasAll(Resource resource) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + return resource.getMeta().getTag(READ_ACCESS_TAG_SYSTEM, READ_ACCESS_TAG_VALUE_ALL) != null; + } + + @Override + public boolean isValid(Resource resource) + { + return isValid(resource, organizationIdentifier -> true, role -> true); + } + + @Override + public boolean isValid(Resource resource, Predicate organizationWithIdentifierExists, + Predicate roleExists) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + // 1 LOCAL && N (ORGANIZATION, ROLE) + // 1 All + // all({LOCAL, ORGANIZATION, ROLE, ALL}) valid + + long tagsCount = resource.getMeta().getTag().stream().filter(Coding::hasSystem).filter(Coding::hasCode) + .filter(c -> READ_ACCESS_TAG_SYSTEM.equals(c.getSystem())) + .filter(c -> READ_ACCESS_TAG_VALUES.contains(c.getCode())).count(); + boolean local = resource.getMeta().getTag().stream().filter(Coding::hasSystem).filter(Coding::hasCode) + .filter(c -> READ_ACCESS_TAG_SYSTEM.equals(c.getSystem())) + .filter(c -> READ_ACCESS_TAG_VALUE_LOCAL.equals(c.getCode())).count() == 1; + boolean all = resource.getMeta().getTag().stream().filter(Coding::hasSystem).filter(Coding::hasCode) + .filter(c -> READ_ACCESS_TAG_SYSTEM.equals(c.getSystem())) + .filter(c -> READ_ACCESS_TAG_VALUE_ALL.equals(c.getCode())).count() == 1; + boolean tagsValid = resource.getMeta().getTag().stream().filter(Coding::hasSystem).filter(Coding::hasCode) + .filter(c -> READ_ACCESS_TAG_SYSTEM.equals(c.getSystem())) + .filter(c -> READ_ACCESS_TAG_VALUES.contains(c.getCode())) + .allMatch(isValidReadAccessTag(organizationWithIdentifierExists, roleExists)); + + return ((local && tagsCount >= 1) ^ (all && tagsCount == 1)) && tagsValid; + } + + private Predicate isValidReadAccessTag(Predicate organizationWithIdentifierExists, + Predicate roleExists) + { + return coding -> switch (coding.getCode()) + { + case READ_ACCESS_TAG_VALUE_LOCAL -> true; + case READ_ACCESS_TAG_VALUE_ORGANIZATION -> + isValidOrganizationReadAccessTag(coding, organizationWithIdentifierExists); + case READ_ACCESS_TAG_VALUE_ROLE -> + isValidRoleReadAccessTag(coding, organizationWithIdentifierExists, roleExists); + case READ_ACCESS_TAG_VALUE_ALL -> true; + default -> false; + }; + } + + private boolean isValidOrganizationReadAccessTag(Coding coding, + Predicate organizationWithIdentifierExists) + { + List exts = coding.getExtension().stream().filter(Extension::hasUrl) + .filter(e -> EXTENSION_READ_ACCESS_ORGANIZATION.equals(e.getUrl())).collect(Collectors.toList()); + + return coding.hasExtension() && exts.size() == 1 + && isValidExtensionReadAccesOrganization(exts.get(0), organizationWithIdentifierExists); + } + + private boolean isValidExtensionReadAccesOrganization(Extension extension, + Predicate organizationWithIdentifierExists) + { + return extension.hasValue() && extension.getValue() instanceof Identifier value + && isValidOrganizationIdentifier(value, organizationWithIdentifierExists); + } + + private boolean isValidOrganizationIdentifier(Identifier identifier, + Predicate organizationWithIdentifierExists) + { + return identifier.hasSystem() && ORGANIZATION_IDENTIFIER_SYSTEM.equals(identifier.getSystem()) + && identifier.hasValue() && organizationWithIdentifierExists.test(identifier); + } + + private boolean isValidRoleReadAccessTag(Coding coding, Predicate organizationWithIdentifierExists, + Predicate roleExists) + { + List exts = coding.getExtension().stream().filter(Extension::hasUrl) + .filter(e -> EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE.equals(e.getUrl())) + .collect(Collectors.toList()); + + return coding.hasExtension() && exts.size() == 1 && isValidExtensionReadAccessParentOrganizationMemberRole( + exts.get(0), organizationWithIdentifierExists, roleExists); + } + + private boolean isValidExtensionReadAccessParentOrganizationMemberRole(Extension extension, + Predicate organizationWithIdentifierExists, Predicate roleExists) + { + return extension.hasExtension() && extension.getExtension().size() == 2 + && extension.getExtension().stream() + .filter(e -> isValidExtensionReadAccessParentOrganizationMemberRoleParentOrganization(e, + organizationWithIdentifierExists)) + .count() == 1 + && extension.getExtension().stream() + .filter(e -> isValidExtensionReadAccessParentOrganizationMemberRoleRole(e, roleExists)) + .count() == 1; + } + + private boolean isValidExtensionReadAccessParentOrganizationMemberRoleParentOrganization(Extension e, + Predicate organizationWithIdentifierExists) + { + return e.hasUrl() && EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION.equals(e.getUrl()) + && e.hasValue() && e.getValue() instanceof Identifier value + && isValidOrganizationIdentifier(value, organizationWithIdentifierExists); + } + + private boolean isValidExtensionReadAccessParentOrganizationMemberRoleRole(Extension e, + Predicate roleExists) + { + return e.hasUrl() && EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE.equals(e.getUrl()) + && e.hasValue() && e.getValue() instanceof Coding value && isValidRole(value, roleExists); + } + + private boolean isValidRole(Coding coding, Predicate roleExists) + { + return coding.hasSystem() && coding.hasCode() && roleExists.test(coding); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/AbstractFhirWebserviceClientJerseyWithRetry.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/AbstractFhirWebserviceClientJerseyWithRetry.java new file mode 100644 index 000000000..f28a0d69a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/AbstractFhirWebserviceClientJerseyWithRetry.java @@ -0,0 +1,113 @@ +package dev.dsf.fhir.client; + +import java.net.UnknownHostException; +import java.util.function.Supplier; + +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.HttpHostConnectException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response.Status; + +public abstract class AbstractFhirWebserviceClientJerseyWithRetry +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractFhirWebserviceClientJerseyWithRetry.class); + + protected final FhirWebserviceClientJersey delegate; + protected final int nTimes; + protected final long delayMillis; + + protected AbstractFhirWebserviceClientJerseyWithRetry(FhirWebserviceClientJersey delegate, int nTimes, + long delayMillis) + { + this.delegate = delegate; + this.nTimes = nTimes; + this.delayMillis = delayMillis; + } + + protected final R retry(int nTimes, long delayMillis, Supplier supplier) + { + RuntimeException caughtException = null; + for (int tryNumber = 0; tryNumber <= nTimes || nTimes == RetryClient.RETRY_FOREVER; tryNumber++) + { + try + { + if (tryNumber == 0) + logger.debug("First try ..."); + else if (nTimes != RetryClient.RETRY_FOREVER) + logger.debug("Retry {} of {}", tryNumber, nTimes); + + return supplier.get(); + } + catch (ProcessingException | WebApplicationException e) + { + if (shouldRetry(e)) + { + if (tryNumber < nTimes || nTimes == RetryClient.RETRY_FOREVER) + { + logger.warn("Caught {} - {}; trying again in {} ms{}", e.getClass(), e.getMessage(), + delayMillis, + nTimes == RetryClient.RETRY_FOREVER ? " (retry " + (tryNumber + 1) + ")" : ""); + + try + { + Thread.sleep(delayMillis); + } + catch (InterruptedException e1) + { + } + } + else + { + logger.warn("Caught {} - {}; not trying again", e.getClass(), e.getMessage()); + } + + if (caughtException != null) + e.addSuppressed(caughtException); + caughtException = e; + } + else + throw e; + } + } + + throw caughtException; + } + + private boolean shouldRetry(RuntimeException e) + { + if (e instanceof WebApplicationException w) + { + return isRetryStatusCode(w); + } + else if (e instanceof ProcessingException) + { + Throwable cause = e; + if (isRetryCause(cause)) + return true; + + while (cause.getCause() != null) + { + cause = cause.getCause(); + if (isRetryCause(cause)) + return true; + } + } + + return false; + } + + private boolean isRetryStatusCode(WebApplicationException e) + { + return Status.Family.SERVER_ERROR.equals(e.getResponse().getStatusInfo().getFamily()); + } + + private boolean isRetryCause(Throwable cause) + { + return cause instanceof ConnectTimeoutException || cause instanceof HttpHostConnectException + || cause instanceof UnknownHostException; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/AbstractJerseyClient.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/AbstractJerseyClient.java new file mode 100644 index 000000000..7a1dc6240 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/AbstractJerseyClient.java @@ -0,0 +1,108 @@ +package dev.dsf.fhir.client; + +import java.security.KeyStore; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.logging.LoggingFeature.Verbosity; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.HttpHeaders; + +public class AbstractJerseyClient +{ + private static final java.util.logging.Logger requestDebugLogger; + static + { + requestDebugLogger = java.util.logging.Logger.getLogger(AbstractJerseyClient.class.getName()); + requestDebugLogger.setLevel(Level.INFO); + } + + private final Client client; + private final String baseUrl; + + public AbstractJerseyClient(String baseUrl, KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword, + ObjectMapper objectMapper, Collection componentsToRegister) + { + this(baseUrl, trustStore, keyStore, keyStorePassword, objectMapper, componentsToRegister, null, null, null, 0, + 0, false, null); + } + + public AbstractJerseyClient(String baseUrl, KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword, + ObjectMapper objectMapper, Collection componentsToRegister, String proxySchemeHostPort, + String proxyUserName, char[] proxyPassword, int connectTimeout, int readTimeout, boolean logRequests, + String userAgentValue) + { + SSLContext sslContext = null; + if (trustStore != null && keyStore == null && keyStorePassword == null) + sslContext = SslConfigurator.newInstance().trustStore(trustStore).createSSLContext(); + else if (trustStore != null && keyStore != null && keyStorePassword != null) + sslContext = SslConfigurator.newInstance().trustStore(trustStore).keyStore(keyStore) + .keyStorePassword(keyStorePassword).createSSLContext(); + + ClientBuilder builder = ClientBuilder.newBuilder(); + + if (sslContext != null) + builder = builder.sslContext(sslContext); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(new ApacheConnectorProvider()); + config.property(ClientProperties.PROXY_URI, proxySchemeHostPort); + config.property(ClientProperties.PROXY_USERNAME, proxyUserName); + config.property(ClientProperties.PROXY_PASSWORD, proxyPassword == null ? null : String.valueOf(proxyPassword)); + builder = builder.withConfig(config); + + if (userAgentValue != null && !userAgentValue.isBlank()) + builder = builder.register((ClientRequestFilter) requestContext -> requestContext.getHeaders() + .add(HttpHeaders.USER_AGENT, userAgentValue)); + + builder = builder.readTimeout(readTimeout, TimeUnit.MILLISECONDS).connectTimeout(connectTimeout, + TimeUnit.MILLISECONDS); + + if (objectMapper != null) + { + JacksonJaxbJsonProvider p = new JacksonJaxbJsonProvider(JacksonJsonProvider.BASIC_ANNOTATIONS); + p.setMapper(objectMapper); + builder.register(p); + } + + if (componentsToRegister != null) + componentsToRegister.forEach(builder::register); + + if (logRequests) + { + builder = builder.register(new LoggingFeature(requestDebugLogger, Level.INFO, Verbosity.PAYLOAD_ANY, + LoggingFeature.DEFAULT_MAX_ENTITY_SIZE)); + } + + client = builder.build(); + + this.baseUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"; + // making sure the root url works, this might be a workaround for a jersey client bug + } + + protected WebTarget getResource() + { + return client.target(baseUrl); + } + + public String getBaseUrl() + { + return baseUrl; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java new file mode 100644 index 000000000..782932d58 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceCientWithRetryImpl.java @@ -0,0 +1,200 @@ +package dev.dsf.fhir.client; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.hl7.fhir.r4.model.Binary; +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; + +class BasicFhirWebserviceCientWithRetryImpl extends AbstractFhirWebserviceClientJerseyWithRetry + implements BasicFhirWebserviceClient +{ + BasicFhirWebserviceCientWithRetryImpl(FhirWebserviceClientJersey delegate, int nTimes, long delayMillis) + { + super(delegate, nTimes, delayMillis); + } + + @Override + public R updateConditionaly(R resource, Map> criteria) + { + return retry(nTimes, delayMillis, () -> delegate.updateConditionaly(resource, criteria)); + } + + @Override + public Binary updateBinary(String id, InputStream in, MediaType mediaType, String securityContextReference) + { + return retry(nTimes, delayMillis, () -> delegate.updateBinary(id, in, mediaType, securityContextReference)); + } + + @Override + public R update(R resource) + { + return retry(nTimes, delayMillis, () -> delegate.update(resource)); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return retry(nTimes, delayMillis, () -> delegate.postBundle(bundle)); + } + + @Override + public R createConditionaly(R resource, String ifNoneExistCriteria) + { + return retry(nTimes, delayMillis, () -> delegate.createConditionaly(resource, ifNoneExistCriteria)); + } + + @Override + public Binary createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return retry(nTimes, delayMillis, () -> delegate.createBinary(in, mediaType, securityContextReference)); + } + + @Override + public R create(R resource) + { + return retry(nTimes, delayMillis, () -> delegate.create(resource)); + } + + @Override + public Bundle searchWithStrictHandling(Class resourceType, Map> parameters) + { + return retry(nTimes, delayMillis, () -> delegate.searchWithStrictHandling(resourceType, parameters)); + } + + @Override + public Bundle search(Class resourceType, Map> parameters) + { + return retry(nTimes, delayMillis, () -> delegate.search(resourceType, parameters)); + } + + @Override + public InputStream readBinary(String id, String version, MediaType mediaType) + { + return retry(nTimes, delayMillis, () -> + { + InputStream in = delegate.readBinary(id, version, mediaType); + return in; + }); + } + + @Override + public InputStream readBinary(String id, MediaType mediaType) + { + return retry(nTimes, delayMillis, () -> + { + InputStream in = delegate.readBinary(id, mediaType); + return in; + }); + } + + @Override + public R read(Class resourceType, String id, String version) + { + return retry(nTimes, delayMillis, () -> delegate.read(resourceType, id, version)); + } + + @Override + public Resource read(String resourceTypeName, String id, String version) + { + return retry(nTimes, delayMillis, () -> delegate.read(resourceTypeName, id, version)); + } + + @Override + public R read(Class resourceType, String id) + { + return retry(nTimes, delayMillis, () -> delegate.read(resourceType, id)); + } + + @Override + public R read(R oldValue) + { + return retry(nTimes, delayMillis, () -> delegate.read(oldValue)); + } + + @Override + public Resource read(String resourceTypeName, String id) + { + return retry(nTimes, delayMillis, () -> delegate.read(resourceTypeName, id)); + } + + @Override + public CapabilityStatement getConformance() + { + return retry(nTimes, delayMillis, () -> delegate.getConformance()); + } + + @Override + public StructureDefinition generateSnapshot(StructureDefinition differential) + { + return retry(nTimes, delayMillis, () -> delegate.generateSnapshot(differential)); + } + + @Override + public StructureDefinition generateSnapshot(String url) + { + return retry(nTimes, delayMillis, () -> delegate.generateSnapshot(url)); + } + + @Override + public boolean exists(IdType resourceTypeIdVersion) + { + return retry(nTimes, delayMillis, () -> delegate.exists(resourceTypeIdVersion)); + } + + @Override + public boolean exists(Class resourceType, String id, String version) + { + return retry(nTimes, delayMillis, () -> delegate.exists(resourceType, id, version)); + } + + @Override + public boolean exists(Class resourceType, String id) + { + return retry(nTimes, delayMillis, () -> delegate.exists(resourceType, id)); + } + + @Override + public void deletePermanently(Class resourceClass, String id) + { + retry(nTimes, delayMillis, (Supplier) () -> + { + delegate.deletePermanently(resourceClass, id); + return null; + }); + } + + @Override + public void deleteConditionaly(Class resourceClass, Map> criteria) + { + retry(nTimes, delayMillis, (Supplier) () -> + { + delegate.deleteConditionaly(resourceClass, criteria); + return null; + }); + } + + @Override + public void delete(Class resourceClass, String id) + { + retry(nTimes, delayMillis, (Supplier) () -> + { + delegate.delete(resourceClass, id); + return null; + }); + } + + @Override + public Bundle history(Class resourceType, String id, int page, int count) + { + return retry(nTimes, delayMillis, () -> delegate.history(resourceType, id, page, count)); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java new file mode 100644 index 000000000..8a1257ad8 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java @@ -0,0 +1,773 @@ +package dev.dsf.fhir.client; + +import java.io.InputStream; +import java.security.KeyStore; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.TimeZone; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.model.Binary; +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.OperationOutcome; +import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.UriType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.annotation.ResourceDef; +import ca.uhn.fhir.rest.api.Constants; +import dev.dsf.fhir.adapter.FhirAdapter; +import dev.dsf.fhir.prefer.PreferHandlingType; +import dev.dsf.fhir.prefer.PreferReturnType; +import dev.dsf.fhir.service.ReferenceCleaner; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation.Builder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.EntityTag; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.ext.RuntimeDelegate; + +public class FhirWebserviceClientJersey extends AbstractJerseyClient implements FhirWebserviceClient +{ + private static final Logger logger = LoggerFactory.getLogger(FhirWebserviceClientJersey.class); + + private static final String RFC_7231_FORMAT = "EEE, dd MMM yyyy HH:mm:ss z"; + private static final Map> RESOURCE_TYPES_BY_NAME = Stream.of(ResourceType.values()) + .filter(type -> !ResourceType.List.equals(type)) + .collect(Collectors.toMap(ResourceType::name, FhirWebserviceClientJersey::getFhirClass)); + + private static Class getFhirClass(ResourceType type) + { + try + { + return Class.forName("org.hl7.fhir.r4.model." + type.name()); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException(e); + } + } + + private final PreferReturnMinimalWithRetry preferReturnMinimal; + private final PreferReturnOutcomeWithRetry preferReturnOutcome; + + public FhirWebserviceClientJersey(String baseUrl, KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword, + ObjectMapper objectMapper, String proxySchemeHostPort, String proxyUserName, char[] proxyPassword, + int connectTimeout, int readTimeout, boolean logRequests, String userAgentValue, FhirContext fhirContext, + ReferenceCleaner referenceCleaner) + { + super(baseUrl, trustStore, keyStore, keyStorePassword, objectMapper, + Collections.singleton(new FhirAdapter(fhirContext, referenceCleaner)), proxySchemeHostPort, + proxyUserName, proxyPassword, connectTimeout, readTimeout, logRequests, userAgentValue); + + preferReturnMinimal = new PreferReturnMinimalWithRetryImpl(this); + preferReturnOutcome = new PreferReturnOutcomeWithRetryImpl(this); + } + + private WebApplicationException handleError(Response response) + { + try + { + OperationOutcome outcome = response.readEntity(OperationOutcome.class); + String message = toString(outcome); + + logger.warn("Request failed, OperationOutcome: {}", message); + return new WebApplicationException(message, response.getStatus()); + } + catch (ProcessingException e) + { + response.close(); + + logger.warn("Request failed: {} - {}", e.getClass().getName(), e.getMessage()); + return new WebApplicationException(e, response.getStatus()); + } + } + + private String toString(OperationOutcome outcome) + { + return outcome == null ? "" : outcome.getIssue().stream().map(this::toString).collect(Collectors.joining("\n")); + } + + private String toString(OperationOutcomeIssueComponent issue) + { + return issue == null ? "" : issue.getSeverity() + " " + issue.getCode() + " " + issue.getDiagnostics(); + } + + private void logStatusAndHeaders(Response response) + { + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + logger.debug("HTTP header Location: {}", response.getLocation()); + logger.debug("HTTP header ETag: {}", response.getHeaderString(HttpHeaders.ETAG)); + logger.debug("HTTP header Last-Modified: {}", response.getHeaderString(HttpHeaders.LAST_MODIFIED)); + } + + private PreferReturn toPreferReturn(PreferReturnType returnType, Class resourceType, + Response response) + { + return switch (returnType) + { + case REPRESENTATION -> PreferReturn.resource(response.readEntity(resourceType)); + case MINIMAL -> PreferReturn.minimal(response.getLocation()); + case OPERATION_OUTCOME -> PreferReturn.outcome(response.readEntity(OperationOutcome.class)); + default -> + throw new RuntimeException(PreferReturn.class.getName() + " value " + returnType + " not supported"); + }; + } + + @Override + public PreferReturnMinimalWithRetry withMinimalReturn() + { + return preferReturnMinimal; + } + + @Override + public PreferReturnOutcomeWithRetry withOperationOutcomeReturn() + { + return preferReturnOutcome; + } + + PreferReturn create(PreferReturnType returnType, Resource resource) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + + Response response = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()).request() + .header(Constants.HEADER_PREFER, returnType.getHeaderValue()).accept(Constants.CT_FHIR_JSON_NEW) + .post(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn createConditionaly(PreferReturnType returnType, Resource resource, String ifNoneExistCriteria) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(ifNoneExistCriteria, "ifNoneExistCriteria"); + + Response response = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()).request() + .header(Constants.HEADER_PREFER, returnType.getHeaderValue()) + .header(Constants.HEADER_IF_NONE_EXIST, ifNoneExistCriteria).accept(Constants.CT_FHIR_JSON_NEW) + .post(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn createBinary(PreferReturnType returnType, InputStream in, MediaType mediaType, + String securityContextReference) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(in, "in"); + Objects.requireNonNull(mediaType, "mediaType"); + // securityContextReference may be null + + Builder request = getResource().path("Binary").request().header(Constants.HEADER_PREFER, + returnType.getHeaderValue()); + if (securityContextReference != null && !securityContextReference.isBlank()) + request = request.header(Constants.HEADER_X_SECURITY_CONTEXT, securityContextReference); + Response response = request.accept(Constants.CT_FHIR_JSON_NEW).post(Entity.entity(in, mediaType)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, Binary.class, response); + else + throw handleError(response); + } + + PreferReturn update(PreferReturnType returnType, Resource resource) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + + Builder builder = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()) + .path(resource.getIdElement().getIdPart()).request() + .header(Constants.HEADER_PREFER, returnType.getHeaderValue()).accept(Constants.CT_FHIR_JSON_NEW); + + if (resource.getMeta().hasVersionId()) + builder.header(Constants.HEADER_IF_MATCH, new EntityTag(resource.getMeta().getVersionId(), true)); + + Response response = builder.put(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn updateConditionaly(PreferReturnType returnType, Resource resource, Map> criteria) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(criteria, "criteria"); + if (criteria.isEmpty()) + throw new IllegalArgumentException("criteria map empty"); + + WebTarget target = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()); + + for (Entry> entry : criteria.entrySet()) + target = target.queryParam(entry.getKey(), entry.getValue().toArray()); + + Builder builder = target.request().accept(Constants.CT_FHIR_JSON_NEW).header(Constants.HEADER_PREFER, + returnType.getHeaderValue()); + + if (resource.getMeta().hasVersionId()) + builder.header(Constants.HEADER_IF_MATCH, new EntityTag(resource.getMeta().getVersionId(), true)); + + Response response = builder.put(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus() || Status.OK.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn updateBinary(PreferReturnType returnType, String id, InputStream in, MediaType mediaType, + String securityContextReference) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(in, "in"); + Objects.requireNonNull(mediaType, "mediaType"); + // securityContextReference may be null + + Builder request = getResource().path("Binary").path(id).request().header(Constants.HEADER_PREFER, + returnType.getHeaderValue()); + if (securityContextReference != null && !securityContextReference.isBlank()) + request = request.header(Constants.HEADER_X_SECURITY_CONTEXT, securityContextReference); + Response response = request.accept(Constants.CT_FHIR_JSON_NEW).put(Entity.entity(in, mediaType)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, Binary.class, response); + else + throw handleError(response); + } + + Bundle postBundle(PreferReturnType returnType, Bundle bundle) + { + Objects.requireNonNull(bundle, "bundle"); + + Response response = getResource().request().header(Constants.HEADER_PREFER, returnType.getHeaderValue()) + .accept(Constants.CT_FHIR_JSON_NEW).post(Entity.entity(bundle, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(Bundle.class); + else + throw handleError(response); + } + + @Override + @SuppressWarnings("unchecked") + public R create(R resource) + { + return (R) create(PreferReturnType.REPRESENTATION, resource).getResource(); + } + + @Override + @SuppressWarnings("unchecked") + public R createConditionaly(R resource, String ifNoneExistCriteria) + { + return (R) createConditionaly(PreferReturnType.REPRESENTATION, resource, ifNoneExistCriteria).getResource(); + } + + @Override + public Binary createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return (Binary) createBinary(PreferReturnType.REPRESENTATION, in, mediaType, securityContextReference) + .getResource(); + } + + @Override + @SuppressWarnings("unchecked") + public R update(R resource) + { + return (R) update(PreferReturnType.REPRESENTATION, resource).getResource(); + } + + @Override + @SuppressWarnings("unchecked") + public R updateConditionaly(R resource, Map> criteria) + { + return (R) updateConditionaly(PreferReturnType.REPRESENTATION, resource, criteria).getResource(); + } + + @Override + public Binary updateBinary(String id, InputStream in, MediaType mediaType, String securityContextReference) + { + return (Binary) updateBinary(PreferReturnType.REPRESENTATION, id, in, mediaType, securityContextReference) + .getResource(); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return postBundle(PreferReturnType.REPRESENTATION, bundle); + } + + @Override + public void delete(Class resourceClass, String id) + { + Objects.requireNonNull(resourceClass, "resourceClass"); + Objects.requireNonNull(id, "id"); + + Response response = getResource().path(resourceClass.getAnnotation(ResourceDef.class).name()).path(id).request() + .accept(Constants.CT_FHIR_JSON_NEW).delete(); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() != response.getStatus() + && Status.NO_CONTENT.getStatusCode() != response.getStatus()) + throw handleError(response); + else + response.close(); + } + + @Override + public void deleteConditionaly(Class resourceClass, Map> criteria) + { + Objects.requireNonNull(resourceClass, "resourceClass"); + Objects.requireNonNull(criteria, "criteria"); + if (criteria.isEmpty()) + throw new IllegalArgumentException("criteria map empty"); + + WebTarget target = getResource().path(resourceClass.getAnnotation(ResourceDef.class).name()); + + for (Entry> entry : criteria.entrySet()) + target = target.queryParam(entry.getKey(), entry.getValue().toArray()); + + Response response = target.request().accept(Constants.CT_FHIR_JSON_NEW).delete(); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() != response.getStatus() + && Status.NO_CONTENT.getStatusCode() != response.getStatus()) + throw handleError(response); + else + response.close(); + } + + @Override + public void deletePermanently(Class resourceClass, String id) + { + Objects.requireNonNull(resourceClass, "resourceClass"); + Objects.requireNonNull(id, "id"); + + Response response = getResource().path(resourceClass.getAnnotation(ResourceDef.class).name()).path(id) + .path("$permanent-delete").request().accept(Constants.CT_FHIR_JSON_NEW).post(null); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() != response.getStatus()) + throw handleError(response); + else + response.close(); + } + + @Override + public Resource read(String resourceTypeName, String id) + { + Objects.requireNonNull(resourceTypeName, "resourceTypeName"); + Objects.requireNonNull(id, "id"); + if (!RESOURCE_TYPES_BY_NAME.containsKey(resourceTypeName)) + throw new IllegalArgumentException("Resource of type " + resourceTypeName + " not supported"); + + Response response = getResource().path(resourceTypeName).path(id).request().accept(Constants.CT_FHIR_JSON_NEW) + .get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return (Resource) response.readEntity(RESOURCE_TYPES_BY_NAME.get(resourceTypeName)); + else + throw handleError(response); + } + + @Override + public R read(Class resourceType, String id) + { + return read(resourceType, id, (R) null); + } + + @Override + @SuppressWarnings("unchecked") + public R read(R oldValue) + { + return read((Class) oldValue.getClass(), oldValue.getIdElement().getIdPart(), oldValue); + } + + private R read(Class resourceType, String id, R oldValue) + { + Objects.requireNonNull(resourceType, "resourceType"); + Objects.requireNonNull(id, "id"); + + Builder request = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()).path(id).request(); + + if (oldValue != null && oldValue.hasMeta()) + { + if (oldValue.getMeta().hasVersionId()) + { + EntityTag eTag = new EntityTag(oldValue.getMeta().getVersionIdElement().getValue(), true); + String eTagValue = RuntimeDelegate.getInstance().createHeaderDelegate(EntityTag.class).toString(eTag); + request.header(HttpHeaders.IF_NONE_MATCH, eTagValue); + logger.trace("Sending {} Header with value '{}'", HttpHeaders.IF_NONE_MATCH, eTagValue); + } + + if (oldValue.getMeta().hasLastUpdated()) + { + String dateValue = formatRfc7231(oldValue.getMeta().getLastUpdated()); + request.header(HttpHeaders.IF_MODIFIED_SINCE, dateValue); + logger.trace("Sending {} Header with value '{}'", HttpHeaders.IF_MODIFIED_SINCE, dateValue); + } + } + + Response response = request.accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(resourceType); + else if (oldValue != null && oldValue.hasMeta() + && (oldValue.getMeta().hasVersionId() || oldValue.getMeta().hasLastUpdated()) + && Status.NOT_MODIFIED.getStatusCode() == response.getStatus()) + return oldValue; + else + throw handleError(response); + } + + private String formatRfc7231(Date date) + { + if (date == null) + return null; + else + { + SimpleDateFormat dateFormat = new SimpleDateFormat(RFC_7231_FORMAT, Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + return dateFormat.format(date); + } + } + + @Override + public boolean exists(Class resourceType, String id) + { + Objects.requireNonNull(resourceType, "resourceType"); + Objects.requireNonNull(id, "id"); + + Response response = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()).path(id).request() + .accept(Constants.CT_FHIR_JSON_NEW).head(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return true; + else if (Status.NOT_FOUND.getStatusCode() == response.getStatus()) + return false; + else + throw handleError(response); + } + + @Override + public InputStream readBinary(String id, MediaType mediaType) + { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(mediaType, "mediaType"); + + Response response = getResource().path("Binary").path(id).request().accept(mediaType).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(InputStream.class); + else + throw handleError(response); + } + + @Override + public Resource read(String resourceTypeName, String id, String version) + { + Objects.requireNonNull(resourceTypeName, "resourceTypeName"); + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(version, "version"); + if (!RESOURCE_TYPES_BY_NAME.containsKey(resourceTypeName)) + throw new IllegalArgumentException("Resource of type " + resourceTypeName + " not supported"); + + Response response = getResource().path(resourceTypeName).path(id).path("_history").path(version).request() + .accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return (Resource) response.readEntity(RESOURCE_TYPES_BY_NAME.get(resourceTypeName)); + else + throw handleError(response); + } + + @Override + public R read(Class resourceType, String id, String version) + { + Objects.requireNonNull(resourceType, "resourceType"); + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(version, "version"); + + Response response = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()).path(id) + .path("_history").path(version).request().accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(resourceType); + else + throw handleError(response); + } + + @Override + public boolean exists(Class resourceType, String id, String version) + { + Objects.requireNonNull(resourceType, "resourceType"); + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(version, "version"); + + Response response = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()).path(id) + .path("_history").path(version).request().accept(Constants.CT_FHIR_JSON_NEW).head(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return true; + else if (Status.NOT_FOUND.getStatusCode() == response.getStatus()) + return false; + else + throw handleError(response); + } + + @Override + public InputStream readBinary(String id, String version, MediaType mediaType) + { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(version, "version"); + Objects.requireNonNull(mediaType, "mediaType"); + + Response response = getResource().path("Binary").path(id).path("_history").path(version).request() + .accept(mediaType).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(InputStream.class); + else + throw handleError(response); + } + + @Override + public boolean exists(IdType resourceTypeIdVersion) + { + Objects.requireNonNull(resourceTypeIdVersion, "resourceTypeIdVersion"); + Objects.requireNonNull(resourceTypeIdVersion.getResourceType(), "resourceTypeIdVersion.resourceType"); + Objects.requireNonNull(resourceTypeIdVersion.getIdPart(), "resourceTypeIdVersion.idPart"); + // version may be null + + WebTarget path = getResource().path(resourceTypeIdVersion.getResourceType()) + .path(resourceTypeIdVersion.getIdPart()); + + if (resourceTypeIdVersion.hasVersionIdPart()) + path = path.path("_history").path(resourceTypeIdVersion.getVersionIdPart()); + + Response response = path.request().accept(Constants.CT_FHIR_JSON_NEW).head(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return true; + else if (Status.NOT_FOUND.getStatusCode() == response.getStatus()) + return false; + else + throw handleError(response); + } + + @Override + public Bundle search(Class resourceType, Map> parameters) + { + Objects.requireNonNull(resourceType, "resourceType"); + + WebTarget target = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()); + if (parameters != null) + { + for (Entry> entry : parameters.entrySet()) + target = target.queryParam(entry.getKey(), entry.getValue().toArray()); + } + + Response response = target.request().accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(Bundle.class); + else + throw handleError(response); + } + + @Override + public Bundle searchWithStrictHandling(Class resourceType, Map> parameters) + { + Objects.requireNonNull(resourceType, "resourceType"); + + WebTarget target = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()); + if (parameters != null) + { + for (Entry> entry : parameters.entrySet()) + target = target.queryParam(entry.getKey(), entry.getValue().toArray()); + } + + Response response = target.request().header(Constants.HEADER_PREFER, PreferHandlingType.STRICT.getHeaderValue()) + .accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(Bundle.class); + else + throw handleError(response); + } + + @Override + public CapabilityStatement getConformance() + { + Response response = getResource().path("metadata").request() + .accept(Constants.CT_FHIR_JSON_NEW + "; fhirVersion=4.0").get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(CapabilityStatement.class); + else + throw handleError(response); + } + + @Override + public StructureDefinition generateSnapshot(String url) + { + Objects.requireNonNull(url, "url"); + + Parameters parameters = new Parameters(); + parameters.addParameter().setName("url").setValue(new UriType(url)); + + Response response = getResource().path(StructureDefinition.class.getAnnotation(ResourceDef.class).name()) + .path("$snapshot").request().accept(Constants.CT_FHIR_JSON_NEW) + .post(Entity.entity(parameters, Constants.CT_FHIR_JSON_NEW)); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(StructureDefinition.class); + else + throw handleError(response); + } + + @Override + public StructureDefinition generateSnapshot(StructureDefinition differential) + { + Objects.requireNonNull(differential, "differential"); + + Parameters parameters = new Parameters(); + parameters.addParameter().setName("resource").setResource(differential); + + Response response = getResource().path(StructureDefinition.class.getAnnotation(ResourceDef.class).name()) + .path("$snapshot").request().accept(Constants.CT_FHIR_JSON_NEW) + .post(Entity.entity(parameters, Constants.CT_FHIR_JSON_NEW)); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(StructureDefinition.class); + else + throw handleError(response); + } + + @Override + public BasicFhirWebserviceClient withRetry(int nTimes, long delayMillis) + { + if (nTimes < 0) + throw new IllegalArgumentException("nTimes < 0"); + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new BasicFhirWebserviceCientWithRetryImpl(this, nTimes, delayMillis); + } + + @Override + public BasicFhirWebserviceClient withRetryForever(long delayMillis) + { + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new BasicFhirWebserviceCientWithRetryImpl(this, RETRY_FOREVER, delayMillis); + } + + @Override + public Bundle history(Class resourceType, String id, int page, int count) + { + WebTarget target = getResource(); + + if (resourceType != null) + target = target.path(resourceType.getAnnotation(ResourceDef.class).name()); + + if (!StringUtils.isBlank(id)) + target = target.path(id); + + if (page != Integer.MIN_VALUE) + target = target.queryParam("_page", page); + + if (count != Integer.MIN_VALUE) + target = target.queryParam("_count", count); + + Response response = target.path("_history").request().accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(Bundle.class); + else + throw handleError(response); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturn.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturn.java new file mode 100644 index 000000000..f6fc2f25c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturn.java @@ -0,0 +1,51 @@ +package dev.dsf.fhir.client; + +import java.net.URI; + +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; + +public class PreferReturn +{ + private final IdType id; + private final Resource resource; + private final OperationOutcome operationOutcome; + + private PreferReturn(IdType id, Resource resource, OperationOutcome operationOutcome) + { + this.id = id; + this.resource = resource; + this.operationOutcome = operationOutcome; + } + + public static PreferReturn minimal(URI location) + { + return new PreferReturn(new IdType(location.toString()), null, null); + } + + public static PreferReturn resource(Resource resource) + { + return new PreferReturn(null, resource, null); + } + + public static PreferReturn outcome(OperationOutcome operationOutcome) + { + return new PreferReturn(null, null, operationOutcome); + } + + public IdType getId() + { + return id; + } + + public Resource getResource() + { + return resource; + } + + public OperationOutcome getOperationOutcome() + { + return operationOutcome; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalRetryImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalRetryImpl.java new file mode 100644 index 000000000..65d2802b2 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalRetryImpl.java @@ -0,0 +1,66 @@ +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 dev.dsf.fhir.prefer.PreferReturnType; +import jakarta.ws.rs.core.MediaType; + +class PreferReturnMinimalRetryImpl extends AbstractFhirWebserviceClientJerseyWithRetry implements PreferReturnMinimal +{ + PreferReturnMinimalRetryImpl(FhirWebserviceClientJersey delegate, int nTimes, long delayMillis) + { + super(delegate, nTimes, delayMillis); + } + + @Override + public IdType create(Resource resource) + { + return retry(nTimes, delayMillis, () -> delegate.create(PreferReturnType.MINIMAL, resource).getId()); + } + + @Override + public IdType createConditionaly(Resource resource, String ifNoneExistCriteria) + { + return retry(nTimes, delayMillis, + () -> delegate.createConditionaly(PreferReturnType.MINIMAL, resource, ifNoneExistCriteria).getId()); + } + + @Override + public IdType createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return retry(nTimes, delayMillis, + () -> delegate.createBinary(PreferReturnType.MINIMAL, in, mediaType, securityContextReference).getId()); + } + + @Override + public IdType update(Resource resource) + { + return retry(nTimes, delayMillis, () -> delegate.update(PreferReturnType.MINIMAL, resource).getId()); + } + + @Override + public IdType updateConditionaly(Resource resource, Map> criteria) + { + return retry(nTimes, delayMillis, + () -> delegate.updateConditionaly(PreferReturnType.MINIMAL, resource, criteria).getId()); + } + + @Override + public IdType updateBinary(String id, InputStream in, MediaType mediaType, String securityContextReference) + { + return retry(nTimes, delayMillis, () -> delegate + .updateBinary(PreferReturnType.MINIMAL, id, in, mediaType, securityContextReference).getId()); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return retry(nTimes, delayMillis, () -> delegate.postBundle(PreferReturnType.MINIMAL, bundle)); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalWithRetryImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalWithRetryImpl.java new file mode 100644 index 000000000..21447a199 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalWithRetryImpl.java @@ -0,0 +1,84 @@ +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 dev.dsf.fhir.prefer.PreferReturnType; +import jakarta.ws.rs.core.MediaType; + +class PreferReturnMinimalWithRetryImpl implements PreferReturnMinimalWithRetry +{ + private final FhirWebserviceClientJersey delegate; + + PreferReturnMinimalWithRetryImpl(FhirWebserviceClientJersey delegate) + { + this.delegate = delegate; + } + + @Override + public IdType create(Resource resource) + { + return delegate.create(PreferReturnType.MINIMAL, resource).getId(); + } + + @Override + public IdType createConditionaly(Resource resource, String ifNoneExistCriteria) + { + return delegate.createConditionaly(PreferReturnType.MINIMAL, resource, ifNoneExistCriteria).getId(); + } + + @Override + public IdType createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return delegate.createBinary(PreferReturnType.MINIMAL, in, mediaType, securityContextReference).getId(); + } + + @Override + public IdType update(Resource resource) + { + return delegate.update(PreferReturnType.MINIMAL, resource).getId(); + } + + @Override + public IdType updateConditionaly(Resource resource, Map> criteria) + { + return delegate.updateConditionaly(PreferReturnType.MINIMAL, resource, criteria).getId(); + } + + @Override + public IdType updateBinary(String id, InputStream in, MediaType mediaType, String securityContextReference) + { + return delegate.updateBinary(PreferReturnType.MINIMAL, id, in, mediaType, securityContextReference).getId(); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return delegate.postBundle(PreferReturnType.MINIMAL, bundle); + } + + @Override + public PreferReturnMinimal withRetry(int nTimes, long delayMillis) + { + if (nTimes < 0) + throw new IllegalArgumentException("nTimes < 0"); + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new PreferReturnMinimalRetryImpl(delegate, nTimes, delayMillis); + } + + @Override + public PreferReturnMinimal withRetryForever(long delayMillis) + { + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new PreferReturnMinimalRetryImpl(delegate, RETRY_FOREVER, delayMillis); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeRetryImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeRetryImpl.java new file mode 100644 index 000000000..1ba8fd6f2 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeRetryImpl.java @@ -0,0 +1,73 @@ +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 dev.dsf.fhir.prefer.PreferReturnType; +import jakarta.ws.rs.core.MediaType; + +class PreferReturnOutcomeRetryImpl extends AbstractFhirWebserviceClientJerseyWithRetry implements PreferReturnOutcome +{ + PreferReturnOutcomeRetryImpl(FhirWebserviceClientJersey delegate, int nTimes, long delayMillis) + { + super(delegate, nTimes, delayMillis); + } + + @Override + public OperationOutcome create(Resource resource) + { + return retry(nTimes, delayMillis, + () -> delegate.create(PreferReturnType.OPERATION_OUTCOME, resource).getOperationOutcome()); + } + + @Override + public OperationOutcome createConditionaly(Resource resource, String ifNoneExistCriteria) + { + return retry(nTimes, delayMillis, + () -> delegate.createConditionaly(PreferReturnType.OPERATION_OUTCOME, resource, ifNoneExistCriteria) + .getOperationOutcome()); + } + + @Override + public OperationOutcome createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return retry(nTimes, delayMillis, + () -> delegate.createBinary(PreferReturnType.OPERATION_OUTCOME, in, mediaType, securityContextReference) + .getOperationOutcome()); + } + + @Override + public OperationOutcome update(Resource resource) + { + return retry(nTimes, delayMillis, + () -> delegate.update(PreferReturnType.OPERATION_OUTCOME, resource).getOperationOutcome()); + } + + @Override + public OperationOutcome updateConditionaly(Resource resource, Map> criteria) + { + return retry(nTimes, delayMillis, () -> delegate + .updateConditionaly(PreferReturnType.OPERATION_OUTCOME, resource, criteria).getOperationOutcome()); + } + + @Override + public OperationOutcome updateBinary(String id, InputStream in, MediaType mediaType, + String securityContextReference) + { + return retry(nTimes, delayMillis, + () -> delegate + .updateBinary(PreferReturnType.OPERATION_OUTCOME, id, in, mediaType, securityContextReference) + .getOperationOutcome()); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return retry(nTimes, delayMillis, () -> delegate.postBundle(PreferReturnType.OPERATION_OUTCOME, bundle)); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeWithRetryImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeWithRetryImpl.java new file mode 100644 index 000000000..7ec4b3d68 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeWithRetryImpl.java @@ -0,0 +1,89 @@ +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 dev.dsf.fhir.prefer.PreferReturnType; +import jakarta.ws.rs.core.MediaType; + +class PreferReturnOutcomeWithRetryImpl implements PreferReturnOutcomeWithRetry +{ + private final FhirWebserviceClientJersey delegate; + + PreferReturnOutcomeWithRetryImpl(FhirWebserviceClientJersey delegate) + { + this.delegate = delegate; + } + + @Override + public OperationOutcome create(Resource resource) + { + return delegate.create(PreferReturnType.OPERATION_OUTCOME, resource).getOperationOutcome(); + } + + @Override + public OperationOutcome createConditionaly(Resource resource, String ifNoneExistCriteria) + { + return delegate.createConditionaly(PreferReturnType.OPERATION_OUTCOME, resource, ifNoneExistCriteria) + .getOperationOutcome(); + } + + @Override + public OperationOutcome createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return delegate.createBinary(PreferReturnType.OPERATION_OUTCOME, in, mediaType, securityContextReference) + .getOperationOutcome(); + } + + @Override + public OperationOutcome update(Resource resource) + { + return delegate.update(PreferReturnType.OPERATION_OUTCOME, resource).getOperationOutcome(); + } + + @Override + public OperationOutcome updateConditionaly(Resource resource, Map> criteria) + { + return delegate.updateConditionaly(PreferReturnType.OPERATION_OUTCOME, resource, criteria) + .getOperationOutcome(); + } + + @Override + public OperationOutcome updateBinary(String id, InputStream in, MediaType mediaType, + String securityContextReference) + { + return delegate.updateBinary(PreferReturnType.OPERATION_OUTCOME, id, in, mediaType, securityContextReference) + .getOperationOutcome(); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return delegate.postBundle(PreferReturnType.OPERATION_OUTCOME, bundle); + } + + @Override + public PreferReturnOutcome withRetry(int nTimes, long delayMillis) + { + if (nTimes < 0) + throw new IllegalArgumentException("nTimes < 0"); + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new PreferReturnOutcomeRetryImpl(delegate, nTimes, delayMillis); + } + + @Override + public PreferReturnOutcome withRetryForever(long delayMillis) + { + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new PreferReturnOutcomeRetryImpl(delegate, RETRY_FOREVER, delayMillis); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/prefer/PreferHandlingType.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/prefer/PreferHandlingType.java new file mode 100644 index 000000000..1c22f452b --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/prefer/PreferHandlingType.java @@ -0,0 +1,31 @@ +package dev.dsf.fhir.prefer; + +public enum PreferHandlingType +{ + STRICT("handling=strict"), LENIENT("handling=lenient"); + + private final String headerValue; + + PreferHandlingType(String headerValue) + { + this.headerValue = headerValue; + } + + public static PreferHandlingType fromString(String prefer) + { + if (prefer == null) + return LENIENT; + + return switch (prefer) + { + case "handling=strict" -> STRICT; + case "handling=lenient" -> LENIENT; + default -> LENIENT; + }; + } + + public String getHeaderValue() + { + return headerValue; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/prefer/PreferReturnType.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/prefer/PreferReturnType.java new file mode 100644 index 000000000..653c6352a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/prefer/PreferReturnType.java @@ -0,0 +1,32 @@ +package dev.dsf.fhir.prefer; + +public enum PreferReturnType +{ + MINIMAL("return=minimal"), REPRESENTATION("return=representation"), OPERATION_OUTCOME("return=OperationOutcome"); + + private final String headerValue; + + PreferReturnType(String headerValue) + { + this.headerValue = headerValue; + } + + public static PreferReturnType fromString(String prefer) + { + if (prefer == null) + return REPRESENTATION; + + return switch (prefer) + { + case "return=minimal" -> MINIMAL; + case "return=OperationOutcome" -> OPERATION_OUTCOME; + case "return=representation" -> REPRESENTATION; + default -> REPRESENTATION; + }; + } + + public String getHeaderValue() + { + return headerValue; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceCleaner.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceCleaner.java new file mode 100644 index 000000000..310e533ae --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceCleaner.java @@ -0,0 +1,18 @@ +package dev.dsf.fhir.service; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Resource; + +public interface ReferenceCleaner +{ + /** + * Removes embedded resources from references within {@link Bundle} entries + * + * @param + * the resource type + * @param resource + * the resource to clean, may be null + * @return null if given resource is null, cleaned up resource (same instance) + */ + R cleanReferenceResourcesIfBundle(R resource); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceCleanerImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceCleanerImpl.java new file mode 100644 index 000000000..93cd11781 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceCleanerImpl.java @@ -0,0 +1,63 @@ +package dev.dsf.fhir.service; + +import java.util.Objects; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.DomainResource; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +public class ReferenceCleanerImpl implements ReferenceCleaner, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(ReferenceCleanerImpl.class); + + private final ReferenceExtractor referenceExtractor; + + public ReferenceCleanerImpl(ReferenceExtractor referenceExtractor) + { + this.referenceExtractor = referenceExtractor; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(referenceExtractor, "referenceExtractor"); + } + + @Override + public R cleanReferenceResourcesIfBundle(R resource) + { + if (resource == null) + return null; + + if (resource instanceof Bundle b) + b.getEntry().stream().map(BundleEntryComponent::getResource).forEach(this::fixBundleEntry); + + return resource; + } + + private void fixBundleEntry(Resource resource) + { + if (resource instanceof Bundle) + { + cleanReferenceResourcesIfBundle(resource); + } + else + { + Stream references = referenceExtractor.getReferences(resource); + + references.filter(r -> r != null).forEach(r -> r.setResource(null)); + + if (resource instanceof DomainResource d && d.hasContained()) + { + logger.warn("{} has contained resources, removing resources", resource.getClass().getName()); + d.setContained(null); + } + } + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceExtractor.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceExtractor.java new file mode 100644 index 000000000..238256c28 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceExtractor.java @@ -0,0 +1,11 @@ +package dev.dsf.fhir.service; + +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; + +public interface ReferenceExtractor +{ + Stream getReferences(Resource resource); +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java new file mode 100644 index 000000000..5abacbd7d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java @@ -0,0 +1,606 @@ +package dev.dsf.fhir.service; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.ActivityDefinition; +import org.hl7.fhir.r4.model.BackboneElement; +import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.DocumentReference; +import org.hl7.fhir.r4.model.DocumentReference.DocumentReferenceContextComponent; +import org.hl7.fhir.r4.model.DocumentReference.DocumentReferenceRelatesToComponent; +import org.hl7.fhir.r4.model.DomainResource; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.HealthcareService; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent; +import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent; +import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupStratifierComponent; +import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupComponent; +import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupPopulationComponent; +import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.OrganizationAffiliation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Patient.ContactComponent; +import org.hl7.fhir.r4.model.Patient.PatientLinkComponent; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Practitioner.PractitionerQualificationComponent; +import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.Provenance.ProvenanceAgentComponent; +import org.hl7.fhir.r4.model.Provenance.ProvenanceEntityComponent; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.ResearchStudy; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.Subscription; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.ValueSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReferenceExtractorImpl implements ReferenceExtractor +{ + private static final Logger logger = LoggerFactory.getLogger(ReferenceExtractorImpl.class); + + private Stream getReference(R resource, Predicate hasReference, + Function getReference) + { + return hasReference.test(resource) ? Stream.of(getReference.apply(resource)) : Stream.empty(); + } + + private Stream getReferences(R resource, Predicate hasReference, + Function> getReference) + { + return hasReference.test(resource) ? Stream.of(getReference.apply(resource)).flatMap(List::stream) + : Stream.empty(); + } + + private Stream getBackboneElementsReference(R resource, + Predicate hasBackboneElements, Function> getBackboneElements, Predicate hasReference, + Function getReference) + { + if (hasBackboneElements.test(resource)) + { + List backboneElements = getBackboneElements.apply(resource); + return backboneElements.stream().map(e -> getReference(e, hasReference, getReference)) + .flatMap(Function.identity()); + } + else + return Stream.empty(); + } + + private Stream getReference(E backboneElement, Predicate hasReference, + Function getReference) + { + return hasReference.test(backboneElement) ? Stream.of(getReference.apply(backboneElement)) : Stream.empty(); + } + + private Stream getBackboneElementReferences( + R resource, Predicate hasBackboneElement, Function getBackboneElement, Predicate hasReference, + Function> getReference) + { + if (hasBackboneElement.test(resource)) + { + E backboneElement = getBackboneElement.apply(resource); + return getReferences(backboneElement, hasReference, getReference); + } + else + return Stream.empty(); + } + + private Stream getBackboneElementReference( + R resource, Predicate hasBackboneElement, Function getBackboneElement, Predicate hasReference, + Function getReference) + { + if (hasBackboneElement.test(resource)) + { + E backboneElement = getBackboneElement.apply(resource); + return getReference(backboneElement, hasReference, getReference); + } + else + return Stream.empty(); + } + + private Stream getBackboneElements2Reference( + R resource, Predicate hasBackboneElements1, Function> getBackboneElements1, + Predicate hasBackboneElements2, Function> getBackboneElements2, Predicate hasReference, + Function getReference) + { + if (hasBackboneElements1.test(resource)) + { + List backboneElements1 = getBackboneElements1.apply(resource); + return backboneElements1.stream().filter(e1 -> hasBackboneElements2.test(e1)) + .flatMap(e1 -> getBackboneElements2.apply(e1).stream()) + .map(e2 -> getReference(e2, hasReference, getReference)).flatMap(Function.identity()); + } + else + return Stream.empty(); + } + + private Stream getBackboneElements4Reference( + R resource, Predicate hasBackboneElements1, Function> getBackboneElements1, + Predicate hasBackboneElements2, Function> getBackboneElements2, + Predicate hasBackboneElements3, Function> getBackboneElements3, + Predicate hasBackboneElements4, Function> getBackboneElements4, Predicate hasReference, + Function getReference) + { + if (hasBackboneElements1.test(resource)) + { + List backboneElements1 = getBackboneElements1.apply(resource); + return backboneElements1.stream().filter(e1 -> hasBackboneElements2.test(e1)) + .flatMap(e1 -> getBackboneElements2.apply(e1).stream()).filter(e2 -> hasBackboneElements3.test(e2)) + .flatMap(e2 -> getBackboneElements3.apply(e2).stream()).filter(e3 -> hasBackboneElements4.test(e3)) + .flatMap(e3 -> getBackboneElements4.apply(e3).stream()) + .map(e4 -> getReference(e4, hasReference, getReference)).flatMap(Function.identity()); + } + else + return Stream.empty(); + } + + private Stream getReferences(E backboneElement, Predicate hasReference, + Function> getReference) + { + return hasReference.test(backboneElement) ? Stream.of(getReference.apply(backboneElement)).flatMap(List::stream) + : Stream.empty(); + } + + private Stream getExtensionReferences(DomainResource resource) + { + var extensions = resource.getExtension().stream().filter(e -> e.getValue() instanceof Reference) + .map(e -> (Reference) e.getValue()); + + var extensionExtensions = resource.getExtension().stream().flatMap(this::getExtensionReferences); + + return Stream.concat(extensions, extensionExtensions); + } + + private Stream getExtensionReferences(BackboneElement resource) + { + var extensions = resource.getExtension().stream().filter(e -> e.getValue() instanceof Reference) + .map(e -> (Reference) e.getValue()); + + var extensionExtensions = resource.getExtension().stream().flatMap(this::getExtensionReferences); + + return Stream.concat(extensions, extensionExtensions); + } + + private Stream getExtensionReferences(Extension resource) + { + var extensions = resource.getExtension().stream().filter(e -> e.getValue() instanceof Reference) + .map(e -> (Reference) e.getValue()); + + var extensionExtensions = resource.getExtension().stream().flatMap(this::getExtensionReferences); + + return Stream.concat(extensions, extensionExtensions); + } + + @SafeVarargs + private Stream concat(Stream... streams) + { + if (streams.length == 0) + return Stream.empty(); + else if (streams.length == 1) + return streams[0]; + else if (streams.length == 2) + return Stream.concat(streams[0], streams[1]); + else + return Arrays.stream(streams).flatMap(Function.identity()); + } + + @Override + public Stream getReferences(Resource resource) + { + return switch (resource) + { + case null -> Stream.empty(); + + case ActivityDefinition ad -> getReferences(ad); + + // not implemented yet, special rules apply for tmp ids + // case Bundle b -> getReferences(b); + + case Binary b -> getReferences(b); + case CodeSystem cs -> getReferences(cs); + case DocumentReference dr -> getReferences(dr); + case Endpoint e -> getReferences(e); + case Group g -> getReferences(g); + case HealthcareService hs -> getReferences(hs); + case Library l -> getReferences(l); + case Location l -> getReferences(l); + case Measure m -> getReferences(m); + case MeasureReport mr -> getReferences(mr); + case NamingSystem ns -> getReferences(ns); + case OperationOutcome oo -> getReferences(oo); + case Organization o -> getReferences(o); + case OrganizationAffiliation oa -> getReferences(oa); + case Patient p -> getReferences(p); + case Practitioner p -> getReferences(p); + case PractitionerRole pr -> getReferences(pr); + case Provenance p -> getReferences(p); + case Questionnaire q -> getReferences(q); + case QuestionnaireResponse qr -> getReferences(qr); + case ResearchStudy rs -> getReferences(rs); + case StructureDefinition sd -> getReferences(sd); + case Subscription s -> getReferences(s); + case Task t -> getReferences(t); + case ValueSet vs -> getReferences(vs); + + case DomainResource dr -> { + logger.debug("DomainResource of type {} not supported, returning extension references only", + dr.getClass().getName()); + yield getExtensionReferences(dr); + } + + default -> { + logger.debug("Resource of type {} not supported, returning no references", + resource.getClass().getName()); + yield Stream.empty(); + } + }; + } + + private Stream getReferences(ActivityDefinition resource) + { + var subjectReference = getReference(resource, ActivityDefinition::hasSubjectReference, + ActivityDefinition::getSubjectReference); + var location = getReference(resource, ActivityDefinition::hasLocation, ActivityDefinition::getLocation); + var productReference = getReference(resource, ActivityDefinition::hasProductReference, + ActivityDefinition::getProductReference); + var specimenRequirement = getReferences(resource, ActivityDefinition::hasSpecimenRequirement, + ActivityDefinition::getSpecimenRequirement); + var observationRequirement = getReferences(resource, ActivityDefinition::hasObservationRequirement, + ActivityDefinition::getObservationRequirement); + var observationResultRequirement = getReferences(resource, ActivityDefinition::hasObservationResultRequirement, + ActivityDefinition::getObservationResultRequirement); + + var extensionReferences = getExtensionReferences(resource); + + return concat(subjectReference, location, productReference, specimenRequirement, observationRequirement, + observationResultRequirement, extensionReferences); + } + + private Stream getReferences(Binary resource) + { + var securityContext = getReference(resource, Binary::hasSecurityContext, Binary::getSecurityContext); + + return securityContext; + } + + private Stream getReferences(CodeSystem resource) + { + var extensionReferences = getExtensionReferences(resource); + + return extensionReferences; + } + + private Stream getReferences(DocumentReference resource) + { + var subject = getReference(resource, DocumentReference::hasSubject, DocumentReference::getSubject); + var author = getReferences(resource, DocumentReference::hasAuthor, DocumentReference::getAuthor); + var authenticator = getReference(resource, DocumentReference::hasAuthenticator, + DocumentReference::getAuthenticator); + var custodian = getReference(resource, DocumentReference::hasCustodian, DocumentReference::getCustodian); + var relatesToTarget = getBackboneElementsReference(resource, DocumentReference::hasRelatesTo, + DocumentReference::getRelatesTo, DocumentReferenceRelatesToComponent::hasTarget, + DocumentReferenceRelatesToComponent::getTarget); + var contextEncounters = getBackboneElementReferences(resource, DocumentReference::hasContent, + DocumentReference::getContext, DocumentReferenceContextComponent::hasEncounter, + DocumentReferenceContextComponent::getEncounter); + var contextSourcePatientInfo = getBackboneElementReference(resource, DocumentReference::hasContent, + DocumentReference::getContext, DocumentReferenceContextComponent::hasSourcePatientInfo, + DocumentReferenceContextComponent::getSourcePatientInfo); + var contextRelated = getBackboneElementReferences(resource, DocumentReference::hasContent, + DocumentReference::getContext, DocumentReferenceContextComponent::hasRelated, + DocumentReferenceContextComponent::getRelated); + + var extensionReferences = getExtensionReferences(resource); + + return concat(subject, author, authenticator, custodian, relatesToTarget, contextEncounters, + contextSourcePatientInfo, contextRelated, extensionReferences); + } + + private Stream getReferences(Endpoint resource) + { + var managingOrganization = getReference(resource, Endpoint::hasManagingOrganization, + Endpoint::getManagingOrganization); + + var extensionReferences = getExtensionReferences(resource); + + return concat(managingOrganization, extensionReferences); + } + + private Stream getReferences(Group resource) + { + var managingEntity = getReference(resource, Group::hasManagingEntity, Group::getManagingEntity); + var memberEntities = getBackboneElementsReference(resource, Group::hasMember, Group::getMember, + Group.GroupMemberComponent::hasEntity, Group.GroupMemberComponent::getEntity); + + var extensionReferences = getExtensionReferences(resource); + + return concat(managingEntity, memberEntities, extensionReferences); + } + + private Stream getReferences(HealthcareService resource) + { + var providedBy = getReference(resource, HealthcareService::hasProvidedBy, HealthcareService::getProvidedBy); + var locations = getReferences(resource, HealthcareService::hasLocation, HealthcareService::getLocation); + var coverageAreas = getReferences(resource, HealthcareService::hasCoverageArea, + HealthcareService::getCoverageArea); + var endpoints = getReferences(resource, HealthcareService::hasEndpoint, HealthcareService::getEndpoint); + + var extensionReferences = getExtensionReferences(resource); + + return concat(providedBy, locations, coverageAreas, endpoints, extensionReferences); + } + + private Stream getReferences(Library resource) + { + var subject = getReference(resource, Library::hasSubjectReference, Library::getSubjectReference); + + var extensionReferences = getExtensionReferences(resource); + + return concat(subject, extensionReferences); + } + + private Stream getReferences(Location resource) + { + var managingOrganization = getReference(resource, Location::hasManagingOrganization, + Location::getManagingOrganization); + var partOf = getReference(resource, Location::hasPartOf, Location::getPartOf); + var endpoints = getReferences(resource, Location::hasEndpoint, Location::getEndpoint); + + var extensionReferences = getExtensionReferences(resource); + + return concat(managingOrganization, partOf, endpoints, extensionReferences); + } + + private Stream getReferences(Measure resource) + { + var subject = getReference(resource, Measure::hasSubjectReference, Measure::getSubjectReference); + + var extensionReferences = getExtensionReferences(resource); + + return concat(subject, extensionReferences); + } + + private Stream getReferences(MeasureReport resource) + { + var subject = getReference(resource, MeasureReport::hasSubject, MeasureReport::getSubject); + var reporter = getReference(resource, MeasureReport::hasReporter, MeasureReport::getReporter); + var subjectResults1 = getBackboneElements2Reference(resource, MeasureReport::hasGroup, MeasureReport::getGroup, + MeasureReportGroupComponent::hasPopulation, MeasureReportGroupComponent::getPopulation, + MeasureReportGroupPopulationComponent::hasSubjectResults, + MeasureReportGroupPopulationComponent::getSubjectResults); + var subjectResults2 = getBackboneElements4Reference(resource, MeasureReport::hasGroup, MeasureReport::getGroup, + MeasureReportGroupComponent::hasStratifier, MeasureReportGroupComponent::getStratifier, + MeasureReportGroupStratifierComponent::hasStratum, MeasureReportGroupStratifierComponent::getStratum, + StratifierGroupComponent::hasPopulation, StratifierGroupComponent::getPopulation, + StratifierGroupPopulationComponent::hasSubjectResults, + StratifierGroupPopulationComponent::getSubjectResults); + var evaluatedResource = getReferences(resource, MeasureReport::hasEvaluatedResource, + MeasureReport::getEvaluatedResource); + + var extensionReferences = getExtensionReferences(resource); + + return concat(subject, reporter, subjectResults1, subjectResults2, evaluatedResource, extensionReferences); + } + + private Stream getReferences(NamingSystem resource) + { + var extensionReferences = getExtensionReferences(resource); + + return extensionReferences; + } + + private Stream getReferences(OperationOutcome resource) + { + return getExtensionReferences(resource); + } + + private Stream getReferences(Organization resource) + { + var partOf = getReference(resource, Organization::hasPartOf, Organization::getPartOf); + var endpoints = getReferences(resource, Organization::hasEndpoint, Organization::getEndpoint); + + var extensionReferences = getExtensionReferences(resource); + + return concat(partOf, endpoints, extensionReferences); + } + + private Stream getReferences(OrganizationAffiliation resource) + { + var organization = getReference(resource, OrganizationAffiliation::hasOrganization, + OrganizationAffiliation::getOrganization); + var participatingOrganization = getReference(resource, OrganizationAffiliation::hasParticipatingOrganization, + OrganizationAffiliation::getParticipatingOrganization); + var network = getReferences(resource, OrganizationAffiliation::hasNetwork, OrganizationAffiliation::getNetwork); + var location = getReferences(resource, OrganizationAffiliation::hasLocation, + OrganizationAffiliation::getLocation); + var healthcareService = getReferences(resource, OrganizationAffiliation::hasHealthcareService, + OrganizationAffiliation::getHealthcareService); + var endpoint = getReferences(resource, OrganizationAffiliation::hasEndpoint, + OrganizationAffiliation::getEndpoint); + + var extensionReferences = getExtensionReferences(resource); + + return concat(organization, participatingOrganization, network, location, healthcareService, endpoint, + extensionReferences); + } + + private Stream getReferences(Patient resource) + { + var contactsOrganization = getBackboneElementsReference(resource, Patient::hasContact, Patient::getContact, + ContactComponent::hasOrganization, ContactComponent::getOrganization); + var generalPractitioners = getReferences(resource, Patient::hasGeneralPractitioner, + Patient::getGeneralPractitioner); + var managingOrganization = getReference(resource, Patient::hasManagingOrganization, + Patient::getManagingOrganization); + var linksOther = getBackboneElementsReference(resource, Patient::hasLink, Patient::getLink, + PatientLinkComponent::hasOther, PatientLinkComponent::getOther); + + var extensionReferences = getExtensionReferences(resource); + + return concat(contactsOrganization, generalPractitioners, managingOrganization, linksOther, + extensionReferences); + } + + private Stream getReferences(Practitioner resource) + { + var qualificationsIssuer = getBackboneElementsReference(resource, Practitioner::hasQualification, + Practitioner::getQualification, PractitionerQualificationComponent::hasIssuer, + PractitionerQualificationComponent::getIssuer); + + var extensionReferences = getExtensionReferences(resource); + + return concat(qualificationsIssuer, extensionReferences); + } + + private Stream getReferences(PractitionerRole resource) + { + var practitioner = getReference(resource, PractitionerRole::hasPractitioner, PractitionerRole::getPractitioner); + var organization = getReference(resource, PractitionerRole::hasOrganization, PractitionerRole::getOrganization); + var locations = getReferences(resource, PractitionerRole::hasLocation, PractitionerRole::getLocation); + var healthcareServices = getReferences(resource, PractitionerRole::hasHealthcareService, + PractitionerRole::getHealthcareService); + var endpoints = getReferences(resource, PractitionerRole::hasEndpoint, PractitionerRole::getEndpoint); + + var extensionReferences = getExtensionReferences(resource); + + return concat(practitioner, organization, locations, healthcareServices, endpoints, extensionReferences); + } + + private Stream getReferences(Provenance resource) + { + var targets = getReferences(resource, Provenance::hasTarget, Provenance::getTarget); + var location = getReference(resource, Provenance::hasLocation, Provenance::getLocation); + var agentsWho = getBackboneElementsReference(resource, Provenance::hasAgent, Provenance::getAgent, + ProvenanceAgentComponent::hasWho, ProvenanceAgentComponent::getWho); + var agentsOnBehalfOf = getBackboneElementsReference(resource, Provenance::hasAgent, Provenance::getAgent, + ProvenanceAgentComponent::hasOnBehalfOf, ProvenanceAgentComponent::getOnBehalfOf); + var entitiesWhat = getBackboneElementsReference(resource, Provenance::hasEntity, Provenance::getEntity, + ProvenanceEntityComponent::hasWhat, ProvenanceEntityComponent::getWhat); + + var extensionReferences = getExtensionReferences(resource); + + return concat(targets, location, agentsWho, agentsOnBehalfOf, entitiesWhat, extensionReferences); + } + + private Stream getReferences(Questionnaire resource) + { + var enableWhen = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, + Questionnaire.QuestionnaireItemComponent::hasEnableWhen, + Questionnaire.QuestionnaireItemComponent::getEnableWhen, + Questionnaire.QuestionnaireItemEnableWhenComponent::hasAnswerReference, + Questionnaire.QuestionnaireItemEnableWhenComponent::getAnswerReference); + var answerOption = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, + Questionnaire.QuestionnaireItemComponent::hasAnswerOption, + Questionnaire.QuestionnaireItemComponent::getAnswerOption, + Questionnaire.QuestionnaireItemAnswerOptionComponent::hasValueReference, + Questionnaire.QuestionnaireItemAnswerOptionComponent::getValueReference); + var initial = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, + Questionnaire.QuestionnaireItemComponent::hasInitial, + Questionnaire.QuestionnaireItemComponent::getInitial, + Questionnaire.QuestionnaireItemInitialComponent::hasValueReference, + Questionnaire.QuestionnaireItemInitialComponent::getValueReference); + + var extensionReferences = getExtensionReferences(resource); + + return concat(enableWhen, answerOption, initial, extensionReferences); + } + + private Stream getReferences(QuestionnaireResponse resource) + { + var author = getReference(resource, QuestionnaireResponse::hasAuthor, QuestionnaireResponse::getAuthor); + var basedOn = getReferences(resource, QuestionnaireResponse::hasBasedOn, QuestionnaireResponse::getBasedOn); + var encounter = getReference(resource, QuestionnaireResponse::hasEncounter, + QuestionnaireResponse::getEncounter); + var partOf = getReferences(resource, QuestionnaireResponse::hasPartOf, QuestionnaireResponse::getPartOf); + var source = getReference(resource, QuestionnaireResponse::hasSource, QuestionnaireResponse::getSource); + var subject = getReference(resource, QuestionnaireResponse::hasSubject, QuestionnaireResponse::getSubject); + + var extensionReferences = getExtensionReferences(resource); + + return concat(author, basedOn, encounter, partOf, source, subject, extensionReferences); + } + + private Stream getReferences(ResearchStudy resource) + { + var protocols = getReferences(resource, ResearchStudy::hasProtocol, ResearchStudy::getProtocol); + var partOfs = getReferences(resource, ResearchStudy::hasPartOf, ResearchStudy::getPartOf); + var enrollments = getReferences(resource, ResearchStudy::hasEnrollment, ResearchStudy::getEnrollment); + var sponsor = getReference(resource, ResearchStudy::hasSponsor, ResearchStudy::getSponsor); + var principalInvestigator = getReference(resource, ResearchStudy::hasPrincipalInvestigator, + ResearchStudy::getPrincipalInvestigator); + var sites = getReferences(resource, ResearchStudy::hasSite, ResearchStudy::getSite); + + var extensionReferences = getExtensionReferences(resource); + + return concat(protocols, partOfs, enrollments, sponsor, principalInvestigator, sites, extensionReferences); + } + + private Stream getReferences(StructureDefinition resource) + { + var extensionReferences = getExtensionReferences(resource); + + return extensionReferences; + } + + private Stream getReferences(Subscription resource) + { + var extensionReferences = getExtensionReferences(resource); + + return extensionReferences; + } + + private Stream getReferences(Task resource) + { + var basedOns = getReferences(resource, Task::hasBasedOn, Task::getBasedOn); + var partOfs = getReferences(resource, Task::hasPartOf, Task::getPartOf); + var focus = getReference(resource, Task::hasFocus, Task::getFocus); + var forRef = getReference(resource, Task::hasFor, Task::getFor); + var encounter = getReference(resource, Task::hasEncounter, Task::getEncounter); + var requester = getReference(resource, Task::hasRequester, Task::getRequester); + var owner = getReference(resource, Task::hasOwner, Task::getOwner); + var location = getReference(resource, Task::hasLocation, Task::getLocation); + var reasonReference = getReference(resource, Task::hasReasonReference, Task::getReasonReference); + var insurance = getReferences(resource, Task::hasInsurance, Task::getInsurance); + var relevanteHistories = getReferences(resource, Task::hasRelevantHistory, Task::getRelevantHistory); + var restrictionRecipiets = getBackboneElementReferences(resource, Task::hasRestriction, Task::getRestriction, + Task.TaskRestrictionComponent::hasRecipient, Task.TaskRestrictionComponent::getRecipient); + + var inputReferences = resource.getInput().stream().filter(in -> in.getValue() instanceof Reference) + .map(in -> (Reference) in.getValue()); + var inputExtensionReferences = resource.getInput().stream().flatMap(this::getExtensionReferences); + + var outputReferences = resource.getOutput().stream().filter(out -> out.getValue() instanceof Reference) + .map(in -> (Reference) in.getValue()); + var outputExtensionReferences = resource.getOutput().stream().flatMap(this::getExtensionReferences); + + var extensionReferences = getExtensionReferences(resource); + + return concat(basedOns, partOfs, focus, forRef, encounter, requester, owner, location, reasonReference, + insurance, relevanteHistories, restrictionRecipiets, inputReferences, inputExtensionReferences, + outputReferences, outputExtensionReferences, extensionReferences); + } + + private Stream getReferences(ValueSet resource) + { + var extensionReferences = getExtensionReferences(resource); + + return extensionReferences; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/resources/META-INF/services/dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/resources/META-INF/services/dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder new file mode 100644 index 000000000..0ac66195a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/main/resources/META-INF/services/dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder @@ -0,0 +1 @@ +dev.dsf.bpe.v1.plugin.ProcessPluginApiBuilderImpl \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/plugin/ProcessPluginImplTest.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/plugin/ProcessPluginImplTest.java similarity index 84% rename from dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/plugin/ProcessPluginImplTest.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/plugin/ProcessPluginImplTest.java index 4d17cf131..0d10752de 100644 --- a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/plugin/ProcessPluginImplTest.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/plugin/ProcessPluginImplTest.java @@ -23,30 +23,34 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; +import org.springframework.context.support.AbstractApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.StandardEnvironment; import com.fasterxml.jackson.databind.ObjectMapper; import ca.uhn.fhir.context.FhirContext; -import dev.dsf.bpe.plugin.BpmnFileAndModel; -import dev.dsf.bpe.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.AbstractProcessPlugin; +import dev.dsf.bpe.api.plugin.BpmnFileAndModel; +import dev.dsf.bpe.api.plugin.ProcessPlugin; import dev.dsf.bpe.v1.ProcessPluginApi; import dev.dsf.bpe.v1.ProcessPluginApiImpl; import dev.dsf.bpe.v1.ProcessPluginDefinition; import dev.dsf.bpe.v1.activity.AbstractServiceDelegate; import dev.dsf.bpe.v1.config.ProxyConfig; import dev.dsf.bpe.v1.service.EndpointProvider; -import dev.dsf.bpe.v1.service.FhirWebserviceClientProvider; +import dev.dsf.bpe.v1.service.FhirWebserviceClientProviderImpl; import dev.dsf.bpe.v1.service.MailService; import dev.dsf.bpe.v1.service.OrganizationProvider; import dev.dsf.bpe.v1.service.QuestionnaireResponseHelper; import dev.dsf.bpe.v1.service.TaskHelper; +import dev.dsf.bpe.v1.variables.ObjectMapperFactory; import dev.dsf.bpe.v1.variables.Variables; -import dev.dsf.bpe.variables.ObjectMapperFactory; import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelper; import dev.dsf.fhir.authorization.read.ReadAccessHelper; @@ -139,7 +143,8 @@ protected void doExecute(DelegateExecution execution, Variables variables) throw private final ProxyConfig proxyConfig = mock(ProxyConfig.class); private final EndpointProvider endpointProvider = mock(EndpointProvider.class); private final FhirContext fhirContext = FhirContext.forR4(); - private final FhirWebserviceClientProvider fhirWebserviceClientProvider = mock(FhirWebserviceClientProvider.class); + private final FhirWebserviceClientProviderImpl fhirWebserviceClientProvider = mock( + FhirWebserviceClientProviderImpl.class); private final MailService mailService = mock(MailService.class); private final ObjectMapper objectMapper = ObjectMapperFactory.createObjectMapper(fhirContext); private final OrganizationProvider organizationProvider = mock(OrganizationProvider.class); @@ -153,11 +158,23 @@ protected void doExecute(DelegateExecution execution, Variables variables) throw processAuthorizationHelper, questionnaireResponseHelper, readAccessHelper, taskHelper); private final ConfigurableEnvironment environment = new StandardEnvironment(); + private final AbstractApplicationContext apiApplicationContext; + + public ProcessPluginImplTest() + { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerSingleton("processPluginApiV1", processPluginApi); + factory.registerSingleton("fhirContext", fhirContext); + + apiApplicationContext = new AnnotationConfigApplicationContext(factory); + apiApplicationContext.refresh(); + } + @Test public void testInitializeAndValidateResourcesAllNull() throws Exception { var definition = createPluginDefinition(null, null, null, null, null); - var plugin = createPlugin(definition, false); + AbstractProcessPlugin plugin = createPlugin(definition, false); assertFalse(plugin.initializeAndValidateResources(null)); try @@ -192,7 +209,7 @@ public void testInitializeAndValidateResourcesEmptySpringConfigBpmnAndFhirResour { var definition = createPluginDefinition("1.0.0.0", LocalDate.now(), Collections.emptyList(), Collections.emptyList(), Collections.emptyMap()); - var plugin = createPlugin(definition, false); + AbstractProcessPlugin plugin = createPlugin(definition, false); assertFalse(plugin.initializeAndValidateResources(null)); try @@ -228,7 +245,7 @@ public void testInitializeAndValidateResourcesNotExistingModelAndFhirResources() var definition = createPluginDefinition("1.0.0.0", LocalDate.now(), List.of(TestConfig.class), List.of("test-plugin/does_not_exist.bpmn"), Map.of("testorg_test", List.of("test-plugin/does_not_exist.xml"))); - var plugin = createPlugin(definition, false); + AbstractProcessPlugin plugin = createPlugin(definition, false); assertFalse(plugin.initializeAndValidateResources(null)); try @@ -263,7 +280,7 @@ public void testInitializeAndValidateResourcesNotExistingFhirResources() throws { var definition = createPluginDefinition("1.0.0.0", LocalDate.now(), List.of(TestConfig.class), List.of("test-plugin/test.bpmn"), Map.of("testorg_test", List.of("test-plugin/does_not_exist.xml"))); - var plugin = createPlugin(definition, false); + AbstractProcessPlugin plugin = createPlugin(definition, false); assertFalse(plugin.initializeAndValidateResources(null)); try @@ -299,14 +316,13 @@ public void testInitializeAndValidateResources() throws Exception var definition = createPluginDefinition("1.0.0.0", LocalDate.now(), List.of(TestConfig.class), List.of("test-plugin/test.bpmn"), Map.of("testorg_test", List.of("test-plugin/ActivityDefinition_test.xml"))); - var plugin = createPlugin(definition, false); + AbstractProcessPlugin plugin = createPlugin(definition, false); assertTrue(plugin.initializeAndValidateResources("test.org")); assertNotNull(plugin.getApplicationContext()); assertNotNull(plugin.getProcessModels()); assertNotNull(plugin.getFhirResources()); - List models = plugin.getProcessModels(); assertEquals(1, models.size()); BpmnFileAndModel bpmnFileAndModel = models.get(0); @@ -327,7 +343,7 @@ public void testInitializeAndValidateResources() throws Exception assertEquals(1, camundaPropertyElements.size()); CamundaProperty property = camundaPropertyElements.stream().findFirst().get(); assertEquals(ProcessPlugin.MODEL_ATTRIBUTE_PROCESS_API_VERSION, property.getCamundaName()); - assertEquals(plugin.getProcessPluginApiVersion(), property.getCamundaValue()); + assertEquals(ProcessPluginFactoryImpl.API_VERSION, Integer.parseInt(property.getCamundaValue())); } private ProcessPluginDefinition createPluginDefinition(String version, LocalDate releaseDate, @@ -337,9 +353,9 @@ private ProcessPluginDefinition createPluginDefinition(String version, LocalDate releaseDate); } - private ProcessPluginImpl createPlugin(ProcessPluginDefinition processPluginDefinition, boolean draft) + private AbstractProcessPlugin createPlugin(ProcessPluginDefinition processPluginDefinition, boolean draft) { - return new ProcessPluginImpl(processPluginDefinition, processPluginApi, draft, Paths.get("test.jar"), - getClass().getClassLoader(), fhirContext, environment); + return new ProcessPluginImpl(processPluginDefinition, ProcessPluginFactoryImpl.API_VERSION, draft, + Paths.get("test.jar"), getClass().getClassLoader(), environment, apiApplicationContext); } } diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/service/OrganizationProviderImplTest.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/service/OrganizationProviderImplTest.java similarity index 98% rename from dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/service/OrganizationProviderImplTest.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/service/OrganizationProviderImplTest.java index 24b5d547e..547861fc7 100644 --- a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/service/OrganizationProviderImplTest.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/service/OrganizationProviderImplTest.java @@ -32,7 +32,6 @@ @RunWith(MockitoJUnitRunner.class) public class OrganizationProviderImplTest { - private static final String ENDPOINT = "endpoint"; private static final BundleEntrySearchComponent INCLUDE_MODE = new BundleEntrySearchComponent() .setMode(SearchEntryMode.INCLUDE); @@ -40,15 +39,14 @@ public class OrganizationProviderImplTest .setMode(SearchEntryMode.MATCH); @Mock - private FhirWebserviceClientProvider clientProvider; + private FhirWebserviceClientProviderImpl clientProvider; @Mock private FhirWebserviceClient client; private OrganizationProviderImpl organizationProvider; @Captor - ArgumentCaptor>> parametersCaptor; - + private ArgumentCaptor>> parametersCaptor; @Before public void setup() @@ -252,5 +250,4 @@ public void getOrganizationsWithNumberOfOrganizationsLessThanNumberOfOrganizatio assertThat(parametersCaptor.getAllValues().get(1).get("_page").get(0), is("2")); assertThat(organizations.size(), is(countOrganizations)); } - } diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/service/QuestionnaireResponseTest.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/service/QuestionnaireResponseTest.java similarity index 100% rename from dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/service/QuestionnaireResponseTest.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/service/QuestionnaireResponseTest.java diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/variables/FhirResourceListSerializationTest.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/variables/FhirResourceListSerializationTest.java similarity index 95% rename from dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/variables/FhirResourceListSerializationTest.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/variables/FhirResourceListSerializationTest.java index a4ff4275c..c9af686f2 100644 --- a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/variables/FhirResourceListSerializationTest.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/variables/FhirResourceListSerializationTest.java @@ -13,8 +13,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import ca.uhn.fhir.context.FhirContext; -import dev.dsf.bpe.variables.FhirResourcesList; -import dev.dsf.bpe.variables.ObjectMapperFactory; public class FhirResourceListSerializationTest { diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/variables/TargetsJsonSerializationTest.java b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/variables/TargetsJsonSerializationTest.java similarity index 97% rename from dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/variables/TargetsJsonSerializationTest.java rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/variables/TargetsJsonSerializationTest.java index 73b55d054..cdf251b1f 100644 --- a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/v1/variables/TargetsJsonSerializationTest.java +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/java/dev/dsf/bpe/v1/variables/TargetsJsonSerializationTest.java @@ -15,9 +15,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import ca.uhn.fhir.context.FhirContext; -import dev.dsf.bpe.variables.ObjectMapperFactory; -import dev.dsf.bpe.variables.TargetImpl; -import dev.dsf.bpe.variables.TargetsImpl; public class TargetsJsonSerializationTest { diff --git a/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/resources/log4j2.xml b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/resources/log4j2.xml new file mode 100644 index 000000000..d30bf4805 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/test/resources/test-plugin/ActivityDefinition_test.xml b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/resources/test-plugin/ActivityDefinition_test.xml similarity index 100% rename from dsf-bpe/dsf-bpe-server/src/test/resources/test-plugin/ActivityDefinition_test.xml rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/resources/test-plugin/ActivityDefinition_test.xml diff --git a/dsf-bpe/dsf-bpe-server/src/test/resources/test-plugin/test.bpmn b/dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/resources/test-plugin/test.bpmn similarity index 100% rename from dsf-bpe/dsf-bpe-server/src/test/resources/test-plugin/test.bpmn rename to dsf-bpe/dsf-bpe-process-api-v1-impl/src/test/resources/test-plugin/test.bpmn diff --git a/dsf-bpe/dsf-bpe-process-api-v1/pom.xml b/dsf-bpe/dsf-bpe-process-api-v1/pom.xml index ce9c58b2f..ffd49153a 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1/pom.xml +++ b/dsf-bpe/dsf-bpe-process-api-v1/pom.xml @@ -11,12 +11,9 @@ - dev.dsf - dsf-fhir-auth - - - dev.dsf - dsf-fhir-webservice-client + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${hapi.fhir.version.v1} org.camunda.bpm @@ -34,6 +31,10 @@ com.sun.mail jakarta.mail + + jakarta.ws.rs-api + jakarta.ws.rs + @@ -41,71 +42,10 @@ spring-web true - - dev.dsf - dsf-fhir-validation - true - de.hs-heilbronn.mi crypto-utils + true - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - test-jar - - - - - - - - - - generate-source-and-javadoc-jars - - true - - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-test-sources - - test-jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - attach-test-javadocs - - test-jar - - - false - - - - - - - - \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/bpe/v1/activity/AbstractTaskMessageSend.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/bpe/v1/activity/AbstractTaskMessageSend.java index 72ed51b48..59f46fb15 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/bpe/v1/activity/AbstractTaskMessageSend.java +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/bpe/v1/activity/AbstractTaskMessageSend.java @@ -473,7 +473,9 @@ protected void sendTask(DelegateExecution execution, Variables variables, Target task.getInstantiatesCanonical(), target.getOrganizationIdentifierValue(), target.getEndpointIdentifierValue(), businessKey, messageName); - logger.trace("Task resource to send: {}", api.getFhirContext().newJsonParser().encodeResourceToString(task)); + logger.trace("Task resource to send: {}", + api.getFhirContext().newJsonParser().setStripVersionsFromReferences(false) + .setOverrideResourceIdWithBundleEntryFullUrl(false).encodeResourceToString(task)); IdType created = doSend(client, task); diff --git a/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/bpe/v1/activity/DefaultUserTaskListener.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/bpe/v1/activity/DefaultUserTaskListener.java index 69a1a85ea..b17fe985b 100644 --- a/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/bpe/v1/activity/DefaultUserTaskListener.java +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/bpe/v1/activity/DefaultUserTaskListener.java @@ -106,7 +106,6 @@ public final void notify(DelegateTask userTask) updateFailedIfInprogress(variables.getTasks(), errorMessage); - // TODO evaluate throwing exception as alternative to stopping the process instance execution.getProcessEngine().getRuntimeService().deleteProcessInstance(execution.getProcessInstanceId(), exception.getMessage()); } diff --git a/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentials.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentials.java new file mode 100644 index 000000000..4f2ef75aa --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/DsfOpenIdCredentials.java @@ -0,0 +1,30 @@ +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/src/main/java/dev/dsf/common/auth/conf/DsfRole.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/conf/DsfRole.java new file mode 100644 index 000000000..409e3f796 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/conf/DsfRole.java @@ -0,0 +1,6 @@ +package dev.dsf.common.auth.conf; + +public interface DsfRole +{ + String name(); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/conf/Identity.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/conf/Identity.java new file mode 100644 index 000000000..e25a4100f --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/conf/Identity.java @@ -0,0 +1,33 @@ +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/src/main/java/dev/dsf/common/auth/conf/OrganizationIdentity.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/conf/OrganizationIdentity.java new file mode 100644 index 000000000..432594953 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/conf/OrganizationIdentity.java @@ -0,0 +1,5 @@ +package dev.dsf.common.auth.conf; + +public interface OrganizationIdentity extends Identity +{ +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentity.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentity.java new file mode 100644 index 000000000..7d415f2e0 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/common/auth/conf/PractitionerIdentity.java @@ -0,0 +1,29 @@ +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/src/main/java/dev/dsf/fhir/authorization/process/All.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/All.java new file mode 100644 index 000000000..05e65770c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/All.java @@ -0,0 +1,232 @@ +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/src/main/java/dev/dsf/fhir/authorization/process/Organization.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/Organization.java new file mode 100644 index 000000000..9cfed6494 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/Organization.java @@ -0,0 +1,353 @@ +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 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/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelper.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelper.java new file mode 100644 index 000000000..1745f0494 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelper.java @@ -0,0 +1,81 @@ +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 requesters, Collection 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/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelperImpl.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelperImpl.java new file mode 100644 index 000000000..e598c0c11 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelperImpl.java @@ -0,0 +1,395 @@ +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 requesters, Collection 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, c -> true, i -> true, c -> 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, i -> true, c -> 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/src/main/java/dev/dsf/fhir/authorization/process/Recipient.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/Recipient.java new file mode 100644 index 000000000..5da781971 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/Recipient.java @@ -0,0 +1,40 @@ +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/src/main/java/dev/dsf/fhir/authorization/process/Requester.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/Requester.java new file mode 100644 index 000000000..6d8d29bf3 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/Requester.java @@ -0,0 +1,93 @@ +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/src/main/java/dev/dsf/fhir/authorization/process/Role.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/Role.java new file mode 100644 index 000000000..a5d07be4d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/Role.java @@ -0,0 +1,466 @@ +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/src/main/java/dev/dsf/fhir/authorization/process/WithAuthorization.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/WithAuthorization.java new file mode 100644 index 000000000..3cd243309 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/process/WithAuthorization.java @@ -0,0 +1,10 @@ +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/src/main/java/dev/dsf/fhir/authorization/read/ReadAccessHelper.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/read/ReadAccessHelper.java new file mode 100644 index 000000000..608bc64c5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/authorization/read/ReadAccessHelper.java @@ -0,0 +1,184 @@ +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/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java new file mode 100644 index 000000000..dcaa14d9d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/BasicFhirWebserviceClient.java @@ -0,0 +1,121 @@ +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 resourceClass, String id); + + void deleteConditionaly(Class resourceClass, Map> criteria); + + void deletePermanently(Class 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 resourceType, Map> parameters); + + Bundle searchWithStrictHandling(Class 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 resourceType) + { + return history(resourceType, null); + } + + default Bundle history(Class resourceType, int page, int count) + { + return history(resourceType, null, page, count); + } + + default Bundle history(Class resourceType, String id) + { + return history(resourceType, id, Integer.MIN_VALUE, Integer.MIN_VALUE); + } + + Bundle history(Class resourceType, String id, int page, int count); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/FhirWebserviceClient.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/FhirWebserviceClient.java new file mode 100644 index 000000000..34b0a9109 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/FhirWebserviceClient.java @@ -0,0 +1,10 @@ +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/src/main/java/dev/dsf/fhir/client/PreferReturnMinimal.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnMinimal.java new file mode 100644 index 000000000..d21e10af5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnMinimal.java @@ -0,0 +1,28 @@ +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/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalWithRetry.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalWithRetry.java new file mode 100644 index 000000000..60ac6c9e7 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnMinimalWithRetry.java @@ -0,0 +1,5 @@ +package dev.dsf.fhir.client; + +public interface PreferReturnMinimalWithRetry extends PreferReturnMinimal, RetryClient +{ +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnOutcome.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnOutcome.java new file mode 100644 index 000000000..c45989c62 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnOutcome.java @@ -0,0 +1,30 @@ +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/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeWithRetry.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeWithRetry.java new file mode 100644 index 000000000..a8bf016d5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnOutcomeWithRetry.java @@ -0,0 +1,5 @@ +package dev.dsf.fhir.client; + +public interface PreferReturnOutcomeWithRetry extends PreferReturnOutcome, RetryClient +{ +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnResource.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnResource.java new file mode 100644 index 000000000..7c1d43506 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/PreferReturnResource.java @@ -0,0 +1,30 @@ +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 +{ + R create(R resource); + + R createConditionaly(R resource, String ifNoneExistCriteria); + + Binary createBinary(InputStream in, MediaType mediaType, String securityContextReference); + + + R update(R resource); + + R updateConditionaly(R resource, Map> criteria); + + Binary 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/src/main/java/dev/dsf/fhir/client/RetryClient.java b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/RetryClient.java new file mode 100644 index 000000000..dddb37d96 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v1/src/main/java/dev/dsf/fhir/client/RetryClient.java @@ -0,0 +1,68 @@ +package dev.dsf.fhir.client; + +public interface RetryClient +{ + int RETRY_ONCE = 1; + int RETRY_FOREVER = -1; + long FIVE_SECONDS = 5_000L; + + /** + * retries once after a delay of {@value RetryClient#FIVE_SECONDS} ms + * + * @return T + */ + default T withRetry() + { + return withRetry(RETRY_ONCE, FIVE_SECONDS); + } + + /** + * retries nTimes and waits {@value RetryClient#FIVE_SECONDS} ms between tries + * + * @param nTimes + * {@code >= 0} + * @return T + * + * @throws IllegalArgumentException + * if param nTimes is {@code <0} + */ + default T withRetry(int nTimes) + { + return withRetry(nTimes, FIVE_SECONDS); + } + + /** + * retries once after a delay of delayMillis ms + * + * @param delayMillis + * {@code >= 0} + * @return T + * @throws IllegalArgumentException + * if param delayMillis is {@code <0} + */ + default T withRetry(long delayMillis) + { + return withRetry(RETRY_ONCE, delayMillis); + } + + /** + * @param nTimes + * {@code >= 0} + * @param delayMillis + * {@code >= 0} + * @return T + * + * @throws IllegalArgumentException + * if param nTimes or delayMillis is {@code <0} + */ + T withRetry(int nTimes, long delayMillis); + + /** + * @param delayMillis + * {@code >= 0} + * @return T + * @throws IllegalArgumentException + * if param delayMillis is {@code <0} + */ + T withRetryForever(long delayMillis); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v1/src/test/java/dev/dsf/bpe/start/ConstantsExampleStarters.java b/dsf-bpe/dsf-bpe-process-api-v1/src/test/java/dev/dsf/bpe/start/ConstantsExampleStarters.java deleted file mode 100644 index 980a1ab81..000000000 --- a/dsf-bpe/dsf-bpe-process-api-v1/src/test/java/dev/dsf/bpe/start/ConstantsExampleStarters.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.dsf.bpe.start; - -public interface ConstantsExampleStarters -{ - String ENV_DSF_CLIENT_CERTIFICATE_PATH = "DSF_CLIENT_CERTIFICATE_PATH"; - String ENV_DSF_CLIENT_CERTIFICATE_PASSWORD = "DSF_CLIENT_CERTIFICATE_PASSWORD"; - - String NAMINGSYSTEM_DSF_ORGANIZATION_IDENTIFIER_VALUE_CONSORTIUM_HIGHMED = "highmed.org"; - String NAMINGSYSTEM_DSF_ORGANIZATION_IDENTIFIER_VALUE_CONSORTIUM_MII = "medizininformatik-initiative.de"; - String NAMINGSYSTEM_DSF_ORGANIZATION_IDENTIFIER_VALUE_CONSORTIUM_NUM = "netzwerk-universitaetsmedizin.de"; - - String TTP_FHIR_BASE_URL = "https://ttp/fhir/"; - String NAMINGSYSTEM_DSF_ORGANIZATION_IDENTIFIER_VALUE_TTP = "Test_TTP"; - - String DIC_1_FHIR_BASE_URL = "https://dic1/fhir/"; - String NAMINGSYSTEM_DSF_ORGANIZATION_IDENTIFIER_VALUE_DIC_1 = "Test_DIC_1"; - - String DIC_2_FHIR_BASE_URL = "https://dic2/fhir/"; - String NAMINGSYSTEM_DSF_ORGANIZATION_IDENTIFIER_VALUE_DIC_2 = "Test_DIC_2"; - - String DIC_3_FHIR_BASE_URL = "https://dic3/fhir/"; - String NAMINGSYSTEM_DSF_ORGANIZATION_IDENTIFIER_VALUE_DIC_3 = "Test_DIC_3"; -} diff --git a/dsf-bpe/dsf-bpe-process-api-v1/src/test/java/dev/dsf/bpe/start/ExampleStarter.java b/dsf-bpe/dsf-bpe-process-api-v1/src/test/java/dev/dsf/bpe/start/ExampleStarter.java deleted file mode 100644 index e92baed6f..000000000 --- a/dsf-bpe/dsf-bpe-process-api-v1/src/test/java/dev/dsf/bpe/start/ExampleStarter.java +++ /dev/null @@ -1,134 +0,0 @@ -package dev.dsf.bpe.start; - -import static dev.dsf.bpe.start.ConstantsExampleStarters.ENV_DSF_CLIENT_CERTIFICATE_PASSWORD; -import static dev.dsf.bpe.start.ConstantsExampleStarters.ENV_DSF_CLIENT_CERTIFICATE_PATH; - -import java.nio.file.Paths; -import java.security.KeyStore; - -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Resource; -import org.hl7.fhir.r4.model.ResourceType; -import org.hl7.fhir.r4.model.Task; - -import ca.uhn.fhir.context.FhirContext; -import de.rwh.utils.crypto.CertificateHelper; -import de.rwh.utils.crypto.io.CertificateReader; -import dev.dsf.fhir.client.FhirWebserviceClient; -import dev.dsf.fhir.client.FhirWebserviceClientJersey; -import dev.dsf.fhir.service.ReferenceCleaner; -import dev.dsf.fhir.service.ReferenceCleanerImpl; -import dev.dsf.fhir.service.ReferenceExtractorImpl; - -public class ExampleStarter -{ - /** - * Creates an object to send start-process-messages to a given FHIR-Endpoint baseUrl based on the provided - * client-certificate path and client-certificate password. - * - * The client-certificate path is first read from the environment variable - * {@link ConstantsExampleStarters#ENV_DSF_CLIENT_CERTIFICATE_PATH}. If args[0] is set, the environment variable is - * overwritten by args[0]. - * - * The client-certificate password is first read from the environment variable - * {@link ConstantsExampleStarters#ENV_DSF_CLIENT_CERTIFICATE_PASSWORD}. If args[1] is set, the environment variable - * is overwritten by args[1]. - * - * @param args - * client-certificate arguments: args[0] can be the path of the client-certificate args[1] can be the - * password of the client-certificate - * @param baseUrl - * the baseUrl of the organization's FHIR-Endpoint - * @return initialized ExampleStarter instance - */ - public static ExampleStarter forServer(String[] args, String baseUrl) - { - String certificatePath = System.getenv(ENV_DSF_CLIENT_CERTIFICATE_PATH); - String certificatePassword = System.getenv(ENV_DSF_CLIENT_CERTIFICATE_PASSWORD); - - if (args.length > 0 && !args[0].isBlank()) - certificatePath = args[0]; - - if (args.length > 1 && !args[1].isBlank()) - certificatePassword = args[1]; - - return ExampleStarter.forServer(certificatePath, certificatePassword, baseUrl); - } - - /** - * Creates an object to send start-process-messages to a given FHIR-Endpoint baseUrl based on the provided - * client-certificate path and client-certificate password. - * - * @param certificatePath - * the path of the client-certificate - * @param certificatePassword - * the password of the client-certificate - * @param baseUrl - * the baseUrl of the organization's FHIR-Endpoint - * @return initialized ExampleStarter instance - */ - public static ExampleStarter forServer(String certificatePath, String certificatePassword, String baseUrl) - { - if (certificatePath == null || certificatePath.isBlank()) - throw new IllegalArgumentException("certificatePath null or blank"); - - if (certificatePassword == null || certificatePassword.isBlank()) - throw new IllegalArgumentException("certificatePassword null or blank"); - - if (baseUrl == null || baseUrl.isBlank()) - throw new IllegalArgumentException("baseUrl null or blank"); - - return new ExampleStarter(certificatePath, certificatePassword, baseUrl); - } - - private final String certificatePath; - private final char[] certificatePassword; - private final String baseUrl; - - private ExampleStarter(String certificatePath, String certificatePassword, String baseUrl) - { - this.certificatePath = certificatePath; - this.certificatePassword = certificatePassword.toCharArray(); - this.baseUrl = baseUrl; - } - - public void startWith(Task task) throws Exception - { - start(task); - } - - public void startWith(Bundle bundle) throws Exception - { - start(bundle); - } - - private void start(Resource resource) throws Exception - { - FhirWebserviceClient client = createClient(baseUrl); - - if (resource instanceof Bundle bundle) - { - bundle.getEntry().stream().map(e -> e.getResource().getResourceType()).filter(ResourceType.Task::equals) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Bundle does not contain a Task resource")); - - client.withMinimalReturn().postBundle(bundle); - } - else if (resource instanceof Task) - client.withMinimalReturn().create(resource); - else - throw new IllegalArgumentException("Resource should be of type Bundle or Task"); - } - - public FhirWebserviceClient createClient(String baseUrl) throws Exception - { - KeyStore keyStore = CertificateReader.fromPkcs12(Paths.get(certificatePath), certificatePassword); - KeyStore trustStore = CertificateHelper.extractTrust(keyStore); - - FhirContext context = FhirContext.forR4(); - ReferenceCleaner referenceCleaner = new ReferenceCleanerImpl(new ReferenceExtractorImpl()); - - return new FhirWebserviceClientJersey(baseUrl, trustStore, keyStore, certificatePassword, null, null, null, - null, 0, 0, false, "DSF Example Starter", context, referenceCleaner); - } -} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml b/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml new file mode 100644 index 000000000..9018032a9 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/pom.xml @@ -0,0 +1,66 @@ + + 4.0.0 + + dsf-bpe-process-api-v2-impl + + + dev.dsf + dsf-bpe-pom + 2.0.0-SNAPSHOT + + + + + dev.dsf + dsf-bpe-process-api + + + dev.dsf + dsf-bpe-process-api-v2 + + + + org.glassfish.jersey.core + jersey-client + + + org.glassfish.jersey.inject + jersey-hk2 + + + org.glassfish.jersey.media + jersey-media-jaxb + + + org.glassfish.jersey.media + jersey-media-json-jackson + + + org.glassfish.jersey.connectors + jersey-apache-connector + + + commons-logging + commons-logging + + + + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${hapi.fhir.version.v2} + + + + org.apache.logging.log4j + log4j-slf4j2-impl + test + + + org.mockito + mockito-core + test + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/ProcessPluginApiImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/ProcessPluginApiImpl.java new file mode 100644 index 000000000..56758471c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/ProcessPluginApiImpl.java @@ -0,0 +1,145 @@ +package dev.dsf.bpe.v2; + +import java.util.Objects; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.springframework.beans.factory.InitializingBean; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import ca.uhn.fhir.context.FhirContext; +import dev.dsf.bpe.v2.config.ProxyConfig; +import dev.dsf.bpe.v2.service.EndpointProvider; +import dev.dsf.bpe.v2.service.FhirWebserviceClientProvider; +import dev.dsf.bpe.v2.service.MailService; +import dev.dsf.bpe.v2.service.OrganizationProvider; +import dev.dsf.bpe.v2.service.QuestionnaireResponseHelper; +import dev.dsf.bpe.v2.service.ReadAccessHelper; +import dev.dsf.bpe.v2.service.TaskHelper; +import dev.dsf.bpe.v2.service.process.ProcessAuthorizationHelper; +import dev.dsf.bpe.v2.variables.Variables; +import dev.dsf.bpe.v2.variables.VariablesImpl; + +public class ProcessPluginApiImpl implements ProcessPluginApi, InitializingBean +{ + private final ProxyConfig proxyConfig; + private final EndpointProvider endpointProvider; + private final FhirContext fhirContext; + private final FhirWebserviceClientProvider fhirWebserviceClientProvider; + private final MailService mailService; + private final ObjectMapper objectMapper; + private final OrganizationProvider organizationProvider; + private final ProcessAuthorizationHelper processAuthorizationHelper; + private final QuestionnaireResponseHelper questionnaireResponseHelper; + private final ReadAccessHelper readAccessHelper; + private final TaskHelper taskHelper; + + public ProcessPluginApiImpl(ProxyConfig proxyConfig, EndpointProvider endpointProvider, FhirContext fhirContext, + FhirWebserviceClientProvider fhirWebserviceClientProvider, MailService mailService, + ObjectMapper objectMapper, OrganizationProvider organizationProvider, + ProcessAuthorizationHelper processAuthorizationHelper, + QuestionnaireResponseHelper questionnaireResponseHelper, ReadAccessHelper readAccessHelper, + TaskHelper taskHelper) + { + this.proxyConfig = proxyConfig; + this.endpointProvider = endpointProvider; + this.fhirContext = fhirContext; + this.fhirWebserviceClientProvider = fhirWebserviceClientProvider; + this.mailService = mailService; + this.objectMapper = objectMapper; + this.organizationProvider = organizationProvider; + this.processAuthorizationHelper = processAuthorizationHelper; + this.questionnaireResponseHelper = questionnaireResponseHelper; + this.readAccessHelper = readAccessHelper; + this.taskHelper = taskHelper; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(proxyConfig, "proxyConfig"); + Objects.requireNonNull(endpointProvider, "endpointProvider"); + Objects.requireNonNull(fhirContext, "fhirContext"); + Objects.requireNonNull(fhirWebserviceClientProvider, "fhirWebserviceClientProvider"); + Objects.requireNonNull(mailService, "mailService"); + Objects.requireNonNull(objectMapper, "objectMapper"); + Objects.requireNonNull(organizationProvider, "organizationProvider"); + Objects.requireNonNull(processAuthorizationHelper, "processAuthorizationHelper"); + Objects.requireNonNull(questionnaireResponseHelper, "questionnaireResponseHelper"); + Objects.requireNonNull(readAccessHelper, "readAccessHelper"); + Objects.requireNonNull(taskHelper, "taskHelper"); + } + + @Override + public ProxyConfig getProxyConfig() + { + return proxyConfig; + } + + @Override + public EndpointProvider getEndpointProvider() + { + return endpointProvider; + } + + @Override + public FhirContext getFhirContext() + { + return fhirContext; + } + + @Override + public FhirWebserviceClientProvider getFhirWebserviceClientProvider() + { + return fhirWebserviceClientProvider; + } + + @Override + public MailService getMailService() + { + return mailService; + } + + @Override + public ObjectMapper getObjectMapper() + { + return objectMapper; + } + + @Override + public OrganizationProvider getOrganizationProvider() + { + return organizationProvider; + } + + @Override + public ProcessAuthorizationHelper getProcessAuthorizationHelper() + { + return processAuthorizationHelper; + } + + @Override + public QuestionnaireResponseHelper getQuestionnaireResponseHelper() + { + return questionnaireResponseHelper; + } + + @Override + public ReadAccessHelper getReadAccessHelper() + { + return readAccessHelper; + } + + @Override + public TaskHelper getTaskHelper() + { + return taskHelper; + } + + @Override + public Variables getVariables(DelegateExecution execution) + { + // returning a new VariablesImpl since DelegateExecution is BPMN activity specific + return new VariablesImpl(execution); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/AbstractFhirWebserviceClientJerseyWithRetry.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/AbstractFhirWebserviceClientJerseyWithRetry.java new file mode 100644 index 000000000..a6fcdeae4 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/AbstractFhirWebserviceClientJerseyWithRetry.java @@ -0,0 +1,113 @@ +package dev.dsf.bpe.v2.client; + +import java.net.UnknownHostException; +import java.util.function.Supplier; + +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.HttpHostConnectException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response.Status; + +public abstract class AbstractFhirWebserviceClientJerseyWithRetry +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractFhirWebserviceClientJerseyWithRetry.class); + + protected final FhirWebserviceClientJersey delegate; + protected final int nTimes; + protected final long delayMillis; + + protected AbstractFhirWebserviceClientJerseyWithRetry(FhirWebserviceClientJersey delegate, int nTimes, + long delayMillis) + { + this.delegate = delegate; + this.nTimes = nTimes; + this.delayMillis = delayMillis; + } + + protected final R retry(int nTimes, long delayMillis, Supplier supplier) + { + RuntimeException caughtException = null; + for (int tryNumber = 0; tryNumber <= nTimes || nTimes == RetryClient.RETRY_FOREVER; tryNumber++) + { + try + { + if (tryNumber == 0) + logger.debug("First try ..."); + else if (nTimes != RetryClient.RETRY_FOREVER) + logger.debug("Retry {} of {}", tryNumber, nTimes); + + return supplier.get(); + } + catch (ProcessingException | WebApplicationException e) + { + if (shouldRetry(e)) + { + if (tryNumber < nTimes || nTimes == RetryClient.RETRY_FOREVER) + { + logger.warn("Caught {} - {}; trying again in {} ms{}", e.getClass(), e.getMessage(), + delayMillis, + nTimes == RetryClient.RETRY_FOREVER ? " (retry " + (tryNumber + 1) + ")" : ""); + + try + { + Thread.sleep(delayMillis); + } + catch (InterruptedException e1) + { + } + } + else + { + logger.warn("Caught {} - {}; not trying again", e.getClass(), e.getMessage()); + } + + if (caughtException != null) + e.addSuppressed(caughtException); + caughtException = e; + } + else + throw e; + } + } + + throw caughtException; + } + + private boolean shouldRetry(RuntimeException e) + { + if (e instanceof WebApplicationException w) + { + return isRetryStatusCode(w); + } + else if (e instanceof ProcessingException) + { + Throwable cause = e; + if (isRetryCause(cause)) + return true; + + while (cause.getCause() != null) + { + cause = cause.getCause(); + if (isRetryCause(cause)) + return true; + } + } + + return false; + } + + private boolean isRetryStatusCode(WebApplicationException e) + { + return Status.Family.SERVER_ERROR.equals(e.getResponse().getStatusInfo().getFamily()); + } + + private boolean isRetryCause(Throwable cause) + { + return cause instanceof ConnectTimeoutException || cause instanceof HttpHostConnectException + || cause instanceof UnknownHostException; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/AbstractJerseyClient.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/AbstractJerseyClient.java new file mode 100644 index 000000000..f8aec0d46 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/AbstractJerseyClient.java @@ -0,0 +1,108 @@ +package dev.dsf.bpe.v2.client; + +import java.security.KeyStore; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.logging.LoggingFeature.Verbosity; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.HttpHeaders; + +public class AbstractJerseyClient +{ + private static final java.util.logging.Logger requestDebugLogger; + static + { + requestDebugLogger = java.util.logging.Logger.getLogger(AbstractJerseyClient.class.getName()); + requestDebugLogger.setLevel(Level.INFO); + } + + private final Client client; + private final String baseUrl; + + public AbstractJerseyClient(String baseUrl, KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword, + ObjectMapper objectMapper, Collection componentsToRegister) + { + this(baseUrl, trustStore, keyStore, keyStorePassword, objectMapper, componentsToRegister, null, null, null, 0, + 0, false, null); + } + + public AbstractJerseyClient(String baseUrl, KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword, + ObjectMapper objectMapper, Collection componentsToRegister, String proxySchemeHostPort, + String proxyUserName, char[] proxyPassword, int connectTimeout, int readTimeout, boolean logRequests, + String userAgentValue) + { + SSLContext sslContext = null; + if (trustStore != null && keyStore == null && keyStorePassword == null) + sslContext = SslConfigurator.newInstance().trustStore(trustStore).createSSLContext(); + else if (trustStore != null && keyStore != null && keyStorePassword != null) + sslContext = SslConfigurator.newInstance().trustStore(trustStore).keyStore(keyStore) + .keyStorePassword(keyStorePassword).createSSLContext(); + + ClientBuilder builder = ClientBuilder.newBuilder(); + + if (sslContext != null) + builder = builder.sslContext(sslContext); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(new ApacheConnectorProvider()); + config.property(ClientProperties.PROXY_URI, proxySchemeHostPort); + config.property(ClientProperties.PROXY_USERNAME, proxyUserName); + config.property(ClientProperties.PROXY_PASSWORD, proxyPassword == null ? null : String.valueOf(proxyPassword)); + builder = builder.withConfig(config); + + if (userAgentValue != null && !userAgentValue.isBlank()) + builder = builder.register((ClientRequestFilter) requestContext -> requestContext.getHeaders() + .add(HttpHeaders.USER_AGENT, userAgentValue)); + + builder = builder.readTimeout(readTimeout, TimeUnit.MILLISECONDS).connectTimeout(connectTimeout, + TimeUnit.MILLISECONDS); + + if (objectMapper != null) + { + JacksonJaxbJsonProvider p = new JacksonJaxbJsonProvider(JacksonJsonProvider.BASIC_ANNOTATIONS); + p.setMapper(objectMapper); + builder.register(p); + } + + if (componentsToRegister != null) + componentsToRegister.forEach(builder::register); + + if (logRequests) + { + builder = builder.register(new LoggingFeature(requestDebugLogger, Level.INFO, Verbosity.PAYLOAD_ANY, + LoggingFeature.DEFAULT_MAX_ENTITY_SIZE)); + } + + client = builder.build(); + + this.baseUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"; + // making sure the root url works, this might be a workaround for a jersey client bug + } + + protected WebTarget getResource() + { + return client.target(baseUrl); + } + + public String getBaseUrl() + { + return baseUrl; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/BasicFhirWebserviceCientWithRetryImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/BasicFhirWebserviceCientWithRetryImpl.java new file mode 100644 index 000000000..3e947dcf8 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/BasicFhirWebserviceCientWithRetryImpl.java @@ -0,0 +1,200 @@ +package dev.dsf.bpe.v2.client; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.hl7.fhir.r4.model.Binary; +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; + +class BasicFhirWebserviceCientWithRetryImpl extends AbstractFhirWebserviceClientJerseyWithRetry + implements BasicFhirWebserviceClient +{ + BasicFhirWebserviceCientWithRetryImpl(FhirWebserviceClientJersey delegate, int nTimes, long delayMillis) + { + super(delegate, nTimes, delayMillis); + } + + @Override + public R updateConditionaly(R resource, Map> criteria) + { + return retry(nTimes, delayMillis, () -> delegate.updateConditionaly(resource, criteria)); + } + + @Override + public Binary updateBinary(String id, InputStream in, MediaType mediaType, String securityContextReference) + { + return retry(nTimes, delayMillis, () -> delegate.updateBinary(id, in, mediaType, securityContextReference)); + } + + @Override + public R update(R resource) + { + return retry(nTimes, delayMillis, () -> delegate.update(resource)); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return retry(nTimes, delayMillis, () -> delegate.postBundle(bundle)); + } + + @Override + public R createConditionaly(R resource, String ifNoneExistCriteria) + { + return retry(nTimes, delayMillis, () -> delegate.createConditionaly(resource, ifNoneExistCriteria)); + } + + @Override + public Binary createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return retry(nTimes, delayMillis, () -> delegate.createBinary(in, mediaType, securityContextReference)); + } + + @Override + public R create(R resource) + { + return retry(nTimes, delayMillis, () -> delegate.create(resource)); + } + + @Override + public Bundle searchWithStrictHandling(Class resourceType, Map> parameters) + { + return retry(nTimes, delayMillis, () -> delegate.searchWithStrictHandling(resourceType, parameters)); + } + + @Override + public Bundle search(Class resourceType, Map> parameters) + { + return retry(nTimes, delayMillis, () -> delegate.search(resourceType, parameters)); + } + + @Override + public InputStream readBinary(String id, String version, MediaType mediaType) + { + return retry(nTimes, delayMillis, () -> + { + InputStream in = delegate.readBinary(id, version, mediaType); + return in; + }); + } + + @Override + public InputStream readBinary(String id, MediaType mediaType) + { + return retry(nTimes, delayMillis, () -> + { + InputStream in = delegate.readBinary(id, mediaType); + return in; + }); + } + + @Override + public R read(Class resourceType, String id, String version) + { + return retry(nTimes, delayMillis, () -> delegate.read(resourceType, id, version)); + } + + @Override + public Resource read(String resourceTypeName, String id, String version) + { + return retry(nTimes, delayMillis, () -> delegate.read(resourceTypeName, id, version)); + } + + @Override + public R read(Class resourceType, String id) + { + return retry(nTimes, delayMillis, () -> delegate.read(resourceType, id)); + } + + @Override + public R read(R oldValue) + { + return retry(nTimes, delayMillis, () -> delegate.read(oldValue)); + } + + @Override + public Resource read(String resourceTypeName, String id) + { + return retry(nTimes, delayMillis, () -> delegate.read(resourceTypeName, id)); + } + + @Override + public CapabilityStatement getConformance() + { + return retry(nTimes, delayMillis, () -> delegate.getConformance()); + } + + @Override + public StructureDefinition generateSnapshot(StructureDefinition differential) + { + return retry(nTimes, delayMillis, () -> delegate.generateSnapshot(differential)); + } + + @Override + public StructureDefinition generateSnapshot(String url) + { + return retry(nTimes, delayMillis, () -> delegate.generateSnapshot(url)); + } + + @Override + public boolean exists(IdType resourceTypeIdVersion) + { + return retry(nTimes, delayMillis, () -> delegate.exists(resourceTypeIdVersion)); + } + + @Override + public boolean exists(Class resourceType, String id, String version) + { + return retry(nTimes, delayMillis, () -> delegate.exists(resourceType, id, version)); + } + + @Override + public boolean exists(Class resourceType, String id) + { + return retry(nTimes, delayMillis, () -> delegate.exists(resourceType, id)); + } + + @Override + public void deletePermanently(Class resourceClass, String id) + { + retry(nTimes, delayMillis, (Supplier) () -> + { + delegate.deletePermanently(resourceClass, id); + return null; + }); + } + + @Override + public void deleteConditionaly(Class resourceClass, Map> criteria) + { + retry(nTimes, delayMillis, (Supplier) () -> + { + delegate.deleteConditionaly(resourceClass, criteria); + return null; + }); + } + + @Override + public void delete(Class resourceClass, String id) + { + retry(nTimes, delayMillis, (Supplier) () -> + { + delegate.delete(resourceClass, id); + return null; + }); + } + + @Override + public Bundle history(Class resourceType, String id, int page, int count) + { + return retry(nTimes, delayMillis, () -> delegate.history(resourceType, id, page, count)); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/FhirAdapter.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/FhirAdapter.java new file mode 100644 index 000000000..0a7bbb21e --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/FhirAdapter.java @@ -0,0 +1,112 @@ +package dev.dsf.bpe.v2.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Set; +import java.util.function.Supplier; + +import org.hl7.fhir.r4.model.BaseResource; +import org.hl7.fhir.r4.model.Bundle; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.api.Constants; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; + +@Provider +@Consumes({ Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML, + Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON }) +@Produces({ Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML, + Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON }) +public class FhirAdapter implements MessageBodyReader, MessageBodyWriter +{ + private final FhirContext fhirContext; + private final ReferenceCleaner referenceCleaner; + + public FhirAdapter(FhirContext fhirContext, ReferenceCleaner referenceCleaner) + { + this.fhirContext = fhirContext; + this.referenceCleaner = referenceCleaner; + } + + private IParser getParser(MediaType mediaType, Supplier parserFactor) + { + /* Parsers are not guaranteed to be thread safe */ + IParser p = parserFactor.get(); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + + if (mediaType != null) + { + if ("true".equals(mediaType.getParameters().getOrDefault("pretty", "false"))) + p.setPrettyPrint(true); + + switch (mediaType.getParameters().getOrDefault("summary", "false")) + { + case "true" -> p.setSummaryMode(true); + case "text" -> p.setEncodeElements(Set.of("*.text", "*.id", "*.meta", "*.(mandatory)")); + case "data" -> p.setSuppressNarratives(true); + } + } + + return p; + } + + private IParser getParser(MediaType mediaType) + { + return switch (mediaType.getType() + "/" + mediaType.getSubtype()) + { + case Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML -> + getParser(mediaType, fhirContext::newXmlParser); + case Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON -> + getParser(mediaType, fhirContext::newJsonParser); + default -> throw new IllegalStateException("MediaType " + mediaType.toString() + " not supported"); + }; + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + return type != null && BaseResource.class.isAssignableFrom(type); + } + + @Override + public void writeTo(BaseResource t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException + { + getParser(mediaType).encodeResourceToWriter(t, new OutputStreamWriter(entityStream)); + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + return type != null && BaseResource.class.isAssignableFrom(type); + } + + @Override + public BaseResource readFrom(Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException + { + BaseResource resource = getParser(mediaType).parseResource(type, new InputStreamReader(entityStream)); + + // HAPI FHIR parser adds contained resources to bundle references + if (resource instanceof Bundle b) + resource = referenceCleaner.cleanReferenceResourcesIfBundle(b); + + return resource; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/FhirWebserviceClientJersey.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/FhirWebserviceClientJersey.java new file mode 100644 index 000000000..99f28560d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/FhirWebserviceClientJersey.java @@ -0,0 +1,769 @@ +package dev.dsf.bpe.v2.client; + +import java.io.InputStream; +import java.security.KeyStore; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.TimeZone; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.model.Binary; +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.OperationOutcome; +import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.UriType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.annotation.ResourceDef; +import ca.uhn.fhir.rest.api.Constants; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation.Builder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.EntityTag; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.ext.RuntimeDelegate; + +public class FhirWebserviceClientJersey extends AbstractJerseyClient implements FhirWebserviceClient +{ + private static final Logger logger = LoggerFactory.getLogger(FhirWebserviceClientJersey.class); + + private static final String RFC_7231_FORMAT = "EEE, dd MMM yyyy HH:mm:ss z"; + private static final Map> RESOURCE_TYPES_BY_NAME = Stream.of(ResourceType.values()) + .filter(type -> !ResourceType.List.equals(type)) + .collect(Collectors.toMap(ResourceType::name, FhirWebserviceClientJersey::getFhirClass)); + + private static Class getFhirClass(ResourceType type) + { + try + { + return Class.forName("org.hl7.fhir.r4.model." + type.name()); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException(e); + } + } + + private final PreferReturnMinimalWithRetry preferReturnMinimal; + private final PreferReturnOutcomeWithRetry preferReturnOutcome; + + public FhirWebserviceClientJersey(String baseUrl, KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword, + ObjectMapper objectMapper, String proxySchemeHostPort, String proxyUserName, char[] proxyPassword, + int connectTimeout, int readTimeout, boolean logRequests, String userAgentValue, FhirContext fhirContext, + ReferenceCleaner referenceCleaner) + { + super(baseUrl, trustStore, keyStore, keyStorePassword, objectMapper, + Collections.singleton(new FhirAdapter(fhirContext, referenceCleaner)), proxySchemeHostPort, + proxyUserName, proxyPassword, connectTimeout, readTimeout, logRequests, userAgentValue); + + preferReturnMinimal = new PreferReturnMinimalWithRetryImpl(this); + preferReturnOutcome = new PreferReturnOutcomeWithRetryImpl(this); + } + + private WebApplicationException handleError(Response response) + { + try + { + OperationOutcome outcome = response.readEntity(OperationOutcome.class); + String message = toString(outcome); + + logger.warn("Request failed, OperationOutcome: {}", message); + return new WebApplicationException(message, response.getStatus()); + } + catch (ProcessingException e) + { + response.close(); + + logger.warn("Request failed: {} - {}", e.getClass().getName(), e.getMessage()); + return new WebApplicationException(e, response.getStatus()); + } + } + + private String toString(OperationOutcome outcome) + { + return outcome == null ? "" : outcome.getIssue().stream().map(this::toString).collect(Collectors.joining("\n")); + } + + private String toString(OperationOutcomeIssueComponent issue) + { + return issue == null ? "" : issue.getSeverity() + " " + issue.getCode() + " " + issue.getDiagnostics(); + } + + private void logStatusAndHeaders(Response response) + { + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + logger.debug("HTTP header Location: {}", response.getLocation()); + logger.debug("HTTP header ETag: {}", response.getHeaderString(HttpHeaders.ETAG)); + logger.debug("HTTP header Last-Modified: {}", response.getHeaderString(HttpHeaders.LAST_MODIFIED)); + } + + private PreferReturn toPreferReturn(PreferReturnType returnType, Class resourceType, + Response response) + { + return switch (returnType) + { + case REPRESENTATION -> PreferReturn.resource(response.readEntity(resourceType)); + case MINIMAL -> PreferReturn.minimal(response.getLocation()); + case OPERATION_OUTCOME -> PreferReturn.outcome(response.readEntity(OperationOutcome.class)); + default -> + throw new RuntimeException(PreferReturn.class.getName() + " value " + returnType + " not supported"); + }; + } + + @Override + public PreferReturnMinimalWithRetry withMinimalReturn() + { + return preferReturnMinimal; + } + + @Override + public PreferReturnOutcomeWithRetry withOperationOutcomeReturn() + { + return preferReturnOutcome; + } + + PreferReturn create(PreferReturnType returnType, Resource resource) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + + Response response = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()).request() + .header(Constants.HEADER_PREFER, returnType.getHeaderValue()).accept(Constants.CT_FHIR_JSON_NEW) + .post(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn createConditionaly(PreferReturnType returnType, Resource resource, String ifNoneExistCriteria) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(ifNoneExistCriteria, "ifNoneExistCriteria"); + + Response response = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()).request() + .header(Constants.HEADER_PREFER, returnType.getHeaderValue()) + .header(Constants.HEADER_IF_NONE_EXIST, ifNoneExistCriteria).accept(Constants.CT_FHIR_JSON_NEW) + .post(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn createBinary(PreferReturnType returnType, InputStream in, MediaType mediaType, + String securityContextReference) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(in, "in"); + Objects.requireNonNull(mediaType, "mediaType"); + // securityContextReference may be null + + Builder request = getResource().path("Binary").request().header(Constants.HEADER_PREFER, + returnType.getHeaderValue()); + if (securityContextReference != null && !securityContextReference.isBlank()) + request = request.header(Constants.HEADER_X_SECURITY_CONTEXT, securityContextReference); + Response response = request.accept(Constants.CT_FHIR_JSON_NEW).post(Entity.entity(in, mediaType)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, Binary.class, response); + else + throw handleError(response); + } + + PreferReturn update(PreferReturnType returnType, Resource resource) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + + Builder builder = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()) + .path(resource.getIdElement().getIdPart()).request() + .header(Constants.HEADER_PREFER, returnType.getHeaderValue()).accept(Constants.CT_FHIR_JSON_NEW); + + if (resource.getMeta().hasVersionId()) + builder.header(Constants.HEADER_IF_MATCH, new EntityTag(resource.getMeta().getVersionId(), true)); + + Response response = builder.put(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn updateConditionaly(PreferReturnType returnType, Resource resource, Map> criteria) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(criteria, "criteria"); + if (criteria.isEmpty()) + throw new IllegalArgumentException("criteria map empty"); + + WebTarget target = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()); + + for (Entry> entry : criteria.entrySet()) + target = target.queryParam(entry.getKey(), entry.getValue().toArray()); + + Builder builder = target.request().accept(Constants.CT_FHIR_JSON_NEW).header(Constants.HEADER_PREFER, + returnType.getHeaderValue()); + + if (resource.getMeta().hasVersionId()) + builder.header(Constants.HEADER_IF_MATCH, new EntityTag(resource.getMeta().getVersionId(), true)); + + Response response = builder.put(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus() || Status.OK.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn updateBinary(PreferReturnType returnType, String id, InputStream in, MediaType mediaType, + String securityContextReference) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(in, "in"); + Objects.requireNonNull(mediaType, "mediaType"); + // securityContextReference may be null + + Builder request = getResource().path("Binary").path(id).request().header(Constants.HEADER_PREFER, + returnType.getHeaderValue()); + if (securityContextReference != null && !securityContextReference.isBlank()) + request = request.header(Constants.HEADER_X_SECURITY_CONTEXT, securityContextReference); + Response response = request.accept(Constants.CT_FHIR_JSON_NEW).put(Entity.entity(in, mediaType)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, Binary.class, response); + else + throw handleError(response); + } + + Bundle postBundle(PreferReturnType returnType, Bundle bundle) + { + Objects.requireNonNull(bundle, "bundle"); + + Response response = getResource().request().header(Constants.HEADER_PREFER, returnType.getHeaderValue()) + .accept(Constants.CT_FHIR_JSON_NEW).post(Entity.entity(bundle, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(Bundle.class); + else + throw handleError(response); + } + + @Override + @SuppressWarnings("unchecked") + public R create(R resource) + { + return (R) create(PreferReturnType.REPRESENTATION, resource).getResource(); + } + + @Override + @SuppressWarnings("unchecked") + public R createConditionaly(R resource, String ifNoneExistCriteria) + { + return (R) createConditionaly(PreferReturnType.REPRESENTATION, resource, ifNoneExistCriteria).getResource(); + } + + @Override + public Binary createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return (Binary) createBinary(PreferReturnType.REPRESENTATION, in, mediaType, securityContextReference) + .getResource(); + } + + @Override + @SuppressWarnings("unchecked") + public R update(R resource) + { + return (R) update(PreferReturnType.REPRESENTATION, resource).getResource(); + } + + @Override + @SuppressWarnings("unchecked") + public R updateConditionaly(R resource, Map> criteria) + { + return (R) updateConditionaly(PreferReturnType.REPRESENTATION, resource, criteria).getResource(); + } + + @Override + public Binary updateBinary(String id, InputStream in, MediaType mediaType, String securityContextReference) + { + return (Binary) updateBinary(PreferReturnType.REPRESENTATION, id, in, mediaType, securityContextReference) + .getResource(); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return postBundle(PreferReturnType.REPRESENTATION, bundle); + } + + @Override + public void delete(Class resourceClass, String id) + { + Objects.requireNonNull(resourceClass, "resourceClass"); + Objects.requireNonNull(id, "id"); + + Response response = getResource().path(resourceClass.getAnnotation(ResourceDef.class).name()).path(id).request() + .accept(Constants.CT_FHIR_JSON_NEW).delete(); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() != response.getStatus() + && Status.NO_CONTENT.getStatusCode() != response.getStatus()) + throw handleError(response); + else + response.close(); + } + + @Override + public void deleteConditionaly(Class resourceClass, Map> criteria) + { + Objects.requireNonNull(resourceClass, "resourceClass"); + Objects.requireNonNull(criteria, "criteria"); + if (criteria.isEmpty()) + throw new IllegalArgumentException("criteria map empty"); + + WebTarget target = getResource().path(resourceClass.getAnnotation(ResourceDef.class).name()); + + for (Entry> entry : criteria.entrySet()) + target = target.queryParam(entry.getKey(), entry.getValue().toArray()); + + Response response = target.request().accept(Constants.CT_FHIR_JSON_NEW).delete(); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() != response.getStatus() + && Status.NO_CONTENT.getStatusCode() != response.getStatus()) + throw handleError(response); + else + response.close(); + } + + @Override + public void deletePermanently(Class resourceClass, String id) + { + Objects.requireNonNull(resourceClass, "resourceClass"); + Objects.requireNonNull(id, "id"); + + Response response = getResource().path(resourceClass.getAnnotation(ResourceDef.class).name()).path(id) + .path("$permanent-delete").request().accept(Constants.CT_FHIR_JSON_NEW).post(null); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() != response.getStatus()) + throw handleError(response); + else + response.close(); + } + + @Override + public Resource read(String resourceTypeName, String id) + { + Objects.requireNonNull(resourceTypeName, "resourceTypeName"); + Objects.requireNonNull(id, "id"); + if (!RESOURCE_TYPES_BY_NAME.containsKey(resourceTypeName)) + throw new IllegalArgumentException("Resource of type " + resourceTypeName + " not supported"); + + Response response = getResource().path(resourceTypeName).path(id).request().accept(Constants.CT_FHIR_JSON_NEW) + .get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return (Resource) response.readEntity(RESOURCE_TYPES_BY_NAME.get(resourceTypeName)); + else + throw handleError(response); + } + + @Override + public R read(Class resourceType, String id) + { + return read(resourceType, id, (R) null); + } + + @Override + @SuppressWarnings("unchecked") + public R read(R oldValue) + { + return read((Class) oldValue.getClass(), oldValue.getIdElement().getIdPart(), oldValue); + } + + private R read(Class resourceType, String id, R oldValue) + { + Objects.requireNonNull(resourceType, "resourceType"); + Objects.requireNonNull(id, "id"); + + Builder request = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()).path(id).request(); + + if (oldValue != null && oldValue.hasMeta()) + { + if (oldValue.getMeta().hasVersionId()) + { + EntityTag eTag = new EntityTag(oldValue.getMeta().getVersionIdElement().getValue(), true); + String eTagValue = RuntimeDelegate.getInstance().createHeaderDelegate(EntityTag.class).toString(eTag); + request.header(HttpHeaders.IF_NONE_MATCH, eTagValue); + logger.trace("Sending {} Header with value '{}'", HttpHeaders.IF_NONE_MATCH, eTagValue); + } + + if (oldValue.getMeta().hasLastUpdated()) + { + String dateValue = formatRfc7231(oldValue.getMeta().getLastUpdated()); + request.header(HttpHeaders.IF_MODIFIED_SINCE, dateValue); + logger.trace("Sending {} Header with value '{}'", HttpHeaders.IF_MODIFIED_SINCE, dateValue); + } + } + + Response response = request.accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(resourceType); + else if (oldValue != null && oldValue.hasMeta() + && (oldValue.getMeta().hasVersionId() || oldValue.getMeta().hasLastUpdated()) + && Status.NOT_MODIFIED.getStatusCode() == response.getStatus()) + return oldValue; + else + throw handleError(response); + } + + private String formatRfc7231(Date date) + { + if (date == null) + return null; + else + { + SimpleDateFormat dateFormat = new SimpleDateFormat(RFC_7231_FORMAT, Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + return dateFormat.format(date); + } + } + + @Override + public boolean exists(Class resourceType, String id) + { + Objects.requireNonNull(resourceType, "resourceType"); + Objects.requireNonNull(id, "id"); + + Response response = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()).path(id).request() + .accept(Constants.CT_FHIR_JSON_NEW).head(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return true; + else if (Status.NOT_FOUND.getStatusCode() == response.getStatus()) + return false; + else + throw handleError(response); + } + + @Override + public InputStream readBinary(String id, MediaType mediaType) + { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(mediaType, "mediaType"); + + Response response = getResource().path("Binary").path(id).request().accept(mediaType).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(InputStream.class); + else + throw handleError(response); + } + + @Override + public Resource read(String resourceTypeName, String id, String version) + { + Objects.requireNonNull(resourceTypeName, "resourceTypeName"); + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(version, "version"); + if (!RESOURCE_TYPES_BY_NAME.containsKey(resourceTypeName)) + throw new IllegalArgumentException("Resource of type " + resourceTypeName + " not supported"); + + Response response = getResource().path(resourceTypeName).path(id).path("_history").path(version).request() + .accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return (Resource) response.readEntity(RESOURCE_TYPES_BY_NAME.get(resourceTypeName)); + else + throw handleError(response); + } + + @Override + public R read(Class resourceType, String id, String version) + { + Objects.requireNonNull(resourceType, "resourceType"); + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(version, "version"); + + Response response = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()).path(id) + .path("_history").path(version).request().accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(resourceType); + else + throw handleError(response); + } + + @Override + public boolean exists(Class resourceType, String id, String version) + { + Objects.requireNonNull(resourceType, "resourceType"); + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(version, "version"); + + Response response = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()).path(id) + .path("_history").path(version).request().accept(Constants.CT_FHIR_JSON_NEW).head(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return true; + else if (Status.NOT_FOUND.getStatusCode() == response.getStatus()) + return false; + else + throw handleError(response); + } + + @Override + public InputStream readBinary(String id, String version, MediaType mediaType) + { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(version, "version"); + Objects.requireNonNull(mediaType, "mediaType"); + + Response response = getResource().path("Binary").path(id).path("_history").path(version).request() + .accept(mediaType).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(InputStream.class); + else + throw handleError(response); + } + + @Override + public boolean exists(IdType resourceTypeIdVersion) + { + Objects.requireNonNull(resourceTypeIdVersion, "resourceTypeIdVersion"); + Objects.requireNonNull(resourceTypeIdVersion.getResourceType(), "resourceTypeIdVersion.resourceType"); + Objects.requireNonNull(resourceTypeIdVersion.getIdPart(), "resourceTypeIdVersion.idPart"); + // version may be null + + WebTarget path = getResource().path(resourceTypeIdVersion.getResourceType()) + .path(resourceTypeIdVersion.getIdPart()); + + if (resourceTypeIdVersion.hasVersionIdPart()) + path = path.path("_history").path(resourceTypeIdVersion.getVersionIdPart()); + + Response response = path.request().accept(Constants.CT_FHIR_JSON_NEW).head(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return true; + else if (Status.NOT_FOUND.getStatusCode() == response.getStatus()) + return false; + else + throw handleError(response); + } + + @Override + public Bundle search(Class resourceType, Map> parameters) + { + Objects.requireNonNull(resourceType, "resourceType"); + + WebTarget target = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()); + if (parameters != null) + { + for (Entry> entry : parameters.entrySet()) + target = target.queryParam(entry.getKey(), entry.getValue().toArray()); + } + + Response response = target.request().accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(Bundle.class); + else + throw handleError(response); + } + + @Override + public Bundle searchWithStrictHandling(Class resourceType, Map> parameters) + { + Objects.requireNonNull(resourceType, "resourceType"); + + WebTarget target = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()); + if (parameters != null) + { + for (Entry> entry : parameters.entrySet()) + target = target.queryParam(entry.getKey(), entry.getValue().toArray()); + } + + Response response = target.request().header(Constants.HEADER_PREFER, PreferHandlingType.STRICT.getHeaderValue()) + .accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(Bundle.class); + else + throw handleError(response); + } + + @Override + public CapabilityStatement getConformance() + { + Response response = getResource().path("metadata").request() + .accept(Constants.CT_FHIR_JSON_NEW + "; fhirVersion=4.0").get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(CapabilityStatement.class); + else + throw handleError(response); + } + + @Override + public StructureDefinition generateSnapshot(String url) + { + Objects.requireNonNull(url, "url"); + + Parameters parameters = new Parameters(); + parameters.addParameter().setName("url").setValue(new UriType(url)); + + Response response = getResource().path(StructureDefinition.class.getAnnotation(ResourceDef.class).name()) + .path("$snapshot").request().accept(Constants.CT_FHIR_JSON_NEW) + .post(Entity.entity(parameters, Constants.CT_FHIR_JSON_NEW)); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(StructureDefinition.class); + else + throw handleError(response); + } + + @Override + public StructureDefinition generateSnapshot(StructureDefinition differential) + { + Objects.requireNonNull(differential, "differential"); + + Parameters parameters = new Parameters(); + parameters.addParameter().setName("resource").setResource(differential); + + Response response = getResource().path(StructureDefinition.class.getAnnotation(ResourceDef.class).name()) + .path("$snapshot").request().accept(Constants.CT_FHIR_JSON_NEW) + .post(Entity.entity(parameters, Constants.CT_FHIR_JSON_NEW)); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(StructureDefinition.class); + else + throw handleError(response); + } + + @Override + public BasicFhirWebserviceClient withRetry(int nTimes, long delayMillis) + { + if (nTimes < 0) + throw new IllegalArgumentException("nTimes < 0"); + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new BasicFhirWebserviceCientWithRetryImpl(this, nTimes, delayMillis); + } + + @Override + public BasicFhirWebserviceClient withRetryForever(long delayMillis) + { + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new BasicFhirWebserviceCientWithRetryImpl(this, RETRY_FOREVER, delayMillis); + } + + @Override + public Bundle history(Class resourceType, String id, int page, int count) + { + WebTarget target = getResource(); + + if (resourceType != null) + target = target.path(resourceType.getAnnotation(ResourceDef.class).name()); + + if (!StringUtils.isBlank(id)) + target = target.path(id); + + if (page != Integer.MIN_VALUE) + target = target.queryParam("_page", page); + + if (count != Integer.MIN_VALUE) + target = target.queryParam("_count", count); + + Response response = target.path("_history").request().accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(Bundle.class); + else + throw handleError(response); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferHandlingType.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferHandlingType.java new file mode 100644 index 000000000..1d8682eb1 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferHandlingType.java @@ -0,0 +1,31 @@ +package dev.dsf.bpe.v2.client; + +public enum PreferHandlingType +{ + STRICT("handling=strict"), LENIENT("handling=lenient"); + + private final String headerValue; + + PreferHandlingType(String headerValue) + { + this.headerValue = headerValue; + } + + public static PreferHandlingType fromString(String prefer) + { + if (prefer == null) + return LENIENT; + + return switch (prefer) + { + case "handling=strict" -> STRICT; + case "handling=lenient" -> LENIENT; + default -> LENIENT; + }; + } + + public String getHeaderValue() + { + return headerValue; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturn.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturn.java new file mode 100644 index 000000000..f80b82b8d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturn.java @@ -0,0 +1,51 @@ +package dev.dsf.bpe.v2.client; + +import java.net.URI; + +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; + +public class PreferReturn +{ + private final IdType id; + private final Resource resource; + private final OperationOutcome operationOutcome; + + private PreferReturn(IdType id, Resource resource, OperationOutcome operationOutcome) + { + this.id = id; + this.resource = resource; + this.operationOutcome = operationOutcome; + } + + public static PreferReturn minimal(URI location) + { + return new PreferReturn(new IdType(location.toString()), null, null); + } + + public static PreferReturn resource(Resource resource) + { + return new PreferReturn(null, resource, null); + } + + public static PreferReturn outcome(OperationOutcome operationOutcome) + { + return new PreferReturn(null, null, operationOutcome); + } + + public IdType getId() + { + return id; + } + + public Resource getResource() + { + return resource; + } + + public OperationOutcome getOperationOutcome() + { + return operationOutcome; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimalRetryImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimalRetryImpl.java new file mode 100644 index 000000000..6a10c5c1e --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimalRetryImpl.java @@ -0,0 +1,65 @@ +package dev.dsf.bpe.v2.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; + +class PreferReturnMinimalRetryImpl extends AbstractFhirWebserviceClientJerseyWithRetry implements PreferReturnMinimal +{ + PreferReturnMinimalRetryImpl(FhirWebserviceClientJersey delegate, int nTimes, long delayMillis) + { + super(delegate, nTimes, delayMillis); + } + + @Override + public IdType create(Resource resource) + { + return retry(nTimes, delayMillis, () -> delegate.create(PreferReturnType.MINIMAL, resource).getId()); + } + + @Override + public IdType createConditionaly(Resource resource, String ifNoneExistCriteria) + { + return retry(nTimes, delayMillis, + () -> delegate.createConditionaly(PreferReturnType.MINIMAL, resource, ifNoneExistCriteria).getId()); + } + + @Override + public IdType createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return retry(nTimes, delayMillis, + () -> delegate.createBinary(PreferReturnType.MINIMAL, in, mediaType, securityContextReference).getId()); + } + + @Override + public IdType update(Resource resource) + { + return retry(nTimes, delayMillis, () -> delegate.update(PreferReturnType.MINIMAL, resource).getId()); + } + + @Override + public IdType updateConditionaly(Resource resource, Map> criteria) + { + return retry(nTimes, delayMillis, + () -> delegate.updateConditionaly(PreferReturnType.MINIMAL, resource, criteria).getId()); + } + + @Override + public IdType updateBinary(String id, InputStream in, MediaType mediaType, String securityContextReference) + { + return retry(nTimes, delayMillis, () -> delegate + .updateBinary(PreferReturnType.MINIMAL, id, in, mediaType, securityContextReference).getId()); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return retry(nTimes, delayMillis, () -> delegate.postBundle(PreferReturnType.MINIMAL, bundle)); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimalWithRetryImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimalWithRetryImpl.java new file mode 100644 index 000000000..c1315bbb5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimalWithRetryImpl.java @@ -0,0 +1,83 @@ +package dev.dsf.bpe.v2.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; + +class PreferReturnMinimalWithRetryImpl implements PreferReturnMinimalWithRetry +{ + private final FhirWebserviceClientJersey delegate; + + PreferReturnMinimalWithRetryImpl(FhirWebserviceClientJersey delegate) + { + this.delegate = delegate; + } + + @Override + public IdType create(Resource resource) + { + return delegate.create(PreferReturnType.MINIMAL, resource).getId(); + } + + @Override + public IdType createConditionaly(Resource resource, String ifNoneExistCriteria) + { + return delegate.createConditionaly(PreferReturnType.MINIMAL, resource, ifNoneExistCriteria).getId(); + } + + @Override + public IdType createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return delegate.createBinary(PreferReturnType.MINIMAL, in, mediaType, securityContextReference).getId(); + } + + @Override + public IdType update(Resource resource) + { + return delegate.update(PreferReturnType.MINIMAL, resource).getId(); + } + + @Override + public IdType updateConditionaly(Resource resource, Map> criteria) + { + return delegate.updateConditionaly(PreferReturnType.MINIMAL, resource, criteria).getId(); + } + + @Override + public IdType updateBinary(String id, InputStream in, MediaType mediaType, String securityContextReference) + { + return delegate.updateBinary(PreferReturnType.MINIMAL, id, in, mediaType, securityContextReference).getId(); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return delegate.postBundle(PreferReturnType.MINIMAL, bundle); + } + + @Override + public PreferReturnMinimal withRetry(int nTimes, long delayMillis) + { + if (nTimes < 0) + throw new IllegalArgumentException("nTimes < 0"); + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new PreferReturnMinimalRetryImpl(delegate, nTimes, delayMillis); + } + + @Override + public PreferReturnMinimal withRetryForever(long delayMillis) + { + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new PreferReturnMinimalRetryImpl(delegate, RETRY_FOREVER, delayMillis); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcomeRetryImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcomeRetryImpl.java new file mode 100644 index 000000000..e7960fa5f --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcomeRetryImpl.java @@ -0,0 +1,72 @@ +package dev.dsf.bpe.v2.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; + +class PreferReturnOutcomeRetryImpl extends AbstractFhirWebserviceClientJerseyWithRetry implements PreferReturnOutcome +{ + PreferReturnOutcomeRetryImpl(FhirWebserviceClientJersey delegate, int nTimes, long delayMillis) + { + super(delegate, nTimes, delayMillis); + } + + @Override + public OperationOutcome create(Resource resource) + { + return retry(nTimes, delayMillis, + () -> delegate.create(PreferReturnType.OPERATION_OUTCOME, resource).getOperationOutcome()); + } + + @Override + public OperationOutcome createConditionaly(Resource resource, String ifNoneExistCriteria) + { + return retry(nTimes, delayMillis, + () -> delegate.createConditionaly(PreferReturnType.OPERATION_OUTCOME, resource, ifNoneExistCriteria) + .getOperationOutcome()); + } + + @Override + public OperationOutcome createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return retry(nTimes, delayMillis, + () -> delegate.createBinary(PreferReturnType.OPERATION_OUTCOME, in, mediaType, securityContextReference) + .getOperationOutcome()); + } + + @Override + public OperationOutcome update(Resource resource) + { + return retry(nTimes, delayMillis, + () -> delegate.update(PreferReturnType.OPERATION_OUTCOME, resource).getOperationOutcome()); + } + + @Override + public OperationOutcome updateConditionaly(Resource resource, Map> criteria) + { + return retry(nTimes, delayMillis, () -> delegate + .updateConditionaly(PreferReturnType.OPERATION_OUTCOME, resource, criteria).getOperationOutcome()); + } + + @Override + public OperationOutcome updateBinary(String id, InputStream in, MediaType mediaType, + String securityContextReference) + { + return retry(nTimes, delayMillis, + () -> delegate + .updateBinary(PreferReturnType.OPERATION_OUTCOME, id, in, mediaType, securityContextReference) + .getOperationOutcome()); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return retry(nTimes, delayMillis, () -> delegate.postBundle(PreferReturnType.OPERATION_OUTCOME, bundle)); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcomeWithRetryImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcomeWithRetryImpl.java new file mode 100644 index 000000000..ebd2bf949 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcomeWithRetryImpl.java @@ -0,0 +1,88 @@ +package dev.dsf.bpe.v2.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; + +class PreferReturnOutcomeWithRetryImpl implements PreferReturnOutcomeWithRetry +{ + private final FhirWebserviceClientJersey delegate; + + PreferReturnOutcomeWithRetryImpl(FhirWebserviceClientJersey delegate) + { + this.delegate = delegate; + } + + @Override + public OperationOutcome create(Resource resource) + { + return delegate.create(PreferReturnType.OPERATION_OUTCOME, resource).getOperationOutcome(); + } + + @Override + public OperationOutcome createConditionaly(Resource resource, String ifNoneExistCriteria) + { + return delegate.createConditionaly(PreferReturnType.OPERATION_OUTCOME, resource, ifNoneExistCriteria) + .getOperationOutcome(); + } + + @Override + public OperationOutcome createBinary(InputStream in, MediaType mediaType, String securityContextReference) + { + return delegate.createBinary(PreferReturnType.OPERATION_OUTCOME, in, mediaType, securityContextReference) + .getOperationOutcome(); + } + + @Override + public OperationOutcome update(Resource resource) + { + return delegate.update(PreferReturnType.OPERATION_OUTCOME, resource).getOperationOutcome(); + } + + @Override + public OperationOutcome updateConditionaly(Resource resource, Map> criteria) + { + return delegate.updateConditionaly(PreferReturnType.OPERATION_OUTCOME, resource, criteria) + .getOperationOutcome(); + } + + @Override + public OperationOutcome updateBinary(String id, InputStream in, MediaType mediaType, + String securityContextReference) + { + return delegate.updateBinary(PreferReturnType.OPERATION_OUTCOME, id, in, mediaType, securityContextReference) + .getOperationOutcome(); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return delegate.postBundle(PreferReturnType.OPERATION_OUTCOME, bundle); + } + + @Override + public PreferReturnOutcome withRetry(int nTimes, long delayMillis) + { + if (nTimes < 0) + throw new IllegalArgumentException("nTimes < 0"); + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new PreferReturnOutcomeRetryImpl(delegate, nTimes, delayMillis); + } + + @Override + public PreferReturnOutcome withRetryForever(long delayMillis) + { + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new PreferReturnOutcomeRetryImpl(delegate, RETRY_FOREVER, delayMillis); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnType.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnType.java new file mode 100644 index 000000000..086e43001 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/PreferReturnType.java @@ -0,0 +1,32 @@ +package dev.dsf.bpe.v2.client; + +public enum PreferReturnType +{ + MINIMAL("return=minimal"), REPRESENTATION("return=representation"), OPERATION_OUTCOME("return=OperationOutcome"); + + private final String headerValue; + + PreferReturnType(String headerValue) + { + this.headerValue = headerValue; + } + + public static PreferReturnType fromString(String prefer) + { + if (prefer == null) + return REPRESENTATION; + + return switch (prefer) + { + case "return=minimal" -> MINIMAL; + case "return=OperationOutcome" -> OPERATION_OUTCOME; + case "return=representation" -> REPRESENTATION; + default -> REPRESENTATION; + }; + } + + public String getHeaderValue() + { + return headerValue; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceCleaner.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceCleaner.java new file mode 100644 index 000000000..a037dcf5a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceCleaner.java @@ -0,0 +1,18 @@ +package dev.dsf.bpe.v2.client; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Resource; + +public interface ReferenceCleaner +{ + /** + * Removes embedded resources from references within {@link Bundle} entries + * + * @param + * the resource type + * @param resource + * the resource to clean, may be null + * @return null if given resource is null, cleaned up resource (same instance) + */ + R cleanReferenceResourcesIfBundle(R resource); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceCleanerImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceCleanerImpl.java new file mode 100644 index 000000000..d22a37aec --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceCleanerImpl.java @@ -0,0 +1,63 @@ +package dev.dsf.bpe.v2.client; + +import java.util.Objects; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.DomainResource; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +public class ReferenceCleanerImpl implements ReferenceCleaner, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(ReferenceCleanerImpl.class); + + private final ReferenceExtractor referenceExtractor; + + public ReferenceCleanerImpl(ReferenceExtractor referenceExtractor) + { + this.referenceExtractor = referenceExtractor; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(referenceExtractor, "referenceExtractor"); + } + + @Override + public R cleanReferenceResourcesIfBundle(R resource) + { + if (resource == null) + return null; + + if (resource instanceof Bundle b) + b.getEntry().stream().map(BundleEntryComponent::getResource).forEach(this::fixBundleEntry); + + return resource; + } + + private void fixBundleEntry(Resource resource) + { + if (resource instanceof Bundle) + { + cleanReferenceResourcesIfBundle(resource); + } + else + { + Stream references = referenceExtractor.getReferences(resource); + + references.filter(r -> r != null).forEach(r -> r.setResource(null)); + + if (resource instanceof DomainResource d && d.hasContained()) + { + logger.warn("{} has contained resources, removing resources", resource.getClass().getName()); + d.setContained(null); + } + } + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceExtractor.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceExtractor.java new file mode 100644 index 000000000..6aad6da11 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceExtractor.java @@ -0,0 +1,11 @@ +package dev.dsf.bpe.v2.client; + +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; + +public interface ReferenceExtractor +{ + Stream getReferences(Resource resource); +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceExtractorImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceExtractorImpl.java new file mode 100644 index 000000000..72dc507ae --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/client/ReferenceExtractorImpl.java @@ -0,0 +1,606 @@ +package dev.dsf.bpe.v2.client; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.ActivityDefinition; +import org.hl7.fhir.r4.model.BackboneElement; +import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.DocumentReference; +import org.hl7.fhir.r4.model.DocumentReference.DocumentReferenceContextComponent; +import org.hl7.fhir.r4.model.DocumentReference.DocumentReferenceRelatesToComponent; +import org.hl7.fhir.r4.model.DomainResource; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.HealthcareService; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent; +import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent; +import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupStratifierComponent; +import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupComponent; +import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupPopulationComponent; +import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.OrganizationAffiliation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Patient.ContactComponent; +import org.hl7.fhir.r4.model.Patient.PatientLinkComponent; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Practitioner.PractitionerQualificationComponent; +import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Provenance; +import org.hl7.fhir.r4.model.Provenance.ProvenanceAgentComponent; +import org.hl7.fhir.r4.model.Provenance.ProvenanceEntityComponent; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.ResearchStudy; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.Subscription; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.ValueSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReferenceExtractorImpl implements ReferenceExtractor +{ + private static final Logger logger = LoggerFactory.getLogger(ReferenceExtractorImpl.class); + + private Stream getReference(R resource, Predicate hasReference, + Function getReference) + { + return hasReference.test(resource) ? Stream.of(getReference.apply(resource)) : Stream.empty(); + } + + private Stream getReferences(R resource, Predicate hasReference, + Function> getReference) + { + return hasReference.test(resource) ? Stream.of(getReference.apply(resource)).flatMap(List::stream) + : Stream.empty(); + } + + private Stream getBackboneElementsReference(R resource, + Predicate hasBackboneElements, Function> getBackboneElements, Predicate hasReference, + Function getReference) + { + if (hasBackboneElements.test(resource)) + { + List backboneElements = getBackboneElements.apply(resource); + return backboneElements.stream().map(e -> getReference(e, hasReference, getReference)) + .flatMap(Function.identity()); + } + else + return Stream.empty(); + } + + private Stream getReference(E backboneElement, Predicate hasReference, + Function getReference) + { + return hasReference.test(backboneElement) ? Stream.of(getReference.apply(backboneElement)) : Stream.empty(); + } + + private Stream getBackboneElementReferences( + R resource, Predicate hasBackboneElement, Function getBackboneElement, Predicate hasReference, + Function> getReference) + { + if (hasBackboneElement.test(resource)) + { + E backboneElement = getBackboneElement.apply(resource); + return getReferences(backboneElement, hasReference, getReference); + } + else + return Stream.empty(); + } + + private Stream getBackboneElementReference( + R resource, Predicate hasBackboneElement, Function getBackboneElement, Predicate hasReference, + Function getReference) + { + if (hasBackboneElement.test(resource)) + { + E backboneElement = getBackboneElement.apply(resource); + return getReference(backboneElement, hasReference, getReference); + } + else + return Stream.empty(); + } + + private Stream getBackboneElements2Reference( + R resource, Predicate hasBackboneElements1, Function> getBackboneElements1, + Predicate hasBackboneElements2, Function> getBackboneElements2, Predicate hasReference, + Function getReference) + { + if (hasBackboneElements1.test(resource)) + { + List backboneElements1 = getBackboneElements1.apply(resource); + return backboneElements1.stream().filter(e1 -> hasBackboneElements2.test(e1)) + .flatMap(e1 -> getBackboneElements2.apply(e1).stream()) + .map(e2 -> getReference(e2, hasReference, getReference)).flatMap(Function.identity()); + } + else + return Stream.empty(); + } + + private Stream getBackboneElements4Reference( + R resource, Predicate hasBackboneElements1, Function> getBackboneElements1, + Predicate hasBackboneElements2, Function> getBackboneElements2, + Predicate hasBackboneElements3, Function> getBackboneElements3, + Predicate hasBackboneElements4, Function> getBackboneElements4, Predicate hasReference, + Function getReference) + { + if (hasBackboneElements1.test(resource)) + { + List backboneElements1 = getBackboneElements1.apply(resource); + return backboneElements1.stream().filter(e1 -> hasBackboneElements2.test(e1)) + .flatMap(e1 -> getBackboneElements2.apply(e1).stream()).filter(e2 -> hasBackboneElements3.test(e2)) + .flatMap(e2 -> getBackboneElements3.apply(e2).stream()).filter(e3 -> hasBackboneElements4.test(e3)) + .flatMap(e3 -> getBackboneElements4.apply(e3).stream()) + .map(e4 -> getReference(e4, hasReference, getReference)).flatMap(Function.identity()); + } + else + return Stream.empty(); + } + + private Stream getReferences(E backboneElement, Predicate hasReference, + Function> getReference) + { + return hasReference.test(backboneElement) ? Stream.of(getReference.apply(backboneElement)).flatMap(List::stream) + : Stream.empty(); + } + + private Stream getExtensionReferences(DomainResource resource) + { + var extensions = resource.getExtension().stream().filter(e -> e.getValue() instanceof Reference) + .map(e -> (Reference) e.getValue()); + + var extensionExtensions = resource.getExtension().stream().flatMap(this::getExtensionReferences); + + return Stream.concat(extensions, extensionExtensions); + } + + private Stream getExtensionReferences(BackboneElement resource) + { + var extensions = resource.getExtension().stream().filter(e -> e.getValue() instanceof Reference) + .map(e -> (Reference) e.getValue()); + + var extensionExtensions = resource.getExtension().stream().flatMap(this::getExtensionReferences); + + return Stream.concat(extensions, extensionExtensions); + } + + private Stream getExtensionReferences(Extension resource) + { + var extensions = resource.getExtension().stream().filter(e -> e.getValue() instanceof Reference) + .map(e -> (Reference) e.getValue()); + + var extensionExtensions = resource.getExtension().stream().flatMap(this::getExtensionReferences); + + return Stream.concat(extensions, extensionExtensions); + } + + @SafeVarargs + private Stream concat(Stream... streams) + { + if (streams.length == 0) + return Stream.empty(); + else if (streams.length == 1) + return streams[0]; + else if (streams.length == 2) + return Stream.concat(streams[0], streams[1]); + else + return Arrays.stream(streams).flatMap(Function.identity()); + } + + @Override + public Stream getReferences(Resource resource) + { + return switch (resource) + { + case null -> Stream.empty(); + + case ActivityDefinition ad -> getReferences(ad); + + // not implemented yet, special rules apply for tmp ids + // case Bundle b -> getReferences(b); + + case Binary b -> getReferences(b); + case CodeSystem cs -> getReferences(cs); + case DocumentReference dr -> getReferences(dr); + case Endpoint e -> getReferences(e); + case Group g -> getReferences(g); + case HealthcareService hs -> getReferences(hs); + case Library l -> getReferences(l); + case Location l -> getReferences(l); + case Measure m -> getReferences(m); + case MeasureReport mr -> getReferences(mr); + case NamingSystem ns -> getReferences(ns); + case OperationOutcome oo -> getReferences(oo); + case Organization o -> getReferences(o); + case OrganizationAffiliation oa -> getReferences(oa); + case Patient p -> getReferences(p); + case Practitioner p -> getReferences(p); + case PractitionerRole pr -> getReferences(pr); + case Provenance p -> getReferences(p); + case Questionnaire q -> getReferences(q); + case QuestionnaireResponse qr -> getReferences(qr); + case ResearchStudy rs -> getReferences(rs); + case StructureDefinition sd -> getReferences(sd); + case Subscription s -> getReferences(s); + case Task t -> getReferences(t); + case ValueSet vs -> getReferences(vs); + + case DomainResource dr -> { + logger.debug("DomainResource of type {} not supported, returning extension references only", + dr.getClass().getName()); + yield getExtensionReferences(dr); + } + + default -> { + logger.debug("Resource of type {} not supported, returning no references", + resource.getClass().getName()); + yield Stream.empty(); + } + }; + } + + private Stream getReferences(ActivityDefinition resource) + { + var subjectReference = getReference(resource, ActivityDefinition::hasSubjectReference, + ActivityDefinition::getSubjectReference); + var location = getReference(resource, ActivityDefinition::hasLocation, ActivityDefinition::getLocation); + var productReference = getReference(resource, ActivityDefinition::hasProductReference, + ActivityDefinition::getProductReference); + var specimenRequirement = getReferences(resource, ActivityDefinition::hasSpecimenRequirement, + ActivityDefinition::getSpecimenRequirement); + var observationRequirement = getReferences(resource, ActivityDefinition::hasObservationRequirement, + ActivityDefinition::getObservationRequirement); + var observationResultRequirement = getReferences(resource, ActivityDefinition::hasObservationResultRequirement, + ActivityDefinition::getObservationResultRequirement); + + var extensionReferences = getExtensionReferences(resource); + + return concat(subjectReference, location, productReference, specimenRequirement, observationRequirement, + observationResultRequirement, extensionReferences); + } + + private Stream getReferences(Binary resource) + { + var securityContext = getReference(resource, Binary::hasSecurityContext, Binary::getSecurityContext); + + return securityContext; + } + + private Stream getReferences(CodeSystem resource) + { + var extensionReferences = getExtensionReferences(resource); + + return extensionReferences; + } + + private Stream getReferences(DocumentReference resource) + { + var subject = getReference(resource, DocumentReference::hasSubject, DocumentReference::getSubject); + var author = getReferences(resource, DocumentReference::hasAuthor, DocumentReference::getAuthor); + var authenticator = getReference(resource, DocumentReference::hasAuthenticator, + DocumentReference::getAuthenticator); + var custodian = getReference(resource, DocumentReference::hasCustodian, DocumentReference::getCustodian); + var relatesToTarget = getBackboneElementsReference(resource, DocumentReference::hasRelatesTo, + DocumentReference::getRelatesTo, DocumentReferenceRelatesToComponent::hasTarget, + DocumentReferenceRelatesToComponent::getTarget); + var contextEncounters = getBackboneElementReferences(resource, DocumentReference::hasContent, + DocumentReference::getContext, DocumentReferenceContextComponent::hasEncounter, + DocumentReferenceContextComponent::getEncounter); + var contextSourcePatientInfo = getBackboneElementReference(resource, DocumentReference::hasContent, + DocumentReference::getContext, DocumentReferenceContextComponent::hasSourcePatientInfo, + DocumentReferenceContextComponent::getSourcePatientInfo); + var contextRelated = getBackboneElementReferences(resource, DocumentReference::hasContent, + DocumentReference::getContext, DocumentReferenceContextComponent::hasRelated, + DocumentReferenceContextComponent::getRelated); + + var extensionReferences = getExtensionReferences(resource); + + return concat(subject, author, authenticator, custodian, relatesToTarget, contextEncounters, + contextSourcePatientInfo, contextRelated, extensionReferences); + } + + private Stream getReferences(Endpoint resource) + { + var managingOrganization = getReference(resource, Endpoint::hasManagingOrganization, + Endpoint::getManagingOrganization); + + var extensionReferences = getExtensionReferences(resource); + + return concat(managingOrganization, extensionReferences); + } + + private Stream getReferences(Group resource) + { + var managingEntity = getReference(resource, Group::hasManagingEntity, Group::getManagingEntity); + var memberEntities = getBackboneElementsReference(resource, Group::hasMember, Group::getMember, + Group.GroupMemberComponent::hasEntity, Group.GroupMemberComponent::getEntity); + + var extensionReferences = getExtensionReferences(resource); + + return concat(managingEntity, memberEntities, extensionReferences); + } + + private Stream getReferences(HealthcareService resource) + { + var providedBy = getReference(resource, HealthcareService::hasProvidedBy, HealthcareService::getProvidedBy); + var locations = getReferences(resource, HealthcareService::hasLocation, HealthcareService::getLocation); + var coverageAreas = getReferences(resource, HealthcareService::hasCoverageArea, + HealthcareService::getCoverageArea); + var endpoints = getReferences(resource, HealthcareService::hasEndpoint, HealthcareService::getEndpoint); + + var extensionReferences = getExtensionReferences(resource); + + return concat(providedBy, locations, coverageAreas, endpoints, extensionReferences); + } + + private Stream getReferences(Library resource) + { + var subject = getReference(resource, Library::hasSubjectReference, Library::getSubjectReference); + + var extensionReferences = getExtensionReferences(resource); + + return concat(subject, extensionReferences); + } + + private Stream getReferences(Location resource) + { + var managingOrganization = getReference(resource, Location::hasManagingOrganization, + Location::getManagingOrganization); + var partOf = getReference(resource, Location::hasPartOf, Location::getPartOf); + var endpoints = getReferences(resource, Location::hasEndpoint, Location::getEndpoint); + + var extensionReferences = getExtensionReferences(resource); + + return concat(managingOrganization, partOf, endpoints, extensionReferences); + } + + private Stream getReferences(Measure resource) + { + var subject = getReference(resource, Measure::hasSubjectReference, Measure::getSubjectReference); + + var extensionReferences = getExtensionReferences(resource); + + return concat(subject, extensionReferences); + } + + private Stream getReferences(MeasureReport resource) + { + var subject = getReference(resource, MeasureReport::hasSubject, MeasureReport::getSubject); + var reporter = getReference(resource, MeasureReport::hasReporter, MeasureReport::getReporter); + var subjectResults1 = getBackboneElements2Reference(resource, MeasureReport::hasGroup, MeasureReport::getGroup, + MeasureReportGroupComponent::hasPopulation, MeasureReportGroupComponent::getPopulation, + MeasureReportGroupPopulationComponent::hasSubjectResults, + MeasureReportGroupPopulationComponent::getSubjectResults); + var subjectResults2 = getBackboneElements4Reference(resource, MeasureReport::hasGroup, MeasureReport::getGroup, + MeasureReportGroupComponent::hasStratifier, MeasureReportGroupComponent::getStratifier, + MeasureReportGroupStratifierComponent::hasStratum, MeasureReportGroupStratifierComponent::getStratum, + StratifierGroupComponent::hasPopulation, StratifierGroupComponent::getPopulation, + StratifierGroupPopulationComponent::hasSubjectResults, + StratifierGroupPopulationComponent::getSubjectResults); + var evaluatedResource = getReferences(resource, MeasureReport::hasEvaluatedResource, + MeasureReport::getEvaluatedResource); + + var extensionReferences = getExtensionReferences(resource); + + return concat(subject, reporter, subjectResults1, subjectResults2, evaluatedResource, extensionReferences); + } + + private Stream getReferences(NamingSystem resource) + { + var extensionReferences = getExtensionReferences(resource); + + return extensionReferences; + } + + private Stream getReferences(OperationOutcome resource) + { + return getExtensionReferences(resource); + } + + private Stream getReferences(Organization resource) + { + var partOf = getReference(resource, Organization::hasPartOf, Organization::getPartOf); + var endpoints = getReferences(resource, Organization::hasEndpoint, Organization::getEndpoint); + + var extensionReferences = getExtensionReferences(resource); + + return concat(partOf, endpoints, extensionReferences); + } + + private Stream getReferences(OrganizationAffiliation resource) + { + var organization = getReference(resource, OrganizationAffiliation::hasOrganization, + OrganizationAffiliation::getOrganization); + var participatingOrganization = getReference(resource, OrganizationAffiliation::hasParticipatingOrganization, + OrganizationAffiliation::getParticipatingOrganization); + var network = getReferences(resource, OrganizationAffiliation::hasNetwork, OrganizationAffiliation::getNetwork); + var location = getReferences(resource, OrganizationAffiliation::hasLocation, + OrganizationAffiliation::getLocation); + var healthcareService = getReferences(resource, OrganizationAffiliation::hasHealthcareService, + OrganizationAffiliation::getHealthcareService); + var endpoint = getReferences(resource, OrganizationAffiliation::hasEndpoint, + OrganizationAffiliation::getEndpoint); + + var extensionReferences = getExtensionReferences(resource); + + return concat(organization, participatingOrganization, network, location, healthcareService, endpoint, + extensionReferences); + } + + private Stream getReferences(Patient resource) + { + var contactsOrganization = getBackboneElementsReference(resource, Patient::hasContact, Patient::getContact, + ContactComponent::hasOrganization, ContactComponent::getOrganization); + var generalPractitioners = getReferences(resource, Patient::hasGeneralPractitioner, + Patient::getGeneralPractitioner); + var managingOrganization = getReference(resource, Patient::hasManagingOrganization, + Patient::getManagingOrganization); + var linksOther = getBackboneElementsReference(resource, Patient::hasLink, Patient::getLink, + PatientLinkComponent::hasOther, PatientLinkComponent::getOther); + + var extensionReferences = getExtensionReferences(resource); + + return concat(contactsOrganization, generalPractitioners, managingOrganization, linksOther, + extensionReferences); + } + + private Stream getReferences(Practitioner resource) + { + var qualificationsIssuer = getBackboneElementsReference(resource, Practitioner::hasQualification, + Practitioner::getQualification, PractitionerQualificationComponent::hasIssuer, + PractitionerQualificationComponent::getIssuer); + + var extensionReferences = getExtensionReferences(resource); + + return concat(qualificationsIssuer, extensionReferences); + } + + private Stream getReferences(PractitionerRole resource) + { + var practitioner = getReference(resource, PractitionerRole::hasPractitioner, PractitionerRole::getPractitioner); + var organization = getReference(resource, PractitionerRole::hasOrganization, PractitionerRole::getOrganization); + var locations = getReferences(resource, PractitionerRole::hasLocation, PractitionerRole::getLocation); + var healthcareServices = getReferences(resource, PractitionerRole::hasHealthcareService, + PractitionerRole::getHealthcareService); + var endpoints = getReferences(resource, PractitionerRole::hasEndpoint, PractitionerRole::getEndpoint); + + var extensionReferences = getExtensionReferences(resource); + + return concat(practitioner, organization, locations, healthcareServices, endpoints, extensionReferences); + } + + private Stream getReferences(Provenance resource) + { + var targets = getReferences(resource, Provenance::hasTarget, Provenance::getTarget); + var location = getReference(resource, Provenance::hasLocation, Provenance::getLocation); + var agentsWho = getBackboneElementsReference(resource, Provenance::hasAgent, Provenance::getAgent, + ProvenanceAgentComponent::hasWho, ProvenanceAgentComponent::getWho); + var agentsOnBehalfOf = getBackboneElementsReference(resource, Provenance::hasAgent, Provenance::getAgent, + ProvenanceAgentComponent::hasOnBehalfOf, ProvenanceAgentComponent::getOnBehalfOf); + var entitiesWhat = getBackboneElementsReference(resource, Provenance::hasEntity, Provenance::getEntity, + ProvenanceEntityComponent::hasWhat, ProvenanceEntityComponent::getWhat); + + var extensionReferences = getExtensionReferences(resource); + + return concat(targets, location, agentsWho, agentsOnBehalfOf, entitiesWhat, extensionReferences); + } + + private Stream getReferences(Questionnaire resource) + { + var enableWhen = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, + Questionnaire.QuestionnaireItemComponent::hasEnableWhen, + Questionnaire.QuestionnaireItemComponent::getEnableWhen, + Questionnaire.QuestionnaireItemEnableWhenComponent::hasAnswerReference, + Questionnaire.QuestionnaireItemEnableWhenComponent::getAnswerReference); + var answerOption = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, + Questionnaire.QuestionnaireItemComponent::hasAnswerOption, + Questionnaire.QuestionnaireItemComponent::getAnswerOption, + Questionnaire.QuestionnaireItemAnswerOptionComponent::hasValueReference, + Questionnaire.QuestionnaireItemAnswerOptionComponent::getValueReference); + var initial = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, + Questionnaire.QuestionnaireItemComponent::hasInitial, + Questionnaire.QuestionnaireItemComponent::getInitial, + Questionnaire.QuestionnaireItemInitialComponent::hasValueReference, + Questionnaire.QuestionnaireItemInitialComponent::getValueReference); + + var extensionReferences = getExtensionReferences(resource); + + return concat(enableWhen, answerOption, initial, extensionReferences); + } + + private Stream getReferences(QuestionnaireResponse resource) + { + var author = getReference(resource, QuestionnaireResponse::hasAuthor, QuestionnaireResponse::getAuthor); + var basedOn = getReferences(resource, QuestionnaireResponse::hasBasedOn, QuestionnaireResponse::getBasedOn); + var encounter = getReference(resource, QuestionnaireResponse::hasEncounter, + QuestionnaireResponse::getEncounter); + var partOf = getReferences(resource, QuestionnaireResponse::hasPartOf, QuestionnaireResponse::getPartOf); + var source = getReference(resource, QuestionnaireResponse::hasSource, QuestionnaireResponse::getSource); + var subject = getReference(resource, QuestionnaireResponse::hasSubject, QuestionnaireResponse::getSubject); + + var extensionReferences = getExtensionReferences(resource); + + return concat(author, basedOn, encounter, partOf, source, subject, extensionReferences); + } + + private Stream getReferences(ResearchStudy resource) + { + var protocols = getReferences(resource, ResearchStudy::hasProtocol, ResearchStudy::getProtocol); + var partOfs = getReferences(resource, ResearchStudy::hasPartOf, ResearchStudy::getPartOf); + var enrollments = getReferences(resource, ResearchStudy::hasEnrollment, ResearchStudy::getEnrollment); + var sponsor = getReference(resource, ResearchStudy::hasSponsor, ResearchStudy::getSponsor); + var principalInvestigator = getReference(resource, ResearchStudy::hasPrincipalInvestigator, + ResearchStudy::getPrincipalInvestigator); + var sites = getReferences(resource, ResearchStudy::hasSite, ResearchStudy::getSite); + + var extensionReferences = getExtensionReferences(resource); + + return concat(protocols, partOfs, enrollments, sponsor, principalInvestigator, sites, extensionReferences); + } + + private Stream getReferences(StructureDefinition resource) + { + var extensionReferences = getExtensionReferences(resource); + + return extensionReferences; + } + + private Stream getReferences(Subscription resource) + { + var extensionReferences = getExtensionReferences(resource); + + return extensionReferences; + } + + private Stream getReferences(Task resource) + { + var basedOns = getReferences(resource, Task::hasBasedOn, Task::getBasedOn); + var partOfs = getReferences(resource, Task::hasPartOf, Task::getPartOf); + var focus = getReference(resource, Task::hasFocus, Task::getFocus); + var forRef = getReference(resource, Task::hasFor, Task::getFor); + var encounter = getReference(resource, Task::hasEncounter, Task::getEncounter); + var requester = getReference(resource, Task::hasRequester, Task::getRequester); + var owner = getReference(resource, Task::hasOwner, Task::getOwner); + var location = getReference(resource, Task::hasLocation, Task::getLocation); + var reasonReference = getReference(resource, Task::hasReasonReference, Task::getReasonReference); + var insurance = getReferences(resource, Task::hasInsurance, Task::getInsurance); + var relevanteHistories = getReferences(resource, Task::hasRelevantHistory, Task::getRelevantHistory); + var restrictionRecipiets = getBackboneElementReferences(resource, Task::hasRestriction, Task::getRestriction, + Task.TaskRestrictionComponent::hasRecipient, Task.TaskRestrictionComponent::getRecipient); + + var inputReferences = resource.getInput().stream().filter(in -> in.getValue() instanceof Reference) + .map(in -> (Reference) in.getValue()); + var inputExtensionReferences = resource.getInput().stream().flatMap(this::getExtensionReferences); + + var outputReferences = resource.getOutput().stream().filter(out -> out.getValue() instanceof Reference) + .map(in -> (Reference) in.getValue()); + var outputExtensionReferences = resource.getOutput().stream().flatMap(this::getExtensionReferences); + + var extensionReferences = getExtensionReferences(resource); + + return concat(basedOns, partOfs, focus, forRef, encounter, requester, owner, location, reasonReference, + insurance, relevanteHistories, restrictionRecipiets, inputReferences, inputExtensionReferences, + outputReferences, outputExtensionReferences, extensionReferences); + } + + private Stream getReferences(ValueSet resource) + { + var extensionReferences = getExtensionReferences(resource); + + return extensionReferences; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/config/ProxyConfigDelegate.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/config/ProxyConfigDelegate.java new file mode 100644 index 000000000..0c5a9392a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/config/ProxyConfigDelegate.java @@ -0,0 +1,49 @@ +package dev.dsf.bpe.v2.config; + +import java.util.List; + +public class ProxyConfigDelegate implements ProxyConfig +{ + private final dev.dsf.bpe.api.config.ProxyConfig delegate; + + public ProxyConfigDelegate(dev.dsf.bpe.api.config.ProxyConfig delegate) + { + this.delegate = delegate; + } + + @Override + public String getUrl() + { + return delegate.getUrl(); + } + + @Override + public boolean isEnabled() + { + return delegate.isEnabled(); + } + + @Override + public String getUsername() + { + return delegate.getUsername(); + } + + @Override + public char[] getPassword() + { + return delegate.getPassword(); + } + + @Override + public List getNoProxyUrls() + { + return delegate.getNoProxyUrls(); + } + + @Override + public boolean isNoProxyUrl(String url) + { + return delegate.isNoProxyUrl(url); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/AbstractListener.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/AbstractListener.java new file mode 100644 index 000000000..4bcd84e26 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/AbstractListener.java @@ -0,0 +1,77 @@ +package dev.dsf.bpe.v2.listener; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.function.Function; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.ParameterComponent; +import org.springframework.beans.factory.InitializingBean; + +public abstract class AbstractListener implements ExecutionListener, InitializingBean +{ + private final String serverBaseUrl; + private final Function variablesFactory; + + public AbstractListener(String serverBaseUrl, Function variablesFactory) + { + this.serverBaseUrl = serverBaseUrl; + this.variablesFactory = variablesFactory; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(serverBaseUrl, "serverBaseUrl"); + Objects.requireNonNull(variablesFactory, "variablesFactory"); + } + + @Override + public final void notify(DelegateExecution execution) throws Exception + { + doNotify(execution, variablesFactory.apply(execution)); + } + + protected abstract void doNotify(DelegateExecution execution, ListenerVariables variables) throws Exception; + + protected final String getLocalVersionlessAbsoluteUrl(Task task) + { + return task == null ? null + : task.getIdElement().toVersionless().withServerBase(serverBaseUrl, ResourceType.Task.name()) + .getValue(); + } + + protected final String getFirstInputParameter(Task task, Coding code) + { + if (task == null || code == null) + return null; + + return task.getInput().stream().filter(ParameterComponent::hasType) + .filter(c -> c.getType().getCoding().stream() + .anyMatch(co -> co != null && Objects.equals(code.getSystem(), co.getSystem()) + && Objects.equals(code.getCode(), co.getCode()))) + .filter(ParameterComponent::hasValue).map(ParameterComponent::getValue) + .filter(v -> v instanceof StringType).map(v -> (StringType) v).map(StringType::getValue).findFirst() + .orElse(null); + } + + protected final String getCurrentTime() + { + return ZonedDateTime.now().truncatedTo(ChronoUnit.SECONDS).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + protected final String getRequesterIdentifierValue(Task task) + { + if (task == null) + return null; + + return task.getRequester().getIdentifier().getValue(); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/ContinueListener.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/ContinueListener.java new file mode 100644 index 000000000..1e39e6798 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/ContinueListener.java @@ -0,0 +1,77 @@ +package dev.dsf.bpe.v2.listener; + +import java.util.function.Function; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.hl7.fhir.r4.model.Task; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.dsf.bpe.api.Constants; +import dev.dsf.bpe.v2.constants.CodeSystems.BpmnMessage; + +public class ContinueListener extends AbstractListener implements ExecutionListener +{ + private static final Logger logger = LoggerFactory.getLogger(ContinueListener.class); + + public ContinueListener(String serverBaseUrl, Function variablesFactory) + { + super(serverBaseUrl, variablesFactory); + } + + @Override + public void doNotify(DelegateExecution execution, ListenerVariables variables) throws Exception + { + Task task = variables.getResource(Constants.TASK_VARIABLE); + execution.removeVariable(Constants.TASK_VARIABLE); + + if (task != null) + { + variables.onContinue(task); + boolean subProcess = execution.getParentId() != null + && !execution.getParentId().equals(execution.getProcessInstanceId()); + logContinue(logger, subProcess, task, subProcess ? variables.getStartTask() : null); + } + else + logger.warn("Variable 'task' null, not updating tasks"); + } + + private void logContinue(Logger logger, boolean subProcess, Task continueTask, Task mainTask) + { + String processUrl = continueTask.getInstantiatesCanonical(); + String messageName = getFirstInputParameter(continueTask, BpmnMessage.messageName()); + String businessKey = getFirstInputParameter(continueTask, BpmnMessage.businessKey()); + String correlationKey = getFirstInputParameter(continueTask, BpmnMessage.correlationKey()); + String continueTaskUrl = getLocalVersionlessAbsoluteUrl(continueTask); + String requester = getRequesterIdentifierValue(continueTask); + + String mainTaskUrl = getLocalVersionlessAbsoluteUrl(mainTask); + + if (subProcess) + { + if (correlationKey != null) + logger.info( + "Continuing subprocess of {} at {} [task: {}, requester: {}, business-key: {}, correlation-key: {}, message: {}, main-task: {}]", + processUrl, getCurrentTime(), continueTaskUrl, requester, businessKey, correlationKey, + messageName, mainTaskUrl); + else + logger.info( + "Continuing subprocess of {} at {} [task: {}, requester: {}, business-key: {}, message: {}, main-task: {}]", + processUrl, getCurrentTime(), continueTaskUrl, requester, businessKey, messageName, + mainTaskUrl); + } + else + { + if (correlationKey != null) + logger.info( + "Continuing process {} at {} [task: {}, requester: {}, business-key: {}, correlation-key: {}, message: {}]", + processUrl, getCurrentTime(), continueTaskUrl, requester, businessKey, correlationKey, + messageName); + else + logger.info("Continuing process {} at {} [task: {}, requester: {}, business-key: {}, message: {}]", + processUrl, getCurrentTime(), continueTaskUrl, requester, businessKey, messageName); + } + + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/EndListener.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/EndListener.java new file mode 100644 index 000000000..d088bcad7 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/EndListener.java @@ -0,0 +1,119 @@ +package dev.dsf.bpe.v2.listener; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.dsf.bpe.v2.client.FhirWebserviceClient; +import dev.dsf.bpe.v2.constants.CodeSystems.BpmnMessage; + +public class EndListener extends AbstractListener implements ExecutionListener +{ + private static final Logger logger = LoggerFactory.getLogger(EndListener.class); + + private final FhirWebserviceClient webserviceClient; + + public EndListener(String serverBaseUrl, Function variablesFactory, + FhirWebserviceClient fhirWebserviceClient) + { + super(serverBaseUrl, variablesFactory); + + this.webserviceClient = fhirWebserviceClient; + } + + @Override + public void afterPropertiesSet() throws Exception + { + super.afterPropertiesSet(); + + Objects.requireNonNull(webserviceClient, "webserviceClient"); + } + + @Override + public void doNotify(DelegateExecution execution, ListenerVariables variables) throws Exception + { + List tasks = variables.getCurrentTasks(); + + for (int i = tasks.size() - 1; i >= 0; i--) + { + Task task = tasks.get(i); + updateIfInprogress(task); + boolean subProcess = execution.getParentId() != null + && !execution.getParentId().equals(execution.getProcessInstanceId()); + logEnd(subProcess, task, subProcess ? variables.getStartTask() : null); + } + + variables.onEnd(); + } + + private void updateIfInprogress(Task task) + { + if (TaskStatus.INPROGRESS.equals(task.getStatus())) + { + task.setStatus(TaskStatus.COMPLETED); + updateAndHandleException(task); + } + else + { + logger.debug("Not updating Task {} with status: {}", getLocalVersionlessAbsoluteUrl(task), + task.getStatus()); + } + } + + private void updateAndHandleException(Task task) + { + try + { + logger.debug("Updating Task {}, new status: {}", getLocalVersionlessAbsoluteUrl(task), + task.getStatus().toCode()); + + webserviceClient.withMinimalReturn().update(task); + } + catch (Exception e) + { + logger.debug("Unable to update Task {}", getLocalVersionlessAbsoluteUrl(task), e); + logger.error("Unable to update Task {}: {} - {}", getLocalVersionlessAbsoluteUrl(task), + e.getClass().getName(), e.getMessage()); + } + } + + private void logEnd(boolean subProcess, Task endTask, Task mainTask) + { + String processUrl = endTask.getInstantiatesCanonical(); + String businessKey = getFirstInputParameter(endTask, BpmnMessage.businessKey()); + String correlationKey = getFirstInputParameter(endTask, BpmnMessage.correlationKey()); + String endTaskUrl = getLocalVersionlessAbsoluteUrl(endTask); + String requester = getRequesterIdentifierValue(endTask); + + String mainTaskUrl = getLocalVersionlessAbsoluteUrl(mainTask); + + if (subProcess) + { + if (correlationKey != null) + logger.info( + "Subprocess of {} finished at {} [task: {}, requester: {}, business-key: {}, correlation-key: {}, main-task: {}]", + processUrl, getCurrentTime(), endTaskUrl, requester, businessKey, correlationKey, mainTaskUrl); + else + logger.info( + "Subprocess of {} finished at {} [task: {}, requester: {}, business-key: {}, main-task: {}]", + processUrl, getCurrentTime(), endTaskUrl, requester, businessKey, mainTaskUrl); + } + else + { + if (correlationKey != null) + logger.info( + "Process {} finished at {} [task: {}, requester: {}, business-key: {}, correlation-key: {}]", + processUrl, getCurrentTime(), endTaskUrl, requester, businessKey, correlationKey); + else + logger.info("Process {} finished at {} [task: {}, requester: {}, business-key: {}]", processUrl, + getCurrentTime(), endTaskUrl, requester, businessKey); + } + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/ListenerVariables.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/ListenerVariables.java new file mode 100644 index 000000000..956845b86 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/ListenerVariables.java @@ -0,0 +1,14 @@ +package dev.dsf.bpe.v2.listener; + +import org.hl7.fhir.r4.model.Task; + +import dev.dsf.bpe.v2.variables.Variables; + +public interface ListenerVariables extends Variables +{ + void onStart(Task task); + + void onContinue(Task task); + + void onEnd(); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/StartListener.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/StartListener.java new file mode 100644 index 000000000..c49fd677c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/listener/StartListener.java @@ -0,0 +1,55 @@ +package dev.dsf.bpe.v2.listener; + +import java.util.function.Function; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.hl7.fhir.r4.model.Task; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.dsf.bpe.api.Constants; +import dev.dsf.bpe.v2.constants.CodeSystems.BpmnMessage; + +public class StartListener extends AbstractListener implements ExecutionListener +{ + private static final Logger logger = LoggerFactory.getLogger(StartListener.class); + + public StartListener(String serverBaseUrl, Function variablesFactory) + { + super(serverBaseUrl, variablesFactory); + } + + @Override + public void doNotify(DelegateExecution execution, ListenerVariables variables) throws Exception + { + Task task = variables.getResource(Constants.TASK_VARIABLE); + execution.removeVariable(Constants.TASK_VARIABLE); + + if (task != null) + { + variables.onStart(task); + logStart(logger, task); + } + else + logger.warn("Variable 'task' null, not updating tasks"); + } + + private void logStart(Logger logger, Task task) + { + String processUrl = task.getInstantiatesCanonical(); + String messageName = getFirstInputParameter(task, BpmnMessage.messageName()); + String businessKey = getFirstInputParameter(task, BpmnMessage.businessKey()); + String correlationKey = getFirstInputParameter(task, BpmnMessage.correlationKey()); + String taskUrl = getLocalVersionlessAbsoluteUrl(task); + String requester = getRequesterIdentifierValue(task); + + if (correlationKey != null) + logger.info( + "Starting process {} at {} [task: {}, requester: {}, business-key: {}, correlation-key: {}, message: {}]", + processUrl, getCurrentTime(), taskUrl, requester, businessKey, correlationKey, messageName); + else + logger.info("Starting process {} at {} [task: {}, requester: {}, business-key: {}, message: {}]", + processUrl, getCurrentTime(), taskUrl, requester, businessKey, messageName); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ApiServicesSpringConfiguration.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ApiServicesSpringConfiguration.java new file mode 100644 index 000000000..315d4b0d5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ApiServicesSpringConfiguration.java @@ -0,0 +1,95 @@ +package dev.dsf.bpe.v2.plugin; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import ca.uhn.fhir.context.FhirContext; +import dev.dsf.bpe.v2.ProcessPluginApi; +import dev.dsf.bpe.v2.activity.DefaultUserTaskListener; +import dev.dsf.bpe.v2.service.EndpointProvider; +import dev.dsf.bpe.v2.service.FhirWebserviceClientProvider; +import dev.dsf.bpe.v2.service.MailService; +import dev.dsf.bpe.v2.service.OrganizationProvider; +import dev.dsf.bpe.v2.service.QuestionnaireResponseHelper; +import dev.dsf.bpe.v2.service.ReadAccessHelper; +import dev.dsf.bpe.v2.service.TaskHelper; +import dev.dsf.bpe.v2.service.process.ProcessAuthorizationHelper; + +@Configuration +public class ApiServicesSpringConfiguration +{ + @Autowired + private ProcessPluginApi api; + + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public DefaultUserTaskListener defaultUserTaskListener() + { + return new DefaultUserTaskListener(api); + } + + @Bean + public EndpointProvider getEndpointProvider() + { + return api.getEndpointProvider(); + } + + @Bean + public FhirContext getFhirContext() + { + return api.getFhirContext(); + } + + @Bean + public FhirWebserviceClientProvider getFhirWebserviceClientProvider() + { + return api.getFhirWebserviceClientProvider(); + } + + @Bean + public MailService getMailService() + { + return api.getMailService(); + } + + @Bean + public ObjectMapper getObjectMapper() + { + return api.getObjectMapper(); + } + + @Bean + public OrganizationProvider getOrganizationProvider() + { + return api.getOrganizationProvider(); + } + + @Bean + public ProcessAuthorizationHelper getProcessAuthorizationHelper() + { + return api.getProcessAuthorizationHelper(); + } + + @Bean + public QuestionnaireResponseHelper getQuestionnaireResponseHelper() + { + return api.getQuestionnaireResponseHelper(); + } + + @Bean + public ReadAccessHelper getReadAccessHelper() + { + return api.getReadAccessHelper(); + } + + @Bean + public TaskHelper getTaskHelper() + { + return api.getTaskHelper(); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginApiBuilderImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginApiBuilderImpl.java new file mode 100644 index 000000000..664460889 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginApiBuilderImpl.java @@ -0,0 +1,24 @@ +package dev.dsf.bpe.v2.plugin; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder; +import dev.dsf.bpe.api.plugin.ProcessPluginFactory; +import dev.dsf.bpe.v2.spring.ApiServiceConfig; + +public class ProcessPluginApiBuilderImpl implements ProcessPluginApiBuilder +{ + @Override + public ProcessPluginFactory build(ClassLoader apiClassLoader, ApplicationContext apiApplicationContext, + ConfigurableEnvironment environment) + { + return new ProcessPluginFactoryImpl(apiClassLoader, apiApplicationContext, environment); + } + + @Override + public Class getSpringServiceConfigClass() + { + return ApiServiceConfig.class; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginFactoryImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginFactoryImpl.java new file mode 100644 index 000000000..1db60db7a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginFactoryImpl.java @@ -0,0 +1,49 @@ +package dev.dsf.bpe.v2.plugin; + +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.camunda.bpm.engine.impl.variable.serializer.TypedValueSerializer; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import dev.dsf.bpe.api.listener.ListenerFactory; +import dev.dsf.bpe.api.plugin.AbstractProcessPluginFactory; +import dev.dsf.bpe.api.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessPluginFactory; +import dev.dsf.bpe.v2.ProcessPluginDefinition; +import dev.dsf.bpe.v2.activity.DefaultUserTaskListener; + +public class ProcessPluginFactoryImpl extends AbstractProcessPluginFactory implements ProcessPluginFactory +{ + public static final int API_VERSION = 2; + + public ProcessPluginFactoryImpl(ClassLoader apiClassLoader, ApplicationContext apiApplicationContext, + ConfigurableEnvironment environment) + { + super(API_VERSION, apiClassLoader, apiApplicationContext, environment, ProcessPluginDefinition.class, + DefaultUserTaskListener.class); + } + + @Override + protected ProcessPlugin createProcessPlugin(Object processPluginDefinition, boolean draft, Path jarFile, + URLClassLoader classLoader) + { + return new ProcessPluginImpl((ProcessPluginDefinition) processPluginDefinition, API_VERSION, draft, jarFile, + classLoader, environment, apiApplicationContext); + } + + @Override + @SuppressWarnings("rawtypes") + public Stream getSerializer() + { + return apiApplicationContext.getBeansOfType(TypedValueSerializer.class).values().stream(); + } + + @Override + public ListenerFactory getListenerFactory() + { + return apiApplicationContext.getBean(ListenerFactory.class); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginImpl.java new file mode 100644 index 000000000..e193b232a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/plugin/ProcessPluginImpl.java @@ -0,0 +1,258 @@ +package dev.dsf.bpe.v2.plugin; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.camunda.bpm.engine.variable.value.PrimitiveValue; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.ActivityDefinition; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.MetadataResource; +import org.hl7.fhir.r4.model.NamingSystem; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskStatus; +import org.hl7.fhir.r4.model.ValueSet; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import dev.dsf.bpe.api.plugin.AbstractProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessPluginFhirConfig; +import dev.dsf.bpe.v2.ProcessPluginApi; +import dev.dsf.bpe.v2.ProcessPluginDefinition; +import dev.dsf.bpe.v2.ProcessPluginDeploymentListener; +import dev.dsf.bpe.v2.constants.CodeSystems; +import dev.dsf.bpe.v2.constants.NamingSystems.OrganizationIdentifier; +import dev.dsf.bpe.v2.constants.NamingSystems.TaskIdentifier; +import dev.dsf.bpe.v2.variables.FhirResourceValues; + +public class ProcessPluginImpl extends AbstractProcessPlugin implements ProcessPlugin +{ + private final ProcessPluginDefinition processPluginDefinition; + private final ProcessPluginApi processPluginApi; + + public ProcessPluginImpl(ProcessPluginDefinition processPluginDefinition, int processPluginApiVersion, + boolean draft, Path jarFile, ClassLoader classLoader, ConfigurableEnvironment environment, + ApplicationContext apiApplicationContext) + { + super(ProcessPluginDefinition.class, processPluginApiVersion, draft, jarFile, classLoader, environment, + apiApplicationContext, ApiServicesSpringConfiguration.class); + + this.processPluginDefinition = processPluginDefinition; + processPluginApi = apiApplicationContext.getBean(ProcessPluginApi.class); + } + + @Override + protected ProcessPluginFhirConfig createFhirConfig() + { + BiFunction parseResource = (String filename, String content) -> + { + if (filename.endsWith(JSON_SUFFIX)) + return newJsonParser().parseResource(content); + else if (filename.endsWith(XML_SUFFIX)) + return newXmlParser().parseResource(content); + else + throw new IllegalArgumentException("FHIR resource filename not ending in .json or .xml"); + }; + + Function encodeResource = resource -> + { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = new OutputStreamWriter(out, StandardCharsets.UTF_8)) + { + newJsonParser().encodeResourceToWriter((IBaseResource) resource, w); + return out.toByteArray(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + }; + + Function> getResourceName = resource -> Optional + .ofNullable(resource instanceof Resource r ? r.getResourceType().name() : null); + + Predicate hasMetadataResourceUrl = resource -> resource instanceof MetadataResource m && m.hasUrl(); + Predicate hasMetadataResourceVersion = resource -> resource instanceof MetadataResource m + && m.hasVersion(); + + Function> getMetadataResourceVersion = resource -> Optional + .ofNullable(resource instanceof MetadataResource m ? m.getVersion() : null); + + Function> getActivityDefinitionUrl = a -> Optional + .ofNullable(a.hasUrlElement() && a.getUrlElement().hasValue() ? a.getUrlElement().getValue() : null); + + Function> getTaskInstantiatesCanonical = resource -> Optional + .ofNullable(resource instanceof Task t && t.hasInstantiatesCanonicalElement() + && t.getInstantiatesCanonicalElement().hasValue() + ? t.getInstantiatesCanonicalElement().getValue() + : null); + + Function> getTaskIdentifierValue = t -> TaskIdentifier + .findFirst(t) + .map(i -> new ProcessPluginFhirConfig.Identifier( + i.hasSystem() ? Optional.of(i.getSystem()) : Optional.empty(), + i.hasValue() ? Optional.of(i.getValue()) : Optional.empty())); + + Predicate isTaskStatusDraft = t -> t.hasStatusElement() && t.getStatusElement().hasValue() + && TaskStatus.DRAFT.equals(t.getStatus()); + + Function> getRequester = t -> t.hasRequester() + ? Optional.ofNullable(t.getRequester()).map(r -> + { + Identifier i = r.getIdentifier(); + return new ProcessPluginFhirConfig.Reference( + Optional.ofNullable(i.getSystemElement()).filter(e -> e.hasValue()).map(e -> e.getValue()), + Optional.ofNullable(i.getValueElement()).filter(e -> e.hasValue()).map(e -> e.getValue()), + Optional.ofNullable(r.getTypeElement()).filter(e -> e.hasValue()).map(e -> e.getValue())); + }) + : Optional.empty(); + + Function> getRecipient = t -> t.hasRestriction() + && t.getRestriction().hasRecipient() && t.getRestriction().getRecipient().size() == 1 + ? Optional.ofNullable(t.getRestriction().getRecipientFirstRep()).map(r -> + { + Identifier i = r.getIdentifier(); + return new ProcessPluginFhirConfig.Reference( + Optional.ofNullable(i.getSystemElement()).filter(e -> e.hasValue()) + .map(e -> e.getValue()), + Optional.ofNullable(i.getValueElement()).filter(e -> e.hasValue()) + .map(e -> e.getValue()), + Optional.ofNullable(r.getTypeElement()).filter(e -> e.hasValue()) + .map(e -> e.getValue())); + }) + : Optional.empty(); + + Predicate hasTaskInputMessageName = t -> t + .getInput().stream().filter( + i -> i.getType().getCoding().stream() + .anyMatch(c -> CodeSystems.BpmnMessage.URL.equals(c.getSystem()) + && CodeSystems.BpmnMessage.Codes.MESSAGE_NAME.equals(c.getCode()))) + .count() == 1; + + return new ProcessPluginFhirConfig<>(ActivityDefinition.class, CodeSystem.class, Library.class, Measure.class, + NamingSystem.class, Questionnaire.class, StructureDefinition.class, Task.class, ValueSet.class, + OrganizationIdentifier.SID, TaskIdentifier.SID, TaskStatus.DRAFT.toCode(), CodeSystems.BpmnMessage.URL, + CodeSystems.BpmnMessage.Codes.MESSAGE_NAME, parseResource, encodeResource, getResourceName, + hasMetadataResourceUrl, hasMetadataResourceVersion, getMetadataResourceVersion, + getActivityDefinitionUrl, NamingSystem::hasName, getTaskInstantiatesCanonical, getTaskIdentifierValue, + isTaskStatusDraft, getRequester, getRecipient, Task::hasInput, hasTaskInputMessageName, + Task::hasOutput); + } + + private IParser newXmlParser() + { + return newParser(FhirContext::newXmlParser); + } + + private IParser newJsonParser() + { + return newParser(FhirContext::newJsonParser); + } + + private IParser newParser(Function parserFactor) + { + IParser p = parserFactor.apply(processPluginApi.getFhirContext()); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + + return p; + } + + @Override + protected List> getDefinitionSpringConfigurations() + { + return processPluginDefinition.getSpringConfigurations(); + } + + @Override + protected String getDefinitionName() + { + return processPluginDefinition.getName(); + } + + @Override + protected String getDefinitionVersion() + { + return processPluginDefinition.getVersion(); + } + + @Override + protected String getDefinitionResourceVersion() + { + return processPluginDefinition.getResourceVersion(); + } + + @Override + protected LocalDate getDefinitionReleaseDate() + { + return processPluginDefinition.getReleaseDate(); + } + + @Override + protected LocalDate getDefinitionResourceReleaseDate() + { + return processPluginDefinition.getResourceReleaseDate(); + } + + @Override + protected Map> getDefinitionFhirResourcesByProcessId() + { + return processPluginDefinition.getFhirResourcesByProcessId(); + } + + @Override + protected List getDefinitionProcessModels() + { + return processPluginDefinition.getProcessModels(); + } + + @Override + public PrimitiveValue createFhirTaskVariable(String taskJson) + { + Task task = newJsonParser().parseResource(Task.class, taskJson); + return FhirResourceValues.create(task); + } + + @Override + public PrimitiveValue createFhirQuestionnaireResponseVariable(String questionnaireResponseJson) + { + QuestionnaireResponse questionnaireResponse = newJsonParser().parseResource(QuestionnaireResponse.class, + questionnaireResponseJson); + return FhirResourceValues.create(questionnaireResponse); + } + + @Override + public dev.dsf.bpe.api.plugin.ProcessPluginDeploymentListener getProcessPluginDeploymentListener() + { + return allActiveProcesses -> + { + List activePluginProcesses = getActivePluginProcesses(allActiveProcesses); + + getApplicationContext().getBeansOfType(ProcessPluginDeploymentListener.class).values().stream() + .forEach(l -> handleProcessPluginDeploymentStateListenerError( + () -> l.onProcessesDeployed(activePluginProcesses), ProcessPluginDeploymentListener.class, + l.getClass())); + + }; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/AbstractResourceProvider.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/AbstractResourceProvider.java new file mode 100644 index 000000000..8817b17f5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/AbstractResourceProvider.java @@ -0,0 +1,81 @@ +package dev.dsf.bpe.v2.service; + +import static org.hl7.fhir.instance.model.api.IBaseBundle.LINK_NEXT; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.SearchEntryMode; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Resource; +import org.springframework.beans.factory.InitializingBean; + +public abstract class AbstractResourceProvider implements InitializingBean +{ + protected final FhirWebserviceClientProvider clientProvider; + protected final String localEndpointAddress; + + public AbstractResourceProvider(FhirWebserviceClientProvider clientProvider, String localEndpointAddress) + { + this.clientProvider = clientProvider; + this.localEndpointAddress = localEndpointAddress; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(clientProvider, "clientProvider"); + Objects.requireNonNull(localEndpointAddress, "localEndpointAddress"); + } + + protected final String toSearchParameter(Identifier identifier) + { + return (identifier.hasSystem() ? identifier.getSystem() + "|" : "") + identifier.getValue(); + } + + protected final String toSearchParameter(Coding coding) + { + return (coding.hasSystem() ? coding.getSystem() + "|" : "") + coding.getCode(); + } + + protected final List search(Class searchType, + Map> searchParameters, SearchEntryMode targetMode, Class targetType, + Predicate filter) + { + List organizations = new ArrayList<>(); + + boolean hasMore = true; + int page = 1; + while (hasMore) + { + Bundle resultBundle = search(searchType, searchParameters, page++); + + organizations.addAll(resultBundle.getEntry().stream().filter(BundleEntryComponent::hasSearch) + .filter(e -> targetMode.equals(e.getSearch().getMode())).filter(BundleEntryComponent::hasResource) + .map(BundleEntryComponent::getResource).filter(targetType::isInstance).map(targetType::cast) + .filter(filter).toList()); + + hasMore = resultBundle.getLink(LINK_NEXT) != null; + } + + return organizations; + } + + private Bundle search(Class searchType, Map> parameters, int page) + { + Map> parametersAndPage = new HashMap<>(parameters); + parametersAndPage.put("_page", Collections.singletonList(String.valueOf(page))); + if (!parameters.containsKey("_sort")) + parametersAndPage.put("_sort", Collections.singletonList("_id")); + + return clientProvider.getLocalWebserviceClient().searchWithStrictHandling(searchType, parametersAndPage); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/EndpointProviderImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/EndpointProviderImpl.java new file mode 100644 index 000000000..dfcee39c5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/EndpointProviderImpl.java @@ -0,0 +1,164 @@ +package dev.dsf.bpe.v2.service; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.SearchEntryMode; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.Endpoint.EndpointStatus; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.OrganizationAffiliation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EndpointProviderImpl extends AbstractResourceProvider implements EndpointProvider +{ + private static final Logger logger = LoggerFactory.getLogger(EndpointProviderImpl.class); + + public EndpointProviderImpl(FhirWebserviceClientProvider clientProvider, String localEndpointAddress) + { + super(clientProvider, localEndpointAddress); + } + + @Override + public Optional getLocalEndpoint() + { + Bundle resultBundle = clientProvider.getLocalWebserviceClient().searchWithStrictHandling(Endpoint.class, + Map.of("status", Collections.singletonList("active"), "address", + Collections.singletonList(localEndpointAddress))); + + if (resultBundle == null || resultBundle.getEntry() == null || resultBundle.getEntry().size() != 1 + || resultBundle.getEntryFirstRep().getResource() == null + || !(resultBundle.getEntryFirstRep().getResource() instanceof Endpoint)) + { + logger.warn("No active (or more than one) Endpoint found with address '{}'", localEndpointAddress); + return Optional.empty(); + } + + return Optional.of((Endpoint) resultBundle.getEntryFirstRep().getResource()); + } + + @Override + public String getLocalEndpointAddress() + { + return localEndpointAddress; + } + + @Override + public Optional getEndpoint(Identifier endpointIdentifier) + { + if (endpointIdentifier == null) + { + logger.debug("Endpoint identifier is null"); + return Optional.empty(); + } + + String endpointIdSp = toSearchParameter(endpointIdentifier); + + Bundle resultBundle = clientProvider.getLocalWebserviceClient().searchWithStrictHandling(Endpoint.class, Map.of( + "status", Collections.singletonList("active"), "identifier", Collections.singletonList(endpointIdSp))); + + if (resultBundle == null || resultBundle.getEntry() == null || resultBundle.getTotal() != 1 + || resultBundle.getEntryFirstRep().getResource() == null + || !(resultBundle.getEntryFirstRep().getResource() instanceof Endpoint)) + { + logger.warn("No active (or more than one) Endpoint found with identifier '{}'", endpointIdSp); + return Optional.empty(); + } + + return Optional.of((Endpoint) resultBundle.getEntryFirstRep().getResource()); + } + + @Override + public Optional getEndpoint(Identifier parentOrganizationIdentifier, + Identifier memberOrganizationIdentifier, Coding memberOrganizationRole) + { + if (parentOrganizationIdentifier == null) + { + logger.debug("Parent organiztion identifier is null"); + return Optional.empty(); + } + else if (memberOrganizationIdentifier == null) + { + logger.debug("Member organiztion identifier is null"); + return Optional.empty(); + } + else if (memberOrganizationRole == null) + { + logger.debug("Member organiztion role is null"); + return Optional.empty(); + } + + String parentOrganizationIdSp = toSearchParameter(parentOrganizationIdentifier); + String memberOrganizationIdSp = toSearchParameter(memberOrganizationIdentifier); + String memberOrganizationRoleSp = toSearchParameter(memberOrganizationRole); + + Bundle resultBundle = clientProvider.getLocalWebserviceClient().searchWithStrictHandling( + OrganizationAffiliation.class, + Map.of("active", Collections.singletonList("true"), "primary-organization:identifier", + Collections.singletonList(parentOrganizationIdSp), "participating-organization:identifier", + Collections.singletonList(memberOrganizationIdSp), "role", + Collections.singletonList(memberOrganizationRoleSp), "_include", + Collections.singletonList("OrganizationAffiliation:endpoint"))); + + if (resultBundle == null || resultBundle.getEntry() == null || resultBundle.getTotal() != 1 + || resultBundle.getEntryFirstRep().getResource() == null + || !(resultBundle.getEntryFirstRep().getResource() instanceof OrganizationAffiliation)) + { + logger.warn( + "No active (or more than one) OrganizationAffiliation found with primary-organization identifier '{}', participating-organization identifier '{}' and role '{}'", + parentOrganizationIdSp, memberOrganizationIdSp, memberOrganizationRoleSp); + return Optional.empty(); + } + else if (getActiveEndpointFromInclude(resultBundle).count() != 1) + { + logger.warn( + "No active Endpoint found for active OrganizationAffiliation with primary-organization identifier '{}', participating-organization identifier '{}' and role '{}'", + parentOrganizationIdSp, memberOrganizationIdSp, memberOrganizationRoleSp); + return Optional.empty(); + } + + return getActiveEndpointFromInclude(resultBundle).findFirst(); + } + + private Stream getActiveEndpointFromInclude(Bundle resultBundle) + { + return resultBundle.getEntry().stream().filter(BundleEntryComponent::hasSearch) + .filter(e -> SearchEntryMode.INCLUDE.equals(e.getSearch().getMode())) + .filter(BundleEntryComponent::hasResource).map(BundleEntryComponent::getResource) + .filter(r -> r instanceof Endpoint).map(r -> (Endpoint) r) + .filter(e -> EndpointStatus.ACTIVE.equals(e.getStatus())); + } + + @Override + public List getEndpoints(Identifier parentOrganizationIdentifier, Coding memberOrganizationRole) + { + if (parentOrganizationIdentifier == null) + { + logger.debug("Parent organiztion identifier is null"); + return Collections.emptyList(); + } + else if (memberOrganizationRole == null) + { + logger.debug("Member organiztion role is null"); + return Collections.emptyList(); + } + + String parentOrganizationIdSp = toSearchParameter(parentOrganizationIdentifier); + String memberOrganizationRoleSp = toSearchParameter(memberOrganizationRole); + + Map> parameters = Map.of("active", Collections.singletonList("true"), + "primary-organization:identifier", Collections.singletonList(parentOrganizationIdSp), "role", + Collections.singletonList(memberOrganizationRoleSp), "_include", + Collections.singletonList("OrganizationAffiliation:endpoint")); + + return search(OrganizationAffiliation.class, parameters, SearchEntryMode.INCLUDE, Endpoint.class, + e -> EndpointStatus.ACTIVE.equals(e.getStatus())); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/FhirWebserviceClientProviderImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/FhirWebserviceClientProviderImpl.java new file mode 100644 index 000000000..7a942ef48 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/FhirWebserviceClientProviderImpl.java @@ -0,0 +1,141 @@ +package dev.dsf.bpe.v2.service; + +import java.security.KeyStore; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.springframework.beans.factory.InitializingBean; + +import ca.uhn.fhir.context.FhirContext; +import dev.dsf.bpe.api.config.ProxyConfig; +import dev.dsf.bpe.api.service.BuildInfoProvider; +import dev.dsf.bpe.v2.client.FhirWebserviceClient; +import dev.dsf.bpe.v2.client.FhirWebserviceClientJersey; +import dev.dsf.bpe.v2.client.ReferenceCleaner; + +public class FhirWebserviceClientProviderImpl implements FhirWebserviceClientProvider, InitializingBean +{ + private static final String USER_AGENT_VALUE = "DSF/"; + + private final Map webserviceClientsByUrl = new HashMap<>(); + + private final FhirContext fhirContext; + + private final String localWebserviceBaseUrl; + private final int localWebserviceReadTimeout; + private final int localWebserviceConnectTimeout; + private final boolean localWebserviceLogRequests; + + private final KeyStore webserviceTrustStore; + private final KeyStore webserviceKeyStore; + private final char[] webserviceKeyStorePassword; + + private final int remoteWebserviceReadTimeout; + private final int remoteWebserviceConnectTimeout; + private final boolean remoteWebserviceLogRequests; + + private final ProxyConfig proxyConfig; + private final BuildInfoProvider buildInfoProvider; + + private final ReferenceCleaner referenceCleaner; + + public FhirWebserviceClientProviderImpl(FhirContext fhirContext, String localWebserviceBaseUrl, + int localWebserviceReadTimeout, int localWebserviceConnectTimeout, boolean localWebserviceLogRequests, + KeyStore webserviceTrustStore, KeyStore webserviceKeyStore, char[] webserviceKeyStorePassword, + int remoteWebserviceReadTimeout, int remoteWebserviceConnectTimeout, boolean remoteWebserviceLogRequests, + ProxyConfig proxyConfig, BuildInfoProvider buildInfoProvider, ReferenceCleaner referenceCleaner) + { + this.fhirContext = fhirContext; + + this.localWebserviceBaseUrl = localWebserviceBaseUrl; + this.localWebserviceReadTimeout = localWebserviceReadTimeout; + this.localWebserviceConnectTimeout = localWebserviceConnectTimeout; + this.localWebserviceLogRequests = localWebserviceLogRequests; + + this.webserviceTrustStore = webserviceTrustStore; + this.webserviceKeyStore = webserviceKeyStore; + this.webserviceKeyStorePassword = webserviceKeyStorePassword; + + this.remoteWebserviceReadTimeout = remoteWebserviceReadTimeout; + this.remoteWebserviceConnectTimeout = remoteWebserviceConnectTimeout; + this.remoteWebserviceLogRequests = remoteWebserviceLogRequests; + + this.proxyConfig = proxyConfig; + this.buildInfoProvider = buildInfoProvider; + this.referenceCleaner = referenceCleaner; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(fhirContext, "fhirContext"); + Objects.requireNonNull(localWebserviceBaseUrl, "localBaseUrl"); + if (localWebserviceReadTimeout < 0) + throw new IllegalArgumentException("localReadTimeout < 0"); + if (localWebserviceConnectTimeout < 0) + throw new IllegalArgumentException("localConnectTimeout < 0"); + Objects.requireNonNull(webserviceTrustStore, "webserviceTrustStore"); + Objects.requireNonNull(webserviceKeyStore, "webserviceKeyStore"); + Objects.requireNonNull(webserviceKeyStorePassword, "webserviceKeyStorePassword"); + if (remoteWebserviceReadTimeout < 0) + throw new IllegalArgumentException("remoteReadTimeout < 0"); + if (remoteWebserviceConnectTimeout < 0) + throw new IllegalArgumentException("remoteConnectTimeout < 0"); + + Objects.requireNonNull(proxyConfig, "proxyConfig"); + Objects.requireNonNull(buildInfoProvider, "buildInfoProvider"); + } + + private FhirWebserviceClient getClient(String webserviceUrl) + { + synchronized (webserviceClientsByUrl) + { + if (webserviceClientsByUrl.containsKey(webserviceUrl)) + return webserviceClientsByUrl.get(webserviceUrl); + else + { + String proxyUrl = proxyConfig.isEnabled(webserviceUrl) ? proxyConfig.getUrl() : null; + String proxyUsername = proxyConfig.isEnabled(webserviceUrl) ? proxyConfig.getUsername() : null; + char[] proxyPassword = proxyConfig.isEnabled(webserviceUrl) ? proxyConfig.getPassword() : null; + + FhirWebserviceClient client; + if (localWebserviceBaseUrl.equals(webserviceUrl)) + client = new FhirWebserviceClientJersey(webserviceUrl, webserviceTrustStore, webserviceKeyStore, + webserviceKeyStorePassword, null, proxyUrl, proxyUsername, proxyPassword, + localWebserviceConnectTimeout, localWebserviceReadTimeout, localWebserviceLogRequests, + USER_AGENT_VALUE + buildInfoProvider.getProjectVersion(), fhirContext, referenceCleaner); + else + client = new FhirWebserviceClientJersey(webserviceUrl, webserviceTrustStore, webserviceKeyStore, + webserviceKeyStorePassword, null, proxyUrl, proxyUsername, proxyPassword, + remoteWebserviceConnectTimeout, remoteWebserviceReadTimeout, remoteWebserviceLogRequests, + USER_AGENT_VALUE + buildInfoProvider.getProjectVersion(), fhirContext, referenceCleaner); + + webserviceClientsByUrl.put(webserviceUrl, client); + return client; + } + } + } + + @Override + public FhirWebserviceClient getLocalWebserviceClient() + { + return getWebserviceClient(localWebserviceBaseUrl); + } + + @Override + public FhirWebserviceClient getWebserviceClient(String webserviceUrl) + { + Objects.requireNonNull(webserviceUrl, "webserviceUrl"); + + FhirWebserviceClient cachedClient = webserviceClientsByUrl.get(webserviceUrl); + if (cachedClient != null) + return cachedClient; + else + { + FhirWebserviceClient newClient = getClient(webserviceUrl); + webserviceClientsByUrl.put(webserviceUrl, newClient); + return newClient; + } + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/MailServiceImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/MailServiceImpl.java new file mode 100644 index 000000000..f12c9ade7 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/MailServiceImpl.java @@ -0,0 +1,33 @@ +package dev.dsf.bpe.v2.service; + +import java.util.Objects; +import java.util.function.Consumer; + +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.springframework.beans.factory.InitializingBean; + +import dev.dsf.bpe.api.service.BpeMailService; + +public class MailServiceImpl implements MailService, InitializingBean +{ + private final BpeMailService delegate; + + public MailServiceImpl(BpeMailService delegate) + { + this.delegate = delegate; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(delegate, "delegate"); + } + + @Override + public void send(String subject, MimeBodyPart body, Consumer messageModifier) + { + delegate.send(subject, body, messageModifier); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/OrganizationProviderImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/OrganizationProviderImpl.java new file mode 100644 index 000000000..a077b97e5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/OrganizationProviderImpl.java @@ -0,0 +1,150 @@ +package dev.dsf.bpe.v2.service; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.SearchEntryMode; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class OrganizationProviderImpl extends AbstractResourceProvider implements OrganizationProvider +{ + private static final Logger logger = LoggerFactory.getLogger(OrganizationProviderImpl.class); + + public OrganizationProviderImpl(FhirWebserviceClientProvider clientProvider, String localEndpointAddress) + { + super(clientProvider, localEndpointAddress); + } + + @Override + public Optional getLocalOrganization() + { + Bundle resultBundle = clientProvider.getLocalWebserviceClient().searchWithStrictHandling(Endpoint.class, + Map.of("status", Collections.singletonList("active"), "address", + Collections.singletonList(localEndpointAddress), "_include", + Collections.singletonList("Endpoint:organization"))); + + if (resultBundle == null || resultBundle.getEntry() == null || resultBundle.getEntry().size() != 2 + || resultBundle.getEntry().get(0).getResource() == null + || !(resultBundle.getEntry().get(0).getResource() instanceof Endpoint) + || resultBundle.getEntry().get(1).getResource() == null + || !(resultBundle.getEntry().get(1).getResource() instanceof Organization)) + { + logger.warn("No active (or more than one) Endpoint found for address '{}'", localEndpointAddress); + return Optional.empty(); + } + else if (getActiveOrganizationFromIncludes(resultBundle).count() != 1) + { + logger.warn("No active (or more than one) Organization found by active Endpoint with address '{}'", + localEndpointAddress); + return Optional.empty(); + } + + return getActiveOrganizationFromIncludes(resultBundle).findFirst(); + } + + private Stream getActiveOrganizationFromIncludes(Bundle resultBundle) + { + return resultBundle.getEntry().stream().filter(BundleEntryComponent::hasSearch) + .filter(e -> SearchEntryMode.INCLUDE.equals(e.getSearch().getMode())) + .filter(BundleEntryComponent::hasResource).map(BundleEntryComponent::getResource) + .filter(r -> r instanceof Organization).map(r -> (Organization) r).filter(Organization::getActive); + } + + @Override + public Optional getOrganization(Identifier organizationIdentifier) + { + if (organizationIdentifier == null) + { + logger.debug("Organization identifier is null"); + return Optional.empty(); + } + + String organizationIdSp = toSearchParameter(organizationIdentifier); + + Bundle resultBundle = clientProvider.getLocalWebserviceClient().searchWithStrictHandling(Organization.class, + Map.of("active", Collections.singletonList("true"), "identifier", + Collections.singletonList(organizationIdSp))); + + if (resultBundle == null || resultBundle.getEntry() == null || resultBundle.getTotal() != 1 + || resultBundle.getEntryFirstRep().getResource() == null + || !(resultBundle.getEntryFirstRep().getResource() instanceof Organization)) + { + logger.warn("No active (or more than one) Organization found for identifier '{}'", organizationIdSp); + return Optional.empty(); + } + + return Optional.of((Organization) resultBundle.getEntryFirstRep().getResource()); + } + + @Override + public List getOrganizations(Identifier parentOrganizationIdentifier) + { + if (parentOrganizationIdentifier == null) + { + logger.debug("Parent organiztion identifier is null"); + return Collections.emptyList(); + } + + String parentOrganizationIdSp = toSearchParameter(parentOrganizationIdentifier); + + Map> parameters = Map.of("active", Collections.singletonList("true"), + "primary-organization:identifier", Collections.singletonList(parentOrganizationIdSp), "_include", + Collections.singletonList("OrganizationAffiliation:participating-organization")); + + return search(OrganizationAffiliation.class, parameters, SearchEntryMode.INCLUDE, Organization.class, + Organization::getActive); + } + + @Override + public List getOrganizations(Identifier parentOrganizationIdentifier, Coding memberOrganizationRole) + { + if (parentOrganizationIdentifier == null) + { + logger.debug("Parent organiztion identifier is null"); + return Collections.emptyList(); + } + else if (memberOrganizationRole == null) + { + logger.debug("Member organiztion role is null"); + return Collections.emptyList(); + } + + String parentOrganizationIdSp = toSearchParameter(parentOrganizationIdentifier); + String memberOrganizationRoleSp = toSearchParameter(memberOrganizationRole); + + Map> parameters = Map.of("active", Collections.singletonList("true"), + "primary-organization:identifier", Collections.singletonList(parentOrganizationIdSp), "role", + Collections.singletonList(memberOrganizationRoleSp), "_include", + Collections.singletonList("OrganizationAffiliation:participating-organization")); + + return search(OrganizationAffiliation.class, parameters, SearchEntryMode.INCLUDE, Organization.class, + Organization::getActive); + } + + @Override + public List getRemoteOrganizations() + { + Optional localOrganizationIdentifier = getLocalOrganizationIdentifier(); + + if (localOrganizationIdentifier.isEmpty()) + { + logger.debug("Local organiztion identifier unknown"); + return Collections.emptyList(); + } + + Map> searchParameters = Map.of("active", Collections.singletonList("true"), + "identifier:not", Collections.singletonList(toSearchParameter(localOrganizationIdentifier.get()))); + return search(Organization.class, searchParameters, SearchEntryMode.MATCH, Organization.class, o -> true); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/QuestionnaireResponseHelperImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/QuestionnaireResponseHelperImpl.java new file mode 100644 index 000000000..6b1e6e94c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/QuestionnaireResponseHelperImpl.java @@ -0,0 +1,105 @@ +package dev.dsf.bpe.v2.service; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.TimeType; +import org.hl7.fhir.r4.model.Type; +import org.hl7.fhir.r4.model.UriType; + +public class QuestionnaireResponseHelperImpl implements QuestionnaireResponseHelper +{ + private final String serverBaseUrl; + + /** + * @param serverBaseUrl + * not null + */ + public QuestionnaireResponseHelperImpl(String serverBaseUrl) + { + this.serverBaseUrl = serverBaseUrl; + } + + @Override + public Stream getItemLeavesMatchingLinkIdAsStream( + QuestionnaireResponse questionnaireResponse, String linkId) + { + return getItemLeavesAsStream(questionnaireResponse).filter(i -> linkId.equals(i.getLinkId())); + } + + @Override + public Stream getItemLeavesAsStream( + QuestionnaireResponse questionnaireResponse) + { + return flatItems(questionnaireResponse.getItem()); + } + + private Stream flatItems( + List toFlat) + { + return toFlat.stream().flatMap(this::leaves); + } + + private Stream leaves( + QuestionnaireResponse.QuestionnaireResponseItemComponent component) + { + if (component.getItem().size() > 0) + return component.getItem().stream().flatMap(this::leaves); + else + return Stream.of(component); + } + + @Override + public void addItemLeafWithAnswer(QuestionnaireResponse questionnaireResponse, String linkId, String text, + Type answer) + { + List answerComponent = Collections + .singletonList(new QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(answer)); + + questionnaireResponse.addItem().setLinkId(linkId).setText(text).setAnswer(answerComponent); + } + + @Override + public void addItemLeafWithoutAnswer(QuestionnaireResponse questionnaireResponse, String linkId, String text) + { + questionnaireResponse.addItem().setLinkId(linkId).setText(text); + } + + @Override + public Type transformQuestionTypeToAnswerType(Questionnaire.QuestionnaireItemComponent question) + { + return switch (question.getType()) + { + case STRING, TEXT -> new StringType("Placeholder.."); + case INTEGER -> new IntegerType(0); + case DECIMAL -> new DecimalType(0.00); + case BOOLEAN -> new BooleanType(false); + case DATE -> new DateType("1900-01-01"); + case TIME -> new TimeType("00:00:00"); + case DATETIME -> new DateTimeType("1900-01-01T00:00:00.000Z"); + case URL -> new UriType("http://example.org/foo"); + case REFERENCE -> new Reference("http://example.org/fhir/Placeholder/id"); + + default -> throw new RuntimeException("Type '" + question.getType().getDisplay() + + "' in Questionnaire.item is not supported as answer type"); + }; + } + + @Override + public String getLocalVersionlessAbsoluteUrl(QuestionnaireResponse questionnaireResponse) + { + return questionnaireResponse.getIdElement().toVersionless() + .withServerBase(serverBaseUrl, ResourceType.QuestionnaireResponse.name()).getValue(); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/ReadAccessHelperImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/ReadAccessHelperImpl.java new file mode 100644 index 000000000..66a6d315c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/ReadAccessHelperImpl.java @@ -0,0 +1,415 @@ +package dev.dsf.bpe.v2.service; + +import java.util.Arrays; +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.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.Organization; +import org.hl7.fhir.r4.model.OrganizationAffiliation; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; + +public class ReadAccessHelperImpl implements ReadAccessHelper +{ + private static final String READ_ACCESS_TAG_SYSTEM = "http://dsf.dev/fhir/CodeSystem/read-access-tag"; + private static final String READ_ACCESS_TAG_VALUE_LOCAL = "LOCAL"; + private static final String READ_ACCESS_TAG_VALUE_ORGANIZATION = "ORGANIZATION"; + private static final String READ_ACCESS_TAG_VALUE_ROLE = "ROLE"; + private static final String READ_ACCESS_TAG_VALUE_ALL = "ALL"; + + private static final String ORGANIZATION_IDENTIFIER_SYSTEM = "http://dsf.dev/sid/organization-identifier"; + + private static final String EXTENSION_READ_ACCESS_ORGANIZATION = "http://dsf.dev/fhir/StructureDefinition/extension-read-access-organization"; + + private static final String EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE = "http://dsf.dev/fhir/StructureDefinition/extension-read-access-parent-organization-role"; + private static final String EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION = "parent-organization"; + private static final String EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE = "organization-role"; + + private static final List READ_ACCESS_TAG_VALUES = Arrays.asList(READ_ACCESS_TAG_VALUE_LOCAL, + READ_ACCESS_TAG_VALUE_ORGANIZATION, READ_ACCESS_TAG_VALUE_ROLE, READ_ACCESS_TAG_VALUE_ALL); + + private Predicate matchesTagValue(String value) + { + return c -> c != null && READ_ACCESS_TAG_SYSTEM.equals(c.getSystem()) && c.hasCode() + && c.getCode().equals(value); + } + + @Override + public R addLocal(R resource) + { + if (resource == null) + return null; + + resource.getMeta().getTag().removeIf(matchesTagValue(READ_ACCESS_TAG_VALUE_ALL)); + resource.getMeta().addTag().setSystem(READ_ACCESS_TAG_SYSTEM).setCode(READ_ACCESS_TAG_VALUE_LOCAL); + + return resource; + } + + @Override + public R addOrganization(R resource, String organizationIdentifier) + { + if (resource == null) + return null; + + Objects.requireNonNull(organizationIdentifier, "organizationIdentifier"); + + if (resource.getMeta().getTag().stream().noneMatch(matchesTagValue(READ_ACCESS_TAG_VALUE_LOCAL))) + addLocal(resource); + + resource.getMeta().addTag().setSystem(READ_ACCESS_TAG_SYSTEM).setCode(READ_ACCESS_TAG_VALUE_ORGANIZATION) + .addExtension().setUrl(EXTENSION_READ_ACCESS_ORGANIZATION) + .setValue(new Identifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue(organizationIdentifier)); + + return resource; + } + + @Override + public R addOrganization(R resource, Organization organization) + { + if (resource == null) + return null; + + Objects.requireNonNull(organization, "organization"); + + if (!organization.hasIdentifier()) + throw new IllegalArgumentException("organization has no identifier"); + + Optional identifierValue = organization.getIdentifier().stream().filter(Identifier::hasSystem) + .filter(i -> ORGANIZATION_IDENTIFIER_SYSTEM.equals(i.getSystem())).filter(Identifier::hasValue) + .map(Identifier::getValue).filter(v -> !v.isBlank()).findFirst(); + + return addOrganization(resource, identifierValue.orElseThrow(() -> new IllegalArgumentException( + "organization has no non blank identifier value with system " + ORGANIZATION_IDENTIFIER_SYSTEM))); + } + + @Override + public R addRole(R resource, String parentOrganizationIdentifier, String roleSystem, + String roleCode) + { + if (resource == null) + return null; + + Objects.requireNonNull(parentOrganizationIdentifier, "parentOrganizationIdentifier"); + Objects.requireNonNull(roleSystem, "roleSystem"); + Objects.requireNonNull(roleCode, "roleCode"); + + if (resource.getMeta().getTag().stream().noneMatch(matchesTagValue(READ_ACCESS_TAG_VALUE_LOCAL))) + addLocal(resource); + + Extension ex = resource.getMeta().addTag().setSystem(READ_ACCESS_TAG_SYSTEM).setCode(READ_ACCESS_TAG_VALUE_ROLE) + .addExtension().setUrl(EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE); + ex.addExtension().setUrl(EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION).setValue( + new Identifier().setSystem(ORGANIZATION_IDENTIFIER_SYSTEM).setValue(parentOrganizationIdentifier)); + ex.addExtension().setUrl(EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE) + .setValue(new Coding().setSystem(roleSystem).setCode(roleCode)); + return resource; + } + + @Override + public R addRole(R resource, OrganizationAffiliation affiliation) + { + if (resource == null) + return null; + + Objects.requireNonNull(affiliation, "affiliation"); + if (!affiliation.hasOrganization()) + throw new IllegalArgumentException("affiliation has no parent-organization reference"); + if (!affiliation.getOrganization().hasIdentifier()) + throw new IllegalArgumentException("affiliation has no parent-organization reference with identifier"); + if (!affiliation.getOrganization().getIdentifier().hasSystem() + || !ORGANIZATION_IDENTIFIER_SYSTEM.equals(affiliation.getOrganization().getIdentifier().getSystem())) + throw new IllegalArgumentException( + "affiliation has no parent-organization reference with identifier system " + + ORGANIZATION_IDENTIFIER_SYSTEM); + if (!affiliation.getOrganization().getIdentifier().hasValue() + || affiliation.getOrganization().getIdentifier().getValue().isBlank()) + throw new IllegalArgumentException( + "affiliation has no parent-organization reference with non blank identifier value"); + + String parentOrganizationIdentifier = affiliation.getOrganization().getIdentifier().getValue(); + + if (!affiliation.hasCode() || affiliation.getCode().size() != 1 || !affiliation.getCodeFirstRep().hasCoding() + || affiliation.getCodeFirstRep().getCoding().size() != 1 + || !affiliation.getCodeFirstRep().getCodingFirstRep().hasCode() + || !affiliation.getCodeFirstRep().getCodingFirstRep().hasSystem()) + throw new IllegalArgumentException("affiliation has no single member role with code and system"); + + String roleSystem = affiliation.getCodeFirstRep().getCodingFirstRep().getSystem(); + String roleCode = affiliation.getCodeFirstRep().getCodingFirstRep().getCode(); + + return addRole(resource, parentOrganizationIdentifier, roleSystem, roleCode); + } + + @Override + public R addAll(R resource) + { + if (resource == null) + return null; + + resource.getMeta().getTag() + .removeIf(matchesTagValue(READ_ACCESS_TAG_VALUE_LOCAL) + .or(matchesTagValue(READ_ACCESS_TAG_VALUE_ORGANIZATION)) + .or(matchesTagValue(READ_ACCESS_TAG_VALUE_ROLE))); + + resource.getMeta().addTag().setSystem(READ_ACCESS_TAG_SYSTEM).setCode(READ_ACCESS_TAG_VALUE_ALL); + return resource; + } + + @Override + public boolean hasLocal(Resource resource) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + return resource.getMeta().getTag(READ_ACCESS_TAG_SYSTEM, READ_ACCESS_TAG_VALUE_LOCAL) != null; + } + + @Override + public boolean hasOrganization(Resource resource, String organizationIdentifier) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + Stream extensions = getTagExtensions(resource, READ_ACCESS_TAG_SYSTEM, + READ_ACCESS_TAG_VALUE_ORGANIZATION, EXTENSION_READ_ACCESS_ORGANIZATION); + + return extensions.filter(Extension::hasValue).map(Extension::getValue).filter(v -> v instanceof Identifier) + .map(v -> (Identifier) v).filter(Identifier::hasValue) + .anyMatch(i -> Objects.equals(i.getValue(), organizationIdentifier)); + } + + @Override + public boolean hasOrganization(Resource resource, Organization organization) + { + if (resource == null || organization == null) + return false; + + return organization.hasIdentifier() && organization.getIdentifier().stream().filter(Identifier::hasSystem) + .filter(i -> ORGANIZATION_IDENTIFIER_SYSTEM.equals(i.getSystem())).filter(Identifier::hasValue) + .map(Identifier::getValue).anyMatch(identifier -> hasOrganization(resource, identifier)); + } + + private Stream getTagExtensions(Resource resource, String tagSystem, String tagCode, String extensionUrl) + { + return resource.getMeta().getTag().stream().filter(c -> Objects.equals(c.getSystem(), tagSystem)) + .filter(c -> Objects.equals(c.getCode(), tagCode)).filter(Coding::hasExtension) + .flatMap(c -> c.getExtension().stream()).filter(e -> Objects.equals(e.getUrl(), extensionUrl)); + } + + @Override + public boolean hasAnyOrganization(Resource resource) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + return resource.getMeta().getTag(READ_ACCESS_TAG_SYSTEM, READ_ACCESS_TAG_VALUE_ORGANIZATION) != null; + } + + @Override + public boolean hasRole(Resource resource, String parentOrganizationIdentifier, String roleSystem, String roleCode) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + Stream extensions = getTagExtensions(resource, READ_ACCESS_TAG_SYSTEM, READ_ACCESS_TAG_VALUE_ROLE, + EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE); + + return extensions.filter(Extension::hasExtension) + .anyMatch(matches(parentOrganizationIdentifier, roleSystem, roleCode)); + } + + @Override + public boolean hasRole(Resource resource, List affiliations) + { + if (affiliations == null || affiliations.isEmpty()) + return false; + + return affiliations.stream().anyMatch(affiliation -> hasRole(resource, affiliation)); + } + + private Predicate matches(String parentOrganizationIdentifier, String roleSystem, + String roleCode) + { + return extensions -> + { + boolean cor = extensions.getExtension().stream().filter(Extension::hasUrl) + .filter(e -> Objects.equals(e.getUrl(), + EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION)) + .filter(Extension::hasValue).map(Extension::getValue).filter(v -> v instanceof Identifier) + .map(v -> (Identifier) v).filter(Identifier::hasSystem).filter(Identifier::hasValue) + .anyMatch(i -> ORGANIZATION_IDENTIFIER_SYSTEM.equals(i.getSystem()) + && Objects.equals(i.getValue(), parentOrganizationIdentifier)); + boolean role = extensions.getExtension().stream().filter(Extension::hasUrl) + .filter(e -> Objects.equals(e.getUrl(), + EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE)) + .filter(Extension::hasValue).map(Extension::getValue).filter(v -> v instanceof Coding) + .map(v -> (Coding) v) + .anyMatch(c -> Objects.equals(c.getSystem(), roleSystem) && Objects.equals(c.getCode(), roleCode)); + return cor && role; + }; + } + + @Override + public boolean hasRole(Resource resource, OrganizationAffiliation affiliation) + { + if (resource == null || affiliation == null || !affiliation.hasOrganization() || !affiliation.hasCode()) + return false; + + Reference parentOrganizationRef = affiliation.getOrganization(); + if (!parentOrganizationRef.hasIdentifier()) + return false; + Identifier parentOrganizationIdentifier = parentOrganizationRef.getIdentifier(); + if (!parentOrganizationIdentifier.hasValue()) + return false; + + String parentOrganizationIdentifierValue = parentOrganizationRef.getIdentifier().getValue(); + + return affiliation.getCode().stream().filter(CodeableConcept::hasCoding).flatMap(c -> c.getCoding().stream()) + .filter(Coding::hasSystem).filter(Coding::hasCode) + .anyMatch(c -> hasRole(resource, parentOrganizationIdentifierValue, c.getSystem(), c.getCode())); + } + + @Override + public boolean hasAnyRole(Resource resource) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + return resource.getMeta().getTag(READ_ACCESS_TAG_SYSTEM, READ_ACCESS_TAG_VALUE_ROLE) != null; + } + + @Override + public boolean hasAll(Resource resource) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + return resource.getMeta().getTag(READ_ACCESS_TAG_SYSTEM, READ_ACCESS_TAG_VALUE_ALL) != null; + } + + @Override + public boolean isValid(Resource resource) + { + return isValid(resource, organizationIdentifier -> true, role -> true); + } + + @Override + public boolean isValid(Resource resource, Predicate organizationWithIdentifierExists, + Predicate roleExists) + { + if (resource == null || !resource.hasMeta() || !resource.getMeta().hasTag()) + return false; + + // 1 LOCAL && N (ORGANIZATION, ROLE) + // 1 All + // all({LOCAL, ORGANIZATION, ROLE, ALL}) valid + + long tagsCount = resource.getMeta().getTag().stream().filter(Coding::hasSystem).filter(Coding::hasCode) + .filter(c -> READ_ACCESS_TAG_SYSTEM.equals(c.getSystem())) + .filter(c -> READ_ACCESS_TAG_VALUES.contains(c.getCode())).count(); + boolean local = resource.getMeta().getTag().stream().filter(Coding::hasSystem).filter(Coding::hasCode) + .filter(c -> READ_ACCESS_TAG_SYSTEM.equals(c.getSystem())) + .filter(c -> READ_ACCESS_TAG_VALUE_LOCAL.equals(c.getCode())).count() == 1; + boolean all = resource.getMeta().getTag().stream().filter(Coding::hasSystem).filter(Coding::hasCode) + .filter(c -> READ_ACCESS_TAG_SYSTEM.equals(c.getSystem())) + .filter(c -> READ_ACCESS_TAG_VALUE_ALL.equals(c.getCode())).count() == 1; + boolean tagsValid = resource.getMeta().getTag().stream().filter(Coding::hasSystem).filter(Coding::hasCode) + .filter(c -> READ_ACCESS_TAG_SYSTEM.equals(c.getSystem())) + .filter(c -> READ_ACCESS_TAG_VALUES.contains(c.getCode())) + .allMatch(isValidReadAccessTag(organizationWithIdentifierExists, roleExists)); + + return ((local && tagsCount >= 1) ^ (all && tagsCount == 1)) && tagsValid; + } + + private Predicate isValidReadAccessTag(Predicate organizationWithIdentifierExists, + Predicate roleExists) + { + return coding -> switch (coding.getCode()) + { + case READ_ACCESS_TAG_VALUE_LOCAL -> true; + case READ_ACCESS_TAG_VALUE_ORGANIZATION -> + isValidOrganizationReadAccessTag(coding, organizationWithIdentifierExists); + case READ_ACCESS_TAG_VALUE_ROLE -> + isValidRoleReadAccessTag(coding, organizationWithIdentifierExists, roleExists); + case READ_ACCESS_TAG_VALUE_ALL -> true; + default -> false; + }; + } + + private boolean isValidOrganizationReadAccessTag(Coding coding, + Predicate organizationWithIdentifierExists) + { + List exts = coding.getExtension().stream().filter(Extension::hasUrl) + .filter(e -> EXTENSION_READ_ACCESS_ORGANIZATION.equals(e.getUrl())).collect(Collectors.toList()); + + return coding.hasExtension() && exts.size() == 1 + && isValidExtensionReadAccesOrganization(exts.get(0), organizationWithIdentifierExists); + } + + private boolean isValidExtensionReadAccesOrganization(Extension extension, + Predicate organizationWithIdentifierExists) + { + return extension.hasValue() && extension.getValue() instanceof Identifier value + && isValidOrganizationIdentifier(value, organizationWithIdentifierExists); + } + + private boolean isValidOrganizationIdentifier(Identifier identifier, + Predicate organizationWithIdentifierExists) + { + return identifier.hasSystem() && ORGANIZATION_IDENTIFIER_SYSTEM.equals(identifier.getSystem()) + && identifier.hasValue() && organizationWithIdentifierExists.test(identifier); + } + + private boolean isValidRoleReadAccessTag(Coding coding, Predicate organizationWithIdentifierExists, + Predicate roleExists) + { + List exts = coding.getExtension().stream().filter(Extension::hasUrl) + .filter(e -> EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE.equals(e.getUrl())) + .collect(Collectors.toList()); + + return coding.hasExtension() && exts.size() == 1 && isValidExtensionReadAccessParentOrganizationMemberRole( + exts.get(0), organizationWithIdentifierExists, roleExists); + } + + private boolean isValidExtensionReadAccessParentOrganizationMemberRole(Extension extension, + Predicate organizationWithIdentifierExists, Predicate roleExists) + { + return extension.hasExtension() && extension.getExtension().size() == 2 + && extension.getExtension().stream() + .filter(e -> isValidExtensionReadAccessParentOrganizationMemberRoleParentOrganization(e, + organizationWithIdentifierExists)) + .count() == 1 + && extension.getExtension().stream() + .filter(e -> isValidExtensionReadAccessParentOrganizationMemberRoleRole(e, roleExists)) + .count() == 1; + } + + private boolean isValidExtensionReadAccessParentOrganizationMemberRoleParentOrganization(Extension e, + Predicate organizationWithIdentifierExists) + { + return e.hasUrl() && EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION.equals(e.getUrl()) + && e.hasValue() && e.getValue() instanceof Identifier value + && isValidOrganizationIdentifier(value, organizationWithIdentifierExists); + } + + private boolean isValidExtensionReadAccessParentOrganizationMemberRoleRole(Extension e, + Predicate roleExists) + { + return e.hasUrl() && EXTENSION_READ_ACCESS_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE.equals(e.getUrl()) + && e.hasValue() && e.getValue() instanceof Coding value && isValidRole(value, roleExists); + } + + private boolean isValidRole(Coding coding, Predicate roleExists) + { + return coding.hasSystem() && coding.hasCode() && roleExists.test(coding); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/TaskHelperImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/TaskHelperImpl.java new file mode 100644 index 000000000..5e2009c07 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/TaskHelperImpl.java @@ -0,0 +1,127 @@ +package dev.dsf.bpe.v2.service; + +import java.util.Objects; +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.ResourceType; +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 class TaskHelperImpl implements TaskHelper +{ + private final String serverBaseUrl; + + /** + * @param serverBaseUrl + * not null + */ + public TaskHelperImpl(String serverBaseUrl) + { + this.serverBaseUrl = serverBaseUrl; + } + + @Override + public String getLocalVersionlessAbsoluteUrl(Task task) + { + if (task == null) + return null; + + return task.getIdElement().toVersionless().withServerBase(serverBaseUrl, ResourceType.Task.name()).getValue(); + } + + @Override + public Stream getInputParameterStringValues(Task task, Coding coding) + { + return getInputParameterValues(task, coding, StringType.class).map(StringType::getValue); + } + + @Override + public Stream getInputParameterStringValues(Task task, String system, String code) + { + return getInputParameterValues(task, system, code, StringType.class).map(StringType::getValue); + } + + @Override + public Stream getInputParameterValues(Task task, Coding coding, Class expectedType) + { + return getInputParameters(task, coding, expectedType).filter(ParameterComponent::hasValue) + .map(c -> expectedType.cast(c.getValue())); + } + + @Override + public Stream getInputParameterValues(Task task, String system, String code, + Class expectedType) + { + return getInputParameters(task, system, code, expectedType).filter(ParameterComponent::hasValue) + .map(c -> expectedType.cast(c.getValue())); + } + + @Override + public Stream getInputParametersWithExtension(Task task, Coding coding, + Class expectedType, String extensionUrl) + { + return getInputParameters(task, coding, expectedType).filter(ParameterComponent::hasExtension) + .filter(c -> c.getExtension().stream().anyMatch(e -> Objects.equals(extensionUrl, e.getUrl()))); + } + + @Override + public Stream getInputParametersWithExtension(Task task, String system, String code, + Class expectedType, String extensionUrl) + { + return getInputParameters(task, system, code, expectedType).filter(ParameterComponent::hasExtension) + .filter(c -> c.getExtension().stream().anyMatch(e -> Objects.equals(extensionUrl, e.getUrl()))); + } + + @Override + public Stream getInputParameters(Task task, Coding coding, Class expectedType) + { + if (coding == null) + return Stream.empty(); + + return getInputParameters(task, coding.getSystem(), coding.getCode(), expectedType); + } + + @Override + public Stream getInputParameters(Task task, String system, String code, + Class expectedType) + { + Objects.requireNonNull(expectedType, "expectedType"); + + if (task == null) + return Stream.empty(); + + return task.getInput().stream().filter(c -> c.hasType() && c.getType().hasCoding()) + .filter(c -> c.getType().getCoding().stream() + .anyMatch(co -> Objects.equals(system, co.getSystem()) && Objects.equals(code, co.getCode()))) + .filter(c -> c.hasValue() && expectedType.isInstance(c.getValue())); + } + + @Override + public ParameterComponent createInput(Type value, Coding coding) + { + return new ParameterComponent(new CodeableConcept(coding), value); + } + + @Override + public ParameterComponent createInput(Type value, String system, String code) + { + return createInput(value, new Coding(system, code, null)); + } + + @Override + public TaskOutputComponent createOutput(Type value, Coding coding) + { + return new TaskOutputComponent(new CodeableConcept(coding), value); + } + + @Override + public TaskOutputComponent createOutput(Type value, String system, String code) + { + return createOutput(value, new Coding(system, code, null)); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/All.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/All.java new file mode 100644 index 000000000..fb71c7982 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/All.java @@ -0,0 +1,205 @@ +package dev.dsf.bpe.v2.service.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.bpe.v2.constants.CodeSystems.ProcessAuthorization; + +public class All implements Recipient, Requester +{ + private static final String EXTENSION_PROCESS_AUTHORIZATION_REQUESTER = "requester"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT = "recipient"; + + private static final String EXTENSION_PROCESS_AUTHORIZATION_PRACTITIONER = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization-practitioner"; + + 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(EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT).setValue(toCoding(false)); + } + + @Override + public Extension toRequesterExtension() + { + return new Extension().setUrl(EXTENSION_PROCESS_AUTHORIZATION_REQUESTER) + .setValue(toCoding(needsPractitionerRole())); + } + + private Coding toCoding(boolean needsPractitionerRole) + { + Coding coding = getProcessAuthorizationCode(); + + if (needsPractitionerRole) + coding.addExtension().setUrl(EXTENSION_PROCESS_AUTHORIZATION_PRACTITIONER) + .setValue(new Coding(practitionerRoleSystem, practitionerRoleCode, null)); + + return coding; + } + + @Override + public Coding getProcessAuthorizationCode() + { + if (localIdentity) + { + if (needsPractitionerRole()) + return ProcessAuthorization.localAllPractitioner(); + else + return ProcessAuthorization.localAll(); + } + else + return ProcessAuthorization.remoteAll(); + } + + @Override + public boolean requesterMatches(Extension requesterExtension) + { + return matches(requesterExtension, EXTENSION_PROCESS_AUTHORIZATION_REQUESTER) + && hasMatchingPractitionerExtension(requesterExtension.getValue().getExtension()); + } + + @Override + public boolean recipientMatches(Extension recipientExtension) + { + return matches(recipientExtension, 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 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 ProcessAuthorization.isLocalAllPractitioner(processAuthorizationCode); + else + return ProcessAuthorization.isLocalAll(processAuthorizationCode); + else + return ProcessAuthorization.isRemoteAll(processAuthorizationCode); + } + + public static Optional fromRequester(Coding coding, Predicate practitionerRoleExists) + { + if (ProcessAuthorization.isLocalAll(coding)) + return Optional.of(new All(true, null, null)); + else if (ProcessAuthorization.isRemoteAll(coding)) + return Optional.of(new All(false, null, null)); + else if (ProcessAuthorization.isLocalAllPractitioner(coding)) + return fromPractitionerRequester(coding, practitionerRoleExists); + else + 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 -> 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 (ProcessAuthorization.isLocalAll(coding)) + return Optional.of(new All(true, null, null)); + else + // remote not allowed for recipient + return Optional.empty(); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/Organization.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/Organization.java new file mode 100644 index 000000000..77f945885 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/Organization.java @@ -0,0 +1,322 @@ +package dev.dsf.bpe.v2.service.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 dev.dsf.bpe.v2.constants.CodeSystems.ProcessAuthorization; +import dev.dsf.bpe.v2.constants.NamingSystems.OrganizationIdentifier; + +public class Organization implements Recipient, Requester +{ + private static final String EXTENSION_PROCESS_AUTHORIZATION_REQUESTER = "requester"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT = "recipient"; + + private static final String EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization-organization"; + + private static final String EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization-organization-practitioner"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_ORGANIZATION = "organization"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_PRACTITIONER_ROLE = "practitioner-role"; + + 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 -> OrganizationIdentifier.SID.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(EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT).setValue(toCoding(false)); + } + + @Override + public Extension toRequesterExtension() + { + return new Extension().setUrl(EXTENSION_PROCESS_AUTHORIZATION_REQUESTER) + .setValue(toCoding(needsPractitionerRole())); + } + + private Coding toCoding(boolean needsPractitionerRole) + { + Identifier organization = OrganizationIdentifier.withValue(organizationIdentifier); + Coding coding = getProcessAuthorizationCode(); + + if (needsPractitionerRole) + { + Extension extension = coding.addExtension() + .setUrl(EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER); + extension.addExtension(EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_ORGANIZATION, + organization); + extension.addExtension(EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_PRACTITIONER_ROLE, + new Coding(practitionerRoleSystem, practitionerRoleCode, null)); + } + else + { + coding.addExtension().setUrl(EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION).setValue(organization); + } + + return coding; + } + + @Override + public Coding getProcessAuthorizationCode() + { + if (localIdentity) + { + if (needsPractitionerRole()) + return ProcessAuthorization.localOrganizationPractitioner(); + else + return ProcessAuthorization.localOrganization(); + } + else + return ProcessAuthorization.remoteOrganization(); + } + + @Override + public boolean requesterMatches(Extension requesterExtension) + { + return matches(requesterExtension, EXTENSION_PROCESS_AUTHORIZATION_REQUESTER, needsPractitionerRole()); + } + + @Override + public boolean recipientMatches(Extension recipientExtension) + { + return matches(recipientExtension, 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 -> EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER.equals(extension.getUrl()) + && !extension.hasValue() && hasMatchingSubOrganizationExtension(extension.getExtension()) + && hasMatchingPractitionerExtension(extension.getExtension()); + } + else + { + return extension -> 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() + && OrganizationIdentifier.SID.equals(identifier.getSystem()) + && organizationIdentifier.equals(identifier.getValue()); + } + + private boolean hasMatchingSubOrganizationExtension(List extensions) + { + return extensions.stream().anyMatch(this::subOrganizationExtensionMatches); + } + + private boolean subOrganizationExtensionMatches(Extension extension) + { + return 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 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 ProcessAuthorization.isLocalOrganizationPractitioner(processAuthorizationCode); + else + return ProcessAuthorization.isLocalOrganization(processAuthorizationCode); + else + return ProcessAuthorization.isRemoteOrganization(processAuthorizationCode); + } + + public static Optional fromRequester(Coding coding, Predicate practitionerRoleExists, + Predicate organizationWithIdentifierExists) + { + if (ProcessAuthorization.isLocalOrganization(coding)) + return Optional.ofNullable(from(true, coding, organizationWithIdentifierExists)); + else if (ProcessAuthorization.isRemoteOrganization(coding)) + return Optional.ofNullable(from(false, coding, organizationWithIdentifierExists)); + else if (ProcessAuthorization.isLocalOrganizationPractitioner(coding)) + return fromPractitionerRequester(coding, practitionerRoleExists, organizationWithIdentifierExists); + else + return Optional.empty(); + } + + public static Optional fromRecipient(Coding coding, + Predicate organizationWithIdentifierExists) + { + if (ProcessAuthorization.isLocalOrganization(coding)) + return Optional.ofNullable(from(true, coding, organizationWithIdentifierExists)); + else + return Optional.empty(); + } + + private static Organization from(boolean localIdentity, Coding coding, + Predicate organizationWithIdentifierExists) + { + if (coding != null && coding.hasExtension()) + { + List organizations = coding.getExtension().stream().filter(Extension::hasUrl) + .filter(e -> 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 + && OrganizationIdentifier.SID.equals(identifier.getSystem()) + && organizationWithIdentifierExists.test(identifier)) + { + return new Organization(localIdentity, identifier.getValue(), null, null); + } + } + } + + return null; + } + + 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 -> 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 -> EXTENSION_PROCESS_AUTHORIZATION_ORGANIZATION_PRACTITIONER_ORGANIZATION + .equals(e.getUrl())) + .collect(Collectors.toList()); + List practitionerRoles = organizationPractitioner.getExtension().stream() + .filter(Extension::hasUrl) + .filter(e -> 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 + && OrganizationIdentifier.SID.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-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/ProcessAuthorizationHelperImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/ProcessAuthorizationHelperImpl.java new file mode 100644 index 000000000..d9010b6c5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/ProcessAuthorizationHelperImpl.java @@ -0,0 +1,508 @@ +package dev.dsf.bpe.v2.service.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; + +import dev.dsf.bpe.v2.constants.CodeSystems.ProcessAuthorization; + +public class ProcessAuthorizationHelperImpl implements ProcessAuthorizationHelper +{ + private static final String EXTENSION_PROCESS_AUTHORIZATION = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_MESSAGE_NAME = "message-name"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_TASK_PROFILE = "task-profile"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_REQUESTER = "requester"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT = "recipient"; + + private static final class RecipientFactoryImpl implements RecipientFactory + { + @Override + public Recipient localAll() + { + return new All(true, null, null); + } + + @Override + public Recipient localOrganization(String organizationIdentifier) + { + return new Organization(true, organizationIdentifier, null, null); + } + + @Override + public Recipient localRole(String parentOrganizationIdentifier, String roleSystem, String roleCode) + { + return new Role(true, parentOrganizationIdentifier, roleSystem, roleCode, null, null); + } + } + + private static final class RequesterFactoryImpl implements RequesterFactory + { + @Override + public Requester localAll() + { + return all(true, null, null); + } + + @Override + public Requester localAllPractitioner(String practitionerRoleSystem, String practitionerRoleCode) + { + return all(true, practitionerRoleSystem, practitionerRoleCode); + } + + @Override + public Requester remoteAll() + { + return all(false, null, null); + } + + private Requester all(boolean localIdentity, String userRoleSystem, String userRoleCode) + { + return new All(localIdentity, userRoleSystem, userRoleCode); + } + + @Override + public Requester localOrganization(String organizationIdentifier) + { + return organization(true, organizationIdentifier, null, null); + } + + @Override + public Requester localOrganizationPractitioner(String organizationIdentifier, String practitionerRoleSystem, + String practitionerRoleCode) + { + return organization(true, organizationIdentifier, practitionerRoleSystem, practitionerRoleCode); + } + + @Override + public Requester remoteOrganization(String organizationIdentifier) + { + return organization(false, organizationIdentifier, null, null); + } + + private Requester organization(boolean localIdentity, String organizationIdentifier, + String practitionerRoleSystem, String practitionerRoleCode) + { + return new Organization(localIdentity, organizationIdentifier, practitionerRoleSystem, + practitionerRoleCode); + } + + @Override + public Requester localRole(String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode) + { + return role(true, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode, null, null); + } + + @Override + public Requester localRolePractitioner(String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode, String practitionerRoleSystem, String practitionerRoleCode) + { + return role(true, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode, + practitionerRoleSystem, practitionerRoleCode); + } + + @Override + public Requester remoteRole(String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode) + { + return role(false, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode, null, null); + } + + private Requester role(boolean localIdentity, String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode, String practitionerRoleSystem, String practitionerRoleCode) + { + return new Role(localIdentity, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode, + practitionerRoleSystem, practitionerRoleCode); + } + } + + private static final RecipientFactory RECIPIENT_FACTORY = new RecipientFactoryImpl(); + private static final RequesterFactory REQUESTER_FACTORY = new RequesterFactoryImpl(); + + @Override + public RecipientFactory getRecipientFactory() + { + return RECIPIENT_FACTORY; + } + + @Override + public RequesterFactory getRequesterFactory() + { + return REQUESTER_FACTORY; + } + + @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 requesters, Collection 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 -> 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 -> 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 -> 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(EXTENSION_PROCESS_AUTHORIZATION); + e.addExtension(newMessageName(messageName)); + e.addExtension(newTaskProfile(taskProfile)); + + return e; + } + + private Extension newMessageName(String messageName) + { + return new Extension(EXTENSION_PROCESS_AUTHORIZATION_MESSAGE_NAME).setValue(new StringType(messageName)); + } + + private Extension newTaskProfile(String taskProfile) + { + return new Extension(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 -> 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 || !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 || !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 || !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 || !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 ProcessAuthorization.Codes.LOCAL_ALL: + case ProcessAuthorization.Codes.LOCAL_ALL_PRACTITIONER: + case ProcessAuthorization.Codes.REMOTE_ALL: + return All.fromRequester(coding, practitionerRoleExists); + + case ProcessAuthorization.Codes.LOCAL_ORGANIZATION: + case ProcessAuthorization.Codes.LOCAL_ORGANIZATION_PRACTITIONER: + case ProcessAuthorization.Codes.REMOTE_ORGANIZATION: + return Organization.fromRequester(coding, practitionerRoleExists, organizationWithIdentifierExists); + + case ProcessAuthorization.Codes.LOCAL_ROLE: + case ProcessAuthorization.Codes.LOCAL_ROLE_PRACTITIONER: + case ProcessAuthorization.Codes.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 || !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 ProcessAuthorization.Codes.LOCAL_ALL -> All.fromRecipient(coding); + + case ProcessAuthorization.Codes.LOCAL_ORGANIZATION -> + Organization.fromRecipient(coding, organizationWithIdentifierExists); + + case ProcessAuthorization.Codes.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 -> 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, c -> true, i -> true, c -> 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 -> 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, i -> true, c -> 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 -> 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 -> 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-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/RequesterFactoryImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/RequesterFactoryImpl.java new file mode 100644 index 000000000..f75fb3b8a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/RequesterFactoryImpl.java @@ -0,0 +1,83 @@ +package dev.dsf.bpe.v2.service.process; + +import dev.dsf.bpe.v2.service.process.ProcessAuthorizationHelper.RequesterFactory; + +public class RequesterFactoryImpl implements RequesterFactory +{ + @Override + public Requester localAll() + { + return all(true, null, null); + } + + @Override + public Requester localAllPractitioner(String practitionerRoleSystem, String practitionerRoleCode) + { + return all(true, practitionerRoleSystem, practitionerRoleCode); + } + + @Override + public Requester remoteAll() + { + return all(false, null, null); + } + + private Requester all(boolean localIdentity, String userRoleSystem, String userRoleCode) + { + return new All(localIdentity, userRoleSystem, userRoleCode); + } + + @Override + public Requester localOrganization(String organizationIdentifier) + { + return organization(true, organizationIdentifier, null, null); + } + + @Override + public Requester localOrganizationPractitioner(String organizationIdentifier, String practitionerRoleSystem, + String practitionerRoleCode) + { + return organization(true, organizationIdentifier, practitionerRoleSystem, practitionerRoleCode); + } + + @Override + public Requester remoteOrganization(String organizationIdentifier) + { + return organization(false, organizationIdentifier, null, null); + } + + private Requester organization(boolean localIdentity, String organizationIdentifier, String practitionerRoleSystem, + String practitionerRoleCode) + { + return new Organization(localIdentity, organizationIdentifier, practitionerRoleSystem, practitionerRoleCode); + } + + @Override + public Requester localRole(String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode) + { + return role(true, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode, null, null); + } + + @Override + public Requester localRolePractitioner(String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode, String practitionerRoleSystem, String practitionerRoleCode) + { + return role(true, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode, + practitionerRoleSystem, practitionerRoleCode); + } + + @Override + public Requester remoteRole(String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode) + { + return role(false, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode, null, null); + } + + private Requester role(boolean localIdentity, String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode, String practitionerRoleSystem, String practitionerRoleCode) + { + return new Role(localIdentity, parentOrganizationIdentifier, organizatioRoleSystem, organizatioRoleCode, + practitionerRoleSystem, practitionerRoleCode); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/Role.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/Role.java new file mode 100644 index 000000000..35fa19017 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/service/process/Role.java @@ -0,0 +1,432 @@ +package dev.dsf.bpe.v2.service.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 dev.dsf.bpe.v2.constants.CodeSystems.ProcessAuthorization; +import dev.dsf.bpe.v2.constants.NamingSystems.OrganizationIdentifier; + +public class Role implements Recipient, Requester +{ + private static final String EXTENSION_PROCESS_AUTHORIZATION_REQUESTER = "requester"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT = "recipient"; + + private static final String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization-parent-organization-role"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION = "parent-organization"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE = "organization-role"; + + private static final String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER = "http://dsf.dev/fhir/StructureDefinition/extension-process-authorization-parent-organization-role-practitioner"; + private static final String EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER_PRACTITIONER_ROLE = "practitioner-role"; + + 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 -> OrganizationIdentifier.SID.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(EXTENSION_PROCESS_AUTHORIZATION_RECIPIENT).setValue(toCoding(false)); + } + + @Override + public Extension toRequesterExtension() + { + return new Extension().setUrl(EXTENSION_PROCESS_AUTHORIZATION_REQUESTER) + .setValue(toCoding(needsPractitionerRole())); + } + + private Coding toCoding(boolean needsPractitionerRole) + { + Identifier parentOrganization = OrganizationIdentifier.withValue(parentOrganizationIdentifier); + Extension parentOrganizationExt = new Extension( + EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION, parentOrganization); + + Coding organizationRole = new Coding(organizationRoleSystem, organizationRoleCode, null); + Extension organizationRoleExt = new Extension( + EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE, organizationRole); + + Coding coding = getProcessAuthorizationCode(); + + if (needsPractitionerRole) + { + Extension practitionerRoleExt = new Extension( + EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER_PRACTITIONER_ROLE, + new Coding(practitionerRoleSystem, practitionerRoleCode, null)); + + coding.addExtension().setUrl(EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER) + .addExtension(parentOrganizationExt).addExtension(organizationRoleExt) + .addExtension(practitionerRoleExt); + } + else + { + coding.addExtension().setUrl(EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE) + .addExtension(parentOrganizationExt).addExtension(organizationRoleExt); + } + + return coding; + } + + @Override + public Coding getProcessAuthorizationCode() + { + if (localIdentity) + { + if (needsPractitionerRole()) + return ProcessAuthorization.localRolePractitioner(); + else + return ProcessAuthorization.localRole(); + } + else + return ProcessAuthorization.remoteRole(); + } + + @Override + public boolean requesterMatches(Extension requesterExtension) + { + return matches(requesterExtension, EXTENSION_PROCESS_AUTHORIZATION_REQUESTER, needsPractitionerRole()); + } + + @Override + public boolean recipientMatches(Extension recipientExtension) + { + return matches(recipientExtension, 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 -> EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PRACTITIONER + .equals(extension.getUrl()) && extension.hasExtension() + && hasMatchingParentOrganizationExtension(extension.getExtension()) + && hasMatchingOrganizationRoleExtension(extension.getExtension()) + && hasMatchingPractitionerRoleExtension(extension.getExtension()); + } + else + { + return extension -> 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 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() + && OrganizationIdentifier.SID.equals(identifier.getSystem()) + && parentOrganizationIdentifier.equals(identifier.getValue()); + } + + private boolean hasMatchingOrganizationRoleExtension(List extensions) + { + return extensions.stream().anyMatch(this::organizationRoleExtensionMatches); + } + + private boolean organizationRoleExtensionMatches(Extension extension) + { + return 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 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 ProcessAuthorization.isLocalRolePractitioner(processAuthorizationCode); + else + return ProcessAuthorization.isLocalRole(processAuthorizationCode); + else + return ProcessAuthorization.isRemoteRole(processAuthorizationCode); + } + + public static Optional fromRequester(Coding coding, Predicate practitionerRoleExists, + Predicate organizationWithIdentifierExists, Predicate organizationRoleExists) + { + if (ProcessAuthorization.isLocalRole(coding)) + return Optional.ofNullable(from(true, coding, organizationWithIdentifierExists, organizationRoleExists)); + else if (ProcessAuthorization.isRemoteRole(coding)) + return Optional.ofNullable(from(false, coding, organizationWithIdentifierExists, organizationRoleExists)); + else if (ProcessAuthorization.isLocalRolePractitioner(coding)) + return fromPractitionerRequester(coding, practitionerRoleExists, organizationWithIdentifierExists, + organizationRoleExists); + else + return Optional.empty(); + } + + public static Optional fromRecipient(Coding coding, + Predicate organizationWithIdentifierExists, Predicate organizationRoleExists) + { + if (ProcessAuthorization.isLocalRole(coding)) + return Optional.ofNullable(from(true, coding, organizationWithIdentifierExists, organizationRoleExists)); + else + return Optional.empty(); + } + + private static Role from(boolean localIdentity, Coding coding, + Predicate organizationWithIdentifierExists, Predicate organizationRoleExists) + { + if (coding != null && coding.hasExtension()) + { + List parentOrganizationRoles = coding.getExtension().stream().filter(Extension::hasUrl) + .filter(e -> 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 -> EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION + .equals(e.getUrl())) + .collect(Collectors.toList()); + List organizationRoles = parentOrganizationRole.getExtension().stream() + .filter(Extension::hasUrl) + .filter(e -> 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 + && OrganizationIdentifier.SID.equals(parentOrganizationIdentifier.getSystem()) + && organizationWithIdentifierExists.test(parentOrganizationIdentifier) + && organizationRoleExists.test(organizationRoleCoding)) + { + return new Role(localIdentity, parentOrganizationIdentifier.getValue(), + organizationRoleCoding.getSystem(), organizationRoleCoding.getCode(), null, null); + } + } + } + } + + return null; + } + + 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 -> 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 -> EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_PARENT_ORGANIZATION + .equals(e.getUrl())) + .collect(Collectors.toList()); + List organizationRoles = parentOrganizationRolePractitioner.getExtension().stream() + .filter(Extension::hasUrl) + .filter(e -> EXTENSION_PROCESS_AUTHORIZATION_PARENT_ORGANIZATION_ROLE_ORGANIZATION_ROLE + .equals(e.getUrl())) + .collect(Collectors.toList()); + List practitionerRoles = parentOrganizationRolePractitioner.getExtension().stream() + .filter(Extension::hasUrl) + .filter(e -> 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 + && OrganizationIdentifier.SID.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-v2-impl/src/main/java/dev/dsf/bpe/v2/spring/ApiServiceConfig.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/spring/ApiServiceConfig.java new file mode 100644 index 000000000..65f38f70d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/spring/ApiServiceConfig.java @@ -0,0 +1,194 @@ +package dev.dsf.bpe.v2.spring; + +import java.util.Locale; +import java.util.UUID; + +import org.camunda.bpm.engine.delegate.ExecutionListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.i18n.HapiLocalizer; +import dev.dsf.bpe.api.config.ClientConfig; +import dev.dsf.bpe.api.listener.ListenerFactory; +import dev.dsf.bpe.api.listener.ListenerFactoryImpl; +import dev.dsf.bpe.api.service.BpeMailService; +import dev.dsf.bpe.api.service.BuildInfoProvider; +import dev.dsf.bpe.v2.ProcessPluginApi; +import dev.dsf.bpe.v2.ProcessPluginApiImpl; +import dev.dsf.bpe.v2.client.ReferenceCleaner; +import dev.dsf.bpe.v2.client.ReferenceCleanerImpl; +import dev.dsf.bpe.v2.client.ReferenceExtractor; +import dev.dsf.bpe.v2.client.ReferenceExtractorImpl; +import dev.dsf.bpe.v2.config.ProxyConfig; +import dev.dsf.bpe.v2.config.ProxyConfigDelegate; +import dev.dsf.bpe.v2.listener.ContinueListener; +import dev.dsf.bpe.v2.listener.EndListener; +import dev.dsf.bpe.v2.listener.StartListener; +import dev.dsf.bpe.v2.plugin.ProcessPluginFactoryImpl; +import dev.dsf.bpe.v2.service.EndpointProvider; +import dev.dsf.bpe.v2.service.EndpointProviderImpl; +import dev.dsf.bpe.v2.service.FhirWebserviceClientProvider; +import dev.dsf.bpe.v2.service.FhirWebserviceClientProviderImpl; +import dev.dsf.bpe.v2.service.MailService; +import dev.dsf.bpe.v2.service.MailServiceImpl; +import dev.dsf.bpe.v2.service.OrganizationProvider; +import dev.dsf.bpe.v2.service.OrganizationProviderImpl; +import dev.dsf.bpe.v2.service.QuestionnaireResponseHelper; +import dev.dsf.bpe.v2.service.QuestionnaireResponseHelperImpl; +import dev.dsf.bpe.v2.service.ReadAccessHelper; +import dev.dsf.bpe.v2.service.ReadAccessHelperImpl; +import dev.dsf.bpe.v2.service.TaskHelper; +import dev.dsf.bpe.v2.service.TaskHelperImpl; +import dev.dsf.bpe.v2.service.process.ProcessAuthorizationHelper; +import dev.dsf.bpe.v2.service.process.ProcessAuthorizationHelperImpl; +import dev.dsf.bpe.v2.variables.FhirResourceSerializer; +import dev.dsf.bpe.v2.variables.FhirResourcesListSerializer; +import dev.dsf.bpe.v2.variables.ObjectMapperFactory; +import dev.dsf.bpe.v2.variables.TargetSerializer; +import dev.dsf.bpe.v2.variables.TargetsSerializer; +import dev.dsf.bpe.v2.variables.VariablesImpl; + +@Configuration +public class ApiServiceConfig +{ + @Autowired + private ClientConfig environmentConfig; + + @Autowired + private dev.dsf.bpe.api.config.ProxyConfig proxyConfig; + + @Autowired + private BuildInfoProvider buildInfoProvider; + + @Autowired + private BpeMailService bpeMailService; + + @Bean + public ProcessPluginApi processPluginApiV2() + { + ProxyConfig proxyConfig = new ProxyConfigDelegate(this.proxyConfig); + + FhirWebserviceClientProvider clientProvider = clientProvider(); + EndpointProvider endpointProvider = new EndpointProviderImpl(clientProvider, + environmentConfig.getFhirServerBaseUrl()); + FhirContext fhirContext = fhirContext(); + MailService mailService = new MailServiceImpl(bpeMailService); + ObjectMapper objectMapper = objectMapper(); + OrganizationProvider organizationProvider = new OrganizationProviderImpl(clientProvider, + environmentConfig.getFhirServerBaseUrl()); + + ProcessAuthorizationHelper processAuthorizationHelper = new ProcessAuthorizationHelperImpl(); + QuestionnaireResponseHelper questionnaireResponseHelper = new QuestionnaireResponseHelperImpl( + environmentConfig.getFhirServerBaseUrl()); + ReadAccessHelper readAccessHelper = new ReadAccessHelperImpl(); + TaskHelper taskHelper = new TaskHelperImpl(environmentConfig.getFhirServerBaseUrl()); + + return new ProcessPluginApiImpl(proxyConfig, endpointProvider, fhirContext, clientProvider, mailService, + objectMapper, organizationProvider, processAuthorizationHelper, questionnaireResponseHelper, + readAccessHelper, taskHelper); + } + + @Bean + public ReferenceExtractor referenceExtractor() + { + return new ReferenceExtractorImpl(); + } + + @Bean + public ReferenceCleaner referenceCleaner() + { + return new ReferenceCleanerImpl(referenceExtractor()); + } + + @Bean + public FhirWebserviceClientProvider clientProvider() + { + char[] keyStorePassword = UUID.randomUUID().toString().toCharArray(); + + return new FhirWebserviceClientProviderImpl(fhirContext(), environmentConfig.getFhirServerBaseUrl(), + environmentConfig.getWebserviceClientLocalReadTimeout(), + environmentConfig.getWebserviceClientLocalConnectTimeout(), + environmentConfig.getWebserviceClientLocalVerbose(), environmentConfig.getWebserviceTrustStore(), + environmentConfig.getWebserviceKeyStore(keyStorePassword), keyStorePassword, + environmentConfig.getWebserviceClientRemoteReadTimeout(), + environmentConfig.getWebserviceClientRemoteConnectTimeout(), + environmentConfig.getWebserviceClientRemoteVerbose(), this.proxyConfig, buildInfoProvider, + referenceCleaner()); + } + + @Bean + public FhirContext fhirContext() + { + FhirContext context = FhirContext.forR4(); + HapiLocalizer localizer = new HapiLocalizer() + { + @Override + public Locale getLocale() + { + return Locale.ROOT; + } + }; + context.setLocalizer(localizer); + return context; + } + + @Bean + public ObjectMapper objectMapper() + { + return ObjectMapperFactory.createObjectMapper(fhirContext()); + } + + @Bean + public FhirResourceSerializer fhirResourceSerializer() + { + return new FhirResourceSerializer(fhirContext()); + } + + @Bean + public FhirResourcesListSerializer fhirResourcesListSerializer() + { + return new FhirResourcesListSerializer(objectMapper()); + } + + @Bean + public TargetSerializer targetSerializer() + { + return new TargetSerializer(objectMapper()); + } + + @Bean + public TargetsSerializer targetsSerializer() + { + return new TargetsSerializer(objectMapper()); + } + + @Bean + public ExecutionListener startListener() + { + return new StartListener(environmentConfig.getFhirServerBaseUrl(), VariablesImpl::new); + } + + @Bean + public ExecutionListener endListener() + { + return new EndListener(environmentConfig.getFhirServerBaseUrl(), VariablesImpl::new, + clientProvider().getLocalWebserviceClient()); + } + + @Bean + public ExecutionListener continueListener() + { + return new ContinueListener(environmentConfig.getFhirServerBaseUrl(), VariablesImpl::new); + } + + @Bean + public ListenerFactory listenerFactory() + { + return new ListenerFactoryImpl(ProcessPluginFactoryImpl.API_VERSION, startListener(), endListener(), + continueListener()); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceJacksonDeserializer.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceJacksonDeserializer.java new file mode 100644 index 000000000..3d992dce4 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceJacksonDeserializer.java @@ -0,0 +1,39 @@ +package dev.dsf.bpe.v2.variables; + +import java.io.IOException; +import java.util.Objects; + +import org.hl7.fhir.r4.model.Resource; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; + +public class FhirResourceJacksonDeserializer extends JsonDeserializer +{ + private final FhirContext fhirContext; + + public FhirResourceJacksonDeserializer(FhirContext fhirContext) + { + this.fhirContext = Objects.requireNonNull(fhirContext, "fhirContext"); + } + + @Override + public Resource deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException + { + String string = p.readValueAsTree().toString(); + return (Resource) newJsonParser().parseResource(string); + } + + private IParser newJsonParser() + { + IParser p = fhirContext.newJsonParser(); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + return p; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceJacksonSerializer.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceJacksonSerializer.java new file mode 100644 index 000000000..c08cb05a9 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceJacksonSerializer.java @@ -0,0 +1,40 @@ +package dev.dsf.bpe.v2.variables; + +import java.io.IOException; +import java.util.Objects; + +import org.hl7.fhir.r4.model.Resource; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; + +public class FhirResourceJacksonSerializer extends JsonSerializer +{ + private final FhirContext fhirContext; + + public FhirResourceJacksonSerializer(FhirContext fhirContext) + { + this.fhirContext = Objects.requireNonNull(fhirContext, "fhirContext"); + } + + @Override + public void serialize(Resource value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonGenerationException + { + String text = newJsonParser().encodeResourceToString(value); + jgen.writeRawValue(text); + } + + private IParser newJsonParser() + { + IParser p = fhirContext.newJsonParser(); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + return p; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceSerializer.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceSerializer.java new file mode 100644 index 000000000..e6646d7b4 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceSerializer.java @@ -0,0 +1,106 @@ +package dev.dsf.bpe.v2.variables; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import org.camunda.bpm.engine.impl.variable.serializer.PrimitiveValueSerializer; +import org.camunda.bpm.engine.impl.variable.serializer.ValueFields; +import org.camunda.bpm.engine.variable.impl.value.UntypedValueImpl; +import org.hl7.fhir.r4.model.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.parser.IParser; +import dev.dsf.bpe.v2.variables.FhirResourceValues.FhirResourceValue; + +public class FhirResourceSerializer extends PrimitiveValueSerializer implements InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(FhirResourceSerializer.class); + + private final FhirContext fhirContext; + + public FhirResourceSerializer(FhirContext fhirContext) + { + super(FhirResourceValues.VALUE_TYPE); + + this.fhirContext = fhirContext; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(fhirContext, "fhirContext"); + } + + @Override + public void writeValue(FhirResourceValue value, ValueFields valueFields) + { + Resource resource = value.getValue(); + try + { + if (resource != null) + { + String s = newJsonParser().encodeResourceToString(resource); + valueFields.setTextValue(resource.getClass().getName()); + valueFields.setByteArrayValue(s.getBytes(StandardCharsets.UTF_8)); + } + } + catch (DataFormatException e) + { + throw new RuntimeException(e); + } + } + + private IParser newJsonParser() + { + IParser p = fhirContext.newJsonParser(); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + return p; + } + + @Override + public FhirResourceValue convertToTypedValue(UntypedValueImpl untypedValue) + { + return FhirResourceValues.create((Resource) untypedValue.getValue()); + } + + @Override + public FhirResourceValue readValue(ValueFields valueFields, boolean asTransientValue) + { + String className = valueFields.getTextValue(); + byte[] bytes = valueFields.getByteArrayValue(); + + try + { + Resource resource; + if (className != null) + { + @SuppressWarnings("unchecked") + Class clazz = (Class) Class.forName(className); + resource = newJsonParser().parseResource(clazz, new ByteArrayInputStream(bytes)); + } + else + { + logger.warn("ClassName from DB null, trying to parse FHIR resource without type information"); + resource = (Resource) newJsonParser().parseResource(new ByteArrayInputStream(bytes)); + } + + return FhirResourceValues.create(resource); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException(e); + } + } + + @Override + public String getName() + { + return "v2/" + super.getName(); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceValues.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceValues.java new file mode 100644 index 000000000..5a1e8d4a0 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourceValues.java @@ -0,0 +1,54 @@ +package dev.dsf.bpe.v2.variables; + +import java.util.Map; + +import org.camunda.bpm.engine.variable.impl.type.PrimitiveValueTypeImpl; +import org.camunda.bpm.engine.variable.impl.value.PrimitiveTypeValueImpl; +import org.camunda.bpm.engine.variable.type.PrimitiveValueType; +import org.camunda.bpm.engine.variable.value.PrimitiveValue; +import org.camunda.bpm.engine.variable.value.TypedValue; +import org.hl7.fhir.r4.model.Resource; + +public final class FhirResourceValues +{ + public interface FhirResourceValue extends PrimitiveValue + { + } + + private static class FhirResourceValueImpl extends PrimitiveTypeValueImpl implements FhirResourceValue + { + private static final long serialVersionUID = 1L; + + public FhirResourceValueImpl(Resource value, PrimitiveValueType type) + { + super(value, type); + } + } + + public static class FhirResourceTypeImpl extends PrimitiveValueTypeImpl + { + private static final long serialVersionUID = 1L; + + private FhirResourceTypeImpl() + { + super(Resource.class); + } + + @Override + public TypedValue createValue(Object value, Map valueInfo) + { + return new FhirResourceValueImpl((Resource) value, VALUE_TYPE); + } + } + + public static final PrimitiveValueType VALUE_TYPE = new FhirResourceTypeImpl(); + + private FhirResourceValues() + { + } + + public static FhirResourceValue create(Resource resource) + { + return new FhirResourceValueImpl(resource, VALUE_TYPE); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourcesList.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourcesList.java new file mode 100644 index 000000000..0bc87a9c9 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourcesList.java @@ -0,0 +1,51 @@ +package dev.dsf.bpe.v2.variables; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.hl7.fhir.r4.model.Resource; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class FhirResourcesList +{ + private final List resources = new ArrayList<>(); + + @JsonCreator + public FhirResourcesList(@JsonProperty("resources") Collection resources) + { + if (resources != null) + this.resources.addAll(resources); + } + + public FhirResourcesList(Resource... resources) + { + this(Arrays.asList(resources)); + } + + @JsonProperty("resources") + public List getResources() + { + return Collections.unmodifiableList(resources); + } + + @SuppressWarnings("unchecked") + @JsonIgnore + public List getResourcesAndCast() + { + return (List) getResources(); + } + + @Override + public String toString() + { + return "FhirResourcesList" + resources.stream().map(r -> r.getIdElement().toUnqualified().getValue()) + .collect(Collectors.joining(", ", "[", "]")); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourcesListSerializer.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourcesListSerializer.java new file mode 100644 index 000000000..a1291f314 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourcesListSerializer.java @@ -0,0 +1,86 @@ +package dev.dsf.bpe.v2.variables; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Objects; + +import org.camunda.bpm.engine.impl.variable.serializer.PrimitiveValueSerializer; +import org.camunda.bpm.engine.impl.variable.serializer.ValueFields; +import org.camunda.bpm.engine.variable.impl.value.UntypedValueImpl; +import org.springframework.beans.factory.InitializingBean; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import dev.dsf.bpe.v2.variables.FhirResourcesListValues.FhirResourcesListValue; + +public class FhirResourcesListSerializer extends PrimitiveValueSerializer + implements InitializingBean +{ + private final ObjectMapper objectMapper; + + public FhirResourcesListSerializer(ObjectMapper objectMapper) + { + super(FhirResourcesListValues.VALUE_TYPE); + + this.objectMapper = objectMapper; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(objectMapper, "objectMapper"); + } + + @Override + public void writeValue(FhirResourcesListValue value, ValueFields valueFields) + { + FhirResourcesList resource = value.getValue(); + try + { + if (resource != null) + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + objectMapper.writeValue(out, resource); + + valueFields.setTextValue(resource.getClass().getName()); + valueFields.setByteArrayValue(out.toByteArray()); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + @Override + public FhirResourcesListValue convertToTypedValue(UntypedValueImpl untypedValue) + { + return FhirResourcesListValues.create((FhirResourcesList) untypedValue.getValue()); + } + + @Override + public FhirResourcesListValue readValue(ValueFields valueFields, boolean asTransientValue) + { + String className = valueFields.getTextValue(); + byte[] bytes = valueFields.getByteArrayValue(); + + try + { + @SuppressWarnings("unchecked") + Class clazz = (Class) Class.forName(className); + FhirResourcesList resource = objectMapper.readValue(bytes, clazz); + + return FhirResourcesListValues.create(resource); + } + catch (ClassNotFoundException | IOException e) + { + throw new RuntimeException(e); + } + } + + @Override + public String getName() + { + return "v2/" + super.getName(); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourcesListValues.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourcesListValues.java new file mode 100644 index 000000000..29ba2cf05 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/FhirResourcesListValues.java @@ -0,0 +1,72 @@ +package dev.dsf.bpe.v2.variables; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.camunda.bpm.engine.variable.impl.type.PrimitiveValueTypeImpl; +import org.camunda.bpm.engine.variable.impl.value.PrimitiveTypeValueImpl; +import org.camunda.bpm.engine.variable.type.PrimitiveValueType; +import org.camunda.bpm.engine.variable.value.PrimitiveValue; +import org.camunda.bpm.engine.variable.value.TypedValue; +import org.hl7.fhir.r4.model.Resource; + +public final class FhirResourcesListValues +{ + public interface FhirResourcesListValue extends PrimitiveValue + { + @SuppressWarnings("unchecked") + default List getFhirResources() + { + return (List) getValue().getResources(); + } + } + + private static class FhirResourcesListValueImpl extends PrimitiveTypeValueImpl + implements FhirResourcesListValue + { + private static final long serialVersionUID = 1L; + + public FhirResourcesListValueImpl(FhirResourcesList value, PrimitiveValueType type) + { + super(value, type); + } + } + + public static class FhirResourcesListTypeImpl extends PrimitiveValueTypeImpl + { + private static final long serialVersionUID = 1L; + + private FhirResourcesListTypeImpl() + { + super(FhirResourcesList.class); + } + + @Override + public TypedValue createValue(Object value, Map valueInfo) + { + return new FhirResourcesListValueImpl((FhirResourcesList) value, VALUE_TYPE); + } + } + + public static final PrimitiveValueType VALUE_TYPE = new FhirResourcesListTypeImpl(); + + private FhirResourcesListValues() + { + } + + public static FhirResourcesListValue create(Resource... resources) + { + return new FhirResourcesListValueImpl(new FhirResourcesList(resources), VALUE_TYPE); + } + + public static FhirResourcesListValue create(Collection resources) + { + return new FhirResourcesListValueImpl(new FhirResourcesList(resources), VALUE_TYPE); + } + + public static FhirResourcesListValue create(FhirResourcesList value) + { + return new FhirResourcesListValueImpl(value, VALUE_TYPE); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/KeyDeserializer.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/KeyDeserializer.java new file mode 100644 index 000000000..f917783fa --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/KeyDeserializer.java @@ -0,0 +1,27 @@ +package dev.dsf.bpe.v2.variables; + +import java.io.IOException; +import java.security.Key; + +import javax.crypto.spec.SecretKeySpec; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.node.TextNode; + +public class KeyDeserializer extends JsonDeserializer +{ + @Override + public Key deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException + { + TreeNode node = p.getCodec().readTree(p); + + String algorithm = ((TextNode) node.get("algorithm")).textValue(); + byte[] value = ((TextNode) node.get("value")).binaryValue(); + + return new SecretKeySpec(value, algorithm); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/KeySerializer.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/KeySerializer.java new file mode 100644 index 000000000..7bd4407f3 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/KeySerializer.java @@ -0,0 +1,24 @@ +package dev.dsf.bpe.v2.variables; + +import java.io.IOException; +import java.security.Key; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +public class KeySerializer extends JsonSerializer +{ + @Override + public void serialize(Key value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonGenerationException + { + jgen.writeStartObject(); + jgen.writeFieldName("algorithm"); + jgen.writeString(value.getAlgorithm()); + jgen.writeFieldName("value"); + jgen.writeBinary(value.getEncoded()); + jgen.writeEndObject(); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/ObjectMapperFactory.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/ObjectMapperFactory.java new file mode 100644 index 000000000..8b1a6f27a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/ObjectMapperFactory.java @@ -0,0 +1,32 @@ +package dev.dsf.bpe.v2.variables; + +import org.hl7.fhir.r4.model.Resource; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import ca.uhn.fhir.context.FhirContext; + +public class ObjectMapperFactory +{ + private ObjectMapperFactory() + { + } + + public static ObjectMapper createObjectMapper(FhirContext fhirContext) + { + return JsonMapper.builder().serializationInclusion(Include.NON_NULL).serializationInclusion(Include.NON_EMPTY) + .addModule(fhirModule(fhirContext)).disable(MapperFeature.AUTO_DETECT_CREATORS) + .disable(MapperFeature.AUTO_DETECT_FIELDS).disable(MapperFeature.AUTO_DETECT_GETTERS) + .disable(MapperFeature.AUTO_DETECT_IS_GETTERS).disable(MapperFeature.AUTO_DETECT_SETTERS).build(); + } + + public static SimpleModule fhirModule(FhirContext fhirContext) + { + return new SimpleModule().addSerializer(Resource.class, new FhirResourceJacksonSerializer(fhirContext)) + .addDeserializer(Resource.class, new FhirResourceJacksonDeserializer(fhirContext)); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetImpl.java new file mode 100644 index 000000000..f1ccf6444 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetImpl.java @@ -0,0 +1,58 @@ +package dev.dsf.bpe.v2.variables; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TargetImpl implements Target +{ + private final String organizationIdentifierValue; + private final String endpointIdentifierValue; + private final String endpointUrl; + private final String correlationKey; + + @JsonCreator + public TargetImpl(@JsonProperty("organizationIdentifierValue") String organizationIdentifierValue, + @JsonProperty("endpointIdentifierValue") String endpointIdentifierValue, + @JsonProperty("endpointUrl") String endpointUrl, @JsonProperty("correlationKey") String correlationKey) + { + this.organizationIdentifierValue = organizationIdentifierValue; + this.endpointIdentifierValue = endpointIdentifierValue; + this.endpointUrl = endpointUrl; + this.correlationKey = correlationKey; + } + + @Override + @JsonProperty("organizationIdentifierValue") + public String getOrganizationIdentifierValue() + { + return organizationIdentifierValue; + } + + @Override + @JsonProperty("endpointIdentifierValue") + public String getEndpointIdentifierValue() + { + return endpointIdentifierValue; + } + + @Override + @JsonProperty("endpointUrl") + public String getEndpointUrl() + { + return endpointUrl; + } + + @Override + @JsonProperty("correlationKey") + public String getCorrelationKey() + { + return correlationKey; + } + + @Override + public String toString() + { + return "TargetImpl [organizationIdentifierValue=" + organizationIdentifierValue + ", endpointIdentifierValue=" + + endpointIdentifierValue + ", endpointUrl=" + endpointUrl + ", correlationKey=" + correlationKey + "]"; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetSerializer.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetSerializer.java new file mode 100644 index 000000000..d06286691 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetSerializer.java @@ -0,0 +1,76 @@ +package dev.dsf.bpe.v2.variables; + +import java.io.IOException; +import java.util.Objects; + +import org.camunda.bpm.engine.impl.variable.serializer.PrimitiveValueSerializer; +import org.camunda.bpm.engine.impl.variable.serializer.ValueFields; +import org.camunda.bpm.engine.variable.impl.value.UntypedValueImpl; +import org.springframework.beans.factory.InitializingBean; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import dev.dsf.bpe.v2.variables.TargetValues.TargetValue; + +public class TargetSerializer extends PrimitiveValueSerializer implements InitializingBean +{ + private final ObjectMapper objectMapper; + + public TargetSerializer(ObjectMapper objectMapper) + { + super(TargetValues.VALUE_TYPE); + + this.objectMapper = objectMapper; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(objectMapper, "objectMapper"); + } + + @Override + public void writeValue(TargetValue value, ValueFields valueFields) + { + Target target = value.getValue(); + try + { + if (target != null) + valueFields.setByteArrayValue(objectMapper.writeValueAsBytes(target)); + } + catch (JsonProcessingException e) + { + throw new RuntimeException(e); + } + } + + @Override + public TargetValue convertToTypedValue(UntypedValueImpl untypedValue) + { + return TargetValues.create((TargetImpl) untypedValue.getValue()); + } + + @Override + public TargetValue readValue(ValueFields valueFields, boolean asTransientValue) + { + byte[] bytes = valueFields.getByteArrayValue(); + + try + { + TargetImpl target = (bytes == null || bytes.length <= 0) ? null + : objectMapper.readValue(bytes, TargetImpl.class); + return TargetValues.create(target); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + @Override + public String getName() + { + return "v2/" + super.getName(); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetValues.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetValues.java new file mode 100644 index 000000000..4b1670abe --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetValues.java @@ -0,0 +1,53 @@ +package dev.dsf.bpe.v2.variables; + +import java.util.Map; + +import org.camunda.bpm.engine.variable.impl.type.PrimitiveValueTypeImpl; +import org.camunda.bpm.engine.variable.impl.value.PrimitiveTypeValueImpl; +import org.camunda.bpm.engine.variable.type.PrimitiveValueType; +import org.camunda.bpm.engine.variable.value.PrimitiveValue; +import org.camunda.bpm.engine.variable.value.TypedValue; + +public final class TargetValues +{ + public interface TargetValue extends PrimitiveValue + { + } + + private static class TargetValueImpl extends PrimitiveTypeValueImpl implements TargetValue + { + private static final long serialVersionUID = 1L; + + public TargetValueImpl(TargetImpl value, PrimitiveValueType type) + { + super(value, type); + } + } + + public static class TargetValueTypeImpl extends PrimitiveValueTypeImpl + { + private static final long serialVersionUID = 1L; + + private TargetValueTypeImpl() + { + super(TargetImpl.class); + } + + @Override + public TypedValue createValue(Object value, Map valueInfo) + { + return new TargetValueImpl((TargetImpl) value, VALUE_TYPE); + } + } + + public static final PrimitiveValueType VALUE_TYPE = new TargetValueTypeImpl(); + + private TargetValues() + { + } + + public static TargetValue create(TargetImpl value) + { + return new TargetValueImpl(value, VALUE_TYPE); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetsImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetsImpl.java new file mode 100644 index 000000000..a46e153d2 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetsImpl.java @@ -0,0 +1,74 @@ +package dev.dsf.bpe.v2.variables; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TargetsImpl implements Targets +{ + private final List entries = new ArrayList<>(); + + @JsonCreator + public TargetsImpl(@JsonProperty("entries") List targets) + { + if (targets != null) + this.entries.addAll(targets); + } + + @JsonProperty("entries") + @Override + public List getEntries() + { + return Collections.unmodifiableList(entries); + } + + @Override + public Targets removeByEndpointIdentifierValue(Target target) + { + if (target == null) + return new TargetsImpl(entries); + + return removeByEndpointIdentifierValue(target.getEndpointIdentifierValue()); + } + + @Override + public Targets removeByEndpointIdentifierValue(String targetEndpointIdentifierValue) + { + if (targetEndpointIdentifierValue == null) + return new TargetsImpl(entries); + + return new TargetsImpl( + entries.stream().filter(t -> !targetEndpointIdentifierValue.equals(t.getEndpointIdentifierValue())) + .collect(Collectors.toList())); + } + + @Override + public Targets removeAllByEndpointIdentifierValue(Collection targetEndpointIdentifierValues) + { + if (targetEndpointIdentifierValues == null || targetEndpointIdentifierValues.isEmpty()) + return new TargetsImpl(entries); + + return new TargetsImpl( + entries.stream().filter(t -> !targetEndpointIdentifierValues.contains(t.getEndpointIdentifierValue())) + .collect(Collectors.toList())); + } + + @JsonIgnore + @Override + public boolean isEmpty() + { + return entries.isEmpty(); + } + + @Override + public String toString() + { + return "TargetsImpl [entries=" + entries + "]"; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetsSerializer.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetsSerializer.java new file mode 100644 index 000000000..175a859c0 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetsSerializer.java @@ -0,0 +1,76 @@ +package dev.dsf.bpe.v2.variables; + +import java.io.IOException; +import java.util.Objects; + +import org.camunda.bpm.engine.impl.variable.serializer.PrimitiveValueSerializer; +import org.camunda.bpm.engine.impl.variable.serializer.ValueFields; +import org.camunda.bpm.engine.variable.impl.value.UntypedValueImpl; +import org.springframework.beans.factory.InitializingBean; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import dev.dsf.bpe.v2.variables.TargetsValues.TargetsValue; + +public class TargetsSerializer extends PrimitiveValueSerializer implements InitializingBean +{ + private final ObjectMapper objectMapper; + + public TargetsSerializer(ObjectMapper objectMapper) + { + super(TargetsValues.VALUE_TYPE); + + this.objectMapper = objectMapper; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(objectMapper, "objectMapper"); + } + + @Override + public void writeValue(TargetsValue value, ValueFields valueFields) + { + Targets targets = value.getValue(); + try + { + if (targets != null) + valueFields.setByteArrayValue(objectMapper.writeValueAsBytes(targets)); + } + catch (JsonProcessingException e) + { + throw new RuntimeException(e); + } + } + + @Override + public TargetsValue convertToTypedValue(UntypedValueImpl untypedValue) + { + return TargetsValues.create((TargetsImpl) untypedValue.getValue()); + } + + @Override + public TargetsValue readValue(ValueFields valueFields, boolean asTransientValue) + { + byte[] bytes = valueFields.getByteArrayValue(); + + try + { + TargetsImpl targets = (bytes == null || bytes.length <= 0) ? null + : objectMapper.readValue(bytes, TargetsImpl.class); + return TargetsValues.create(targets); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + @Override + public String getName() + { + return "v2/" + super.getName(); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetsValues.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetsValues.java new file mode 100644 index 000000000..c523c98ab --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/TargetsValues.java @@ -0,0 +1,53 @@ +package dev.dsf.bpe.v2.variables; + +import java.util.Map; + +import org.camunda.bpm.engine.variable.impl.type.PrimitiveValueTypeImpl; +import org.camunda.bpm.engine.variable.impl.value.PrimitiveTypeValueImpl; +import org.camunda.bpm.engine.variable.type.PrimitiveValueType; +import org.camunda.bpm.engine.variable.value.PrimitiveValue; +import org.camunda.bpm.engine.variable.value.TypedValue; + +public final class TargetsValues +{ + public interface TargetsValue extends PrimitiveValue + { + } + + private static class TargetsValueImpl extends PrimitiveTypeValueImpl implements TargetsValue + { + private static final long serialVersionUID = 1L; + + public TargetsValueImpl(TargetsImpl value, PrimitiveValueType type) + { + super(value, type); + } + } + + public static class TargetsValueTypeImpl extends PrimitiveValueTypeImpl + { + private static final long serialVersionUID = 1L; + + private TargetsValueTypeImpl() + { + super(TargetsImpl.class); + } + + @Override + public TypedValue createValue(Object value, Map valueInfo) + { + return new TargetsValueImpl((TargetsImpl) value, VALUE_TYPE); + } + } + + public static final PrimitiveValueType VALUE_TYPE = new TargetsValueTypeImpl(); + + private TargetsValues() + { + } + + public static TargetsValue create(TargetsImpl value) + { + return new TargetsValueImpl(value, VALUE_TYPE); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java new file mode 100644 index 000000000..59b4000f4 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/java/dev/dsf/bpe/v2/variables/VariablesImpl.java @@ -0,0 +1,316 @@ +package dev.dsf.bpe.v2.variables; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.variable.value.TypedValue; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.Task; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.dsf.bpe.api.Constants; +import dev.dsf.bpe.v2.constants.BpmnExecutionVariables; +import dev.dsf.bpe.v2.listener.ListenerVariables; +import dev.dsf.bpe.v2.variables.FhirResourceValues.FhirResourceValue; +import dev.dsf.bpe.v2.variables.FhirResourcesListValues.FhirResourcesListValue; +import dev.dsf.bpe.v2.variables.TargetValues.TargetValue; + +public class VariablesImpl implements Variables, ListenerVariables +{ + private static final Logger logger = LoggerFactory.getLogger(VariablesImpl.class); + + private static final String TASKS_PREFIX = VariablesImpl.class.getName() + ".tasks."; + private static final String START_TASK = VariablesImpl.class.getName() + ".startTask"; + + private static final class DistinctTask + { + final Task task; + + DistinctTask(Task task) + { + this.task = task; + } + + Task getTask() + { + return task; + } + + @Override + public boolean equals(Object otherO) + { + if (otherO instanceof DistinctTask other) + return Objects.equals(other.task.getIdElement().getIdPart(), task.getIdElement().getIdPart()); + else + return false; + } + + @Override + public int hashCode() + { + return task.getIdElement().getIdPart().hashCode(); + } + } + + private final DelegateExecution execution; + + public VariablesImpl(DelegateExecution execution) + { + this.execution = Objects.requireNonNull(execution, "execution"); + } + + @Override + public void setAlternativeBusinessKey(String alternativeBusinessKey) + { + execution.setVariable(BpmnExecutionVariables.ALTERNATIVE_BUSINESS_KEY, alternativeBusinessKey); + } + + @Override + public Target createTarget(String organizationIdentifierValue, String endpointIdentifierValue, + String endpointAddress, String correlationKey) + { + Objects.requireNonNull(organizationIdentifierValue, "organizationIdentifierValue"); + Objects.requireNonNull(endpointIdentifierValue, "endpointIdentifierValue"); + Objects.requireNonNull(endpointAddress, "endpointAddress"); + + return new TargetImpl(organizationIdentifierValue, endpointIdentifierValue, endpointAddress, correlationKey); + } + + @Override + public void setTarget(Target target) throws IllegalArgumentException + { + if (target == null) + { + execution.setVariable(BpmnExecutionVariables.TARGET, null); + return; + } + + if (!(target instanceof TargetImpl)) + throw new IllegalArgumentException( + "Given target implementing class " + target.getClass().getName() + " not supported"); + + TargetValue variable = TargetValues.create((TargetImpl) target); + execution.setVariable(BpmnExecutionVariables.TARGET, variable); + } + + @Override + public Target getTarget() + { + return (TargetImpl) execution.getVariable(BpmnExecutionVariables.TARGET); + } + + @SuppressWarnings("unchecked") + @Override + public Targets createTargets(List targets) + { + if (targets == null) + return new TargetsImpl(Collections.emptyList()); + + Optional firstNonMatch = targets.stream().filter(t -> !(t instanceof TargetImpl)).findFirst(); + if (firstNonMatch.isPresent()) + throw new IllegalArgumentException("Target implementing class " + firstNonMatch.get().getClass().getName() + + " (in given List) not supported"); + + return new TargetsImpl((List) targets); + } + + @Override + public void setTargets(Targets targets) throws IllegalArgumentException + { + if (targets == null) + execution.setVariable(BpmnExecutionVariables.TARGETS, null); + + else if (targets instanceof TargetsImpl t) + execution.setVariable(BpmnExecutionVariables.TARGETS, TargetsValues.create(t)); + + else + throw new IllegalArgumentException( + "Given targets implementing class " + targets.getClass().getName() + " not supported"); + } + + @Override + public Targets getTargets() + { + return (Targets) execution.getVariable(BpmnExecutionVariables.TARGETS); + } + + @Override + public void setResourceList(String variableName, List resources) + { + FhirResourcesListValue variable = resources == null ? null : FhirResourcesListValues.create(resources); + execution.setVariable(variableName, variable); + } + + @Override + public List getResourceList(String variableName) + { + FhirResourcesList list = (FhirResourcesList) execution.getVariable(variableName); + return list != null ? list.getResourcesAndCast() : null; + } + + private List getResourceListOrDefault(String variableName, List defaultList) + { + List list = getResourceList(variableName); + return list != null ? list : defaultList; + } + + @Override + public void setResource(String variableName, Resource resource) + { + FhirResourceValue variable = resource == null ? null : FhirResourceValues.create(resource); + execution.setVariable(variableName, variable); + } + + @Override + @SuppressWarnings("unchecked") + public R getResource(String variableName) + { + Resource resource = (Resource) execution.getVariable(variableName); + return (R) resource; + } + + @Override + public Task getStartTask() + { + logger.trace("getStartTask - parentActivityInstanceId: {}, parentId: {}", + execution.getParentActivityInstanceId(), execution.getParentId()); + + return getResource(START_TASK); + } + + @Override + public Task getLatestTask() + { + logger.trace("getLatestTask - parentActivityInstanceId: {}, parentId: {}", + execution.getParentActivityInstanceId(), execution.getParentId()); + + List tasks = getCurrentTasks(); + return tasks == null || tasks.isEmpty() ? null : tasks.get(tasks.size() - 1); + } + + @Override + public List getTasks() + { + logger.trace("getTasks - parentActivityInstanceId: {}, parentId: {}", execution.getParentActivityInstanceId(), + execution.getParentId()); + + List tasks = Stream + .concat(Stream.of(getStartTask()), + execution.getVariables().keySet().stream().filter(k -> k.startsWith(TASKS_PREFIX)) + .map(this::getResourceList).flatMap(List::stream).filter(r -> r instanceof Task) + .map(r -> (Task) r)) + .filter(t -> t != null).map(DistinctTask::new).distinct().map(DistinctTask::getTask).toList(); + + return Collections.unmodifiableList(tasks); + } + + @Override + public List getCurrentTasks() + { + logger.trace("getCurrentTasks - parentActivityInstanceId: {}, parentId: {}", + execution.getParentActivityInstanceId(), execution.getParentId()); + + Stream start = execution.getParentId() == null ? Stream.of(getStartTask()) : Stream.empty(); + Stream current = getResourceListOrDefault(TASKS_PREFIX + execution.getParentActivityInstanceId(), + Collections. emptyList()).stream(); + + return Collections.unmodifiableList(Stream.concat(start, current).toList()); + } + + @Override + public void updateTask(Task task) + { + logger.trace("updateTask - Task.id: {}", task == null ? "null" : task.getIdElement().getIdPart()); + + if (task != null) + { + if (getStartTask() != null + && Objects.equals(getStartTask().getIdElement().getIdPart(), task.getIdElement().getIdPart())) + setResource(START_TASK, task); + else + { + String instanceId = execution.getParentActivityInstanceId(); + List tasks = getResourceListOrDefault(TASKS_PREFIX + instanceId, Collections.emptyList()); + + if (tasks.stream().anyMatch(t -> t.getIdElement().getIdPart().equals(task.getIdElement().getIdPart()))) + setResourceList(TASKS_PREFIX + instanceId, tasks); + else + logger.warn("Given task {} not part of tasks list '{}', ignoring task", + task.getIdElement().getIdPart(), instanceId); + } + } + else + logger.warn("Given task is null"); + } + + @Override + public QuestionnaireResponse getLatestReceivedQuestionnaireResponse() + { + return (QuestionnaireResponse) getResource(Constants.QUESTIONNAIRE_RESPONSE_VARIABLE); + } + + @Override + public void setVariable(String variableName, TypedValue value) + { + Objects.requireNonNull(variableName, "variableName"); + + execution.setVariable(variableName, value); + } + + @Override + public Object getVariable(String variableName) + { + Objects.requireNonNull(variableName, "variableName"); + + return execution.getVariable(variableName); + } + + @Override + public void onStart(Task task) + { + logger.trace("onStart - Task.id: {}", task == null ? "null" : task.getIdElement().getIdPart()); + + if (task != null) + setResource(START_TASK, task); + else + logger.warn("Given task is null"); + } + + @Override + public void onContinue(Task task) + { + logger.trace("onContinue - Task.id: {}", task == null ? "null" : task.getIdElement().getIdPart()); + + if (task != null) + { + String instanceId = execution.getParentActivityInstanceId(); + + List tasks = new ArrayList<>( + getResourceListOrDefault(TASKS_PREFIX + instanceId, Collections.emptyList())); + tasks.add(task); + + setResourceList(TASKS_PREFIX + instanceId, tasks); + } + else + logger.warn("Given task is null"); + } + + @Override + public void onEnd() + { + logger.trace("onEnd"); + + String instanceId = execution.getParentActivityInstanceId(); + List tasks = new ArrayList<>( + getResourceListOrDefault(TASKS_PREFIX + instanceId, Collections.emptyList())); + tasks.removeAll(getCurrentTasks()); + setResourceList(TASKS_PREFIX + instanceId, tasks); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/resources/META-INF/services/dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/resources/META-INF/services/dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder new file mode 100644 index 000000000..f6e76e46a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/main/resources/META-INF/services/dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder @@ -0,0 +1 @@ +dev.dsf.bpe.v2.plugin.ProcessPluginApiBuilderImpl \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/resources/log4j2.xml b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/resources/log4j2.xml new file mode 100644 index 000000000..d30bf4805 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2-impl/src/test/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2/pom.xml b/dsf-bpe/dsf-bpe-process-api-v2/pom.xml new file mode 100644 index 000000000..ca81616fa --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/pom.xml @@ -0,0 +1,39 @@ + + 4.0.0 + + dsf-bpe-process-api-v2 + + + dev.dsf + dsf-bpe-pom + 2.0.0-SNAPSHOT + + + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${hapi.fhir.version.v2} + + + org.camunda.bpm + camunda-engine + + + org.springframework + spring-context + + + com.fasterxml.jackson.core + jackson-databind + + + com.sun.mail + jakarta.mail + + + jakarta.ws.rs-api + jakarta.ws.rs + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/ProcessPluginApi.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/ProcessPluginApi.java new file mode 100644 index 000000000..4ce8212bb --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/ProcessPluginApi.java @@ -0,0 +1,52 @@ +package dev.dsf.bpe.v2; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import ca.uhn.fhir.context.FhirContext; +import dev.dsf.bpe.v2.config.ProxyConfig; +import dev.dsf.bpe.v2.service.EndpointProvider; +import dev.dsf.bpe.v2.service.FhirWebserviceClientProvider; +import dev.dsf.bpe.v2.service.MailService; +import dev.dsf.bpe.v2.service.OrganizationProvider; +import dev.dsf.bpe.v2.service.QuestionnaireResponseHelper; +import dev.dsf.bpe.v2.service.ReadAccessHelper; +import dev.dsf.bpe.v2.service.TaskHelper; +import dev.dsf.bpe.v2.service.process.ProcessAuthorizationHelper; +import dev.dsf.bpe.v2.variables.Variables; + +/** + * Gives access to services available to process plugins. This api and all services excepted {@link Variables} can be + * injected using {@link Autowired} into spring {@link Configuration} classes. + * + * @see ProcessPluginDefinition#getSpringConfigurations() + */ +public interface ProcessPluginApi +{ + ProxyConfig getProxyConfig(); + + EndpointProvider getEndpointProvider(); + + FhirContext getFhirContext(); + + FhirWebserviceClientProvider getFhirWebserviceClientProvider(); + + MailService getMailService(); + + ObjectMapper getObjectMapper(); + + OrganizationProvider getOrganizationProvider(); + + ProcessAuthorizationHelper getProcessAuthorizationHelper(); + + QuestionnaireResponseHelper getQuestionnaireResponseHelper(); + + ReadAccessHelper getReadAccessHelper(); + + TaskHelper getTaskHelper(); + + Variables getVariables(DelegateExecution execution); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/ProcessPluginDefinition.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/ProcessPluginDefinition.java new file mode 100644 index 000000000..e16cdaaf6 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/ProcessPluginDefinition.java @@ -0,0 +1,140 @@ +package dev.dsf.bpe.v2; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +import dev.dsf.bpe.v2.activity.AbstractServiceDelegate; +import dev.dsf.bpe.v2.activity.AbstractTaskMessageSend; +import dev.dsf.bpe.v2.activity.DefaultUserTaskListener; +import dev.dsf.bpe.v2.documentation.ProcessDocumentation; + +/** + * A provider configuration file named "dev.dsf.ProcessPluginDefinition" containing the canonical name of the class + * implementing this interface needs to be part of the process plugin at "/META-INF/services/". For more details on the + * content of the provider configuration file, see {@link ServiceLoader}. + */ +public interface ProcessPluginDefinition +{ + String RESOURCE_VERSION_PATTERN_STRING = "(?\\d+\\.\\d+)"; + String PLUGIN_VERSION_PATTERN_STRING = "(?" + 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 AbstractServiceDelegate + * @see AbstractTaskMessageSend + * @see DefaultUserTaskListener + * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE + */ + List> getSpringConfigurations(); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/ProcessPluginDeploymentListener.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/ProcessPluginDeploymentListener.java new file mode 100644 index 000000000..cb88f858b --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/ProcessPluginDeploymentListener.java @@ -0,0 +1,17 @@ +package dev.dsf.bpe.v2; + +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 ProcessPluginDeploymentListener +{ + void onProcessesDeployed(List processes); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/AbstractServiceDelegate.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/AbstractServiceDelegate.java new file mode 100644 index 000000000..888358100 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/AbstractServiceDelegate.java @@ -0,0 +1,157 @@ +package dev.dsf.bpe.v2.activity; + +import java.util.List; +import java.util.Objects; + +import org.camunda.bpm.engine.delegate.BpmnError; +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.delegate.JavaDelegate; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskOutputComponent; +import org.hl7.fhir.r4.model.Task.TaskStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import dev.dsf.bpe.v2.ProcessPluginApi; +import dev.dsf.bpe.v2.ProcessPluginDefinition; +import dev.dsf.bpe.v2.activity.AbstractServiceDelegate; +import dev.dsf.bpe.v2.constants.CodeSystems.BpmnMessage; +import dev.dsf.bpe.v2.variables.Variables; + +/** + * Abstract implementation of the {@link JavaDelegate} interface with added error handling and convenient access to + * process execution variables with the variables parameter of the + * {@link #doExecute(DelegateExecution, Variables)} method. + *

+ * Configure BPMN service tasks with an implementation of type 'Java class' with the fully qualified class name of the + * class extending this abstract implementation. + *

+ * Configure your service task implementation as a {@link Bean} in your spring {@link Configuration} class with scope + * "prototype". + * + * @see ProcessPluginDefinition#getSpringConfigurations() + */ +public abstract class AbstractServiceDelegate implements JavaDelegate, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractServiceDelegate.class); + + protected final ProcessPluginApi api; + + /** + * @param api + * not null + */ + public AbstractServiceDelegate(ProcessPluginApi api) + { + this.api = api; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(api, "api"); + } + + @Override + public final void execute(DelegateExecution execution) throws Exception + { + final Variables variables = api.getVariables(execution); + + try + { + logger.trace("Execution of task with id='{}'", execution.getCurrentActivityId()); + + doExecute(execution, variables); + } + // Error boundary event, do not stop process execution + catch (BpmnError error) + { + logger.debug("Error while executing service delegate {}", getClass().getName(), error); + logger.error( + "Process {} encountered error boundary event in step {} for task {}, error-code: {}, message: {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), + api.getTaskHelper().getLocalVersionlessAbsoluteUrl(variables.getStartTask()), error.getErrorCode(), + error.getMessage()); + + throw error; + } + // Not an error boundary event, stop process execution + catch (Exception exception) + { + logger.debug("Error while executing service delegate {}", getClass().getName(), exception); + logger.error("Process {} has fatal error in step {} for task {}, reason: {} - {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), + api.getTaskHelper().getLocalVersionlessAbsoluteUrl(variables.getStartTask()), + exception.getClass().getName(), exception.getMessage()); + + String errorMessage = "Process " + execution.getProcessDefinitionId() + " has fatal error in step " + + execution.getActivityInstanceId() + ", reason: " + exception.getMessage(); + + updateFailedIfInprogress(variables.getTasks(), errorMessage); + + // TODO evaluate throwing exception as alternative to stopping the process instance + execution.getProcessEngine().getRuntimeService().deleteProcessInstance(execution.getProcessInstanceId(), + exception.getMessage()); + } + } + + /** + * Implement this method to execute custom business logic within BPMN service tasks. + * + * @param execution + * Process instance information and variables + * @param variables + * DSF process variables + * @throws BpmnError + * Thrown when an error boundary event should be called + * @throws Exception + * Uncaught exceptions thrown by this method will result in Task status failed for all current + * in-progress Task resource with the exception message added as an error output. An exception + * (not {@link BpmnError}) thrown by this method will also result in the process instance stopping + * execution and being deleted. + */ + protected abstract void doExecute(DelegateExecution execution, Variables variables) throws BpmnError, Exception; + + private void updateFailedIfInprogress(List tasks, String errorMessage) + { + for (int i = tasks.size() - 1; i >= 0; i--) + { + Task task = tasks.get(i); + + if (TaskStatus.INPROGRESS.equals(task.getStatus())) + { + task.setStatus(Task.TaskStatus.FAILED); + task.addOutput(new TaskOutputComponent(new CodeableConcept(BpmnMessage.error()), + new StringType(errorMessage))); + updateAndHandleException(task); + } + else + { + logger.debug("Not updating Task {} with status: {}", + api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), task.getStatus()); + } + } + } + + private void updateAndHandleException(Task task) + { + try + { + logger.debug("Updating Task {}, new status: {}", api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), + task.getStatus().toCode()); + + api.getFhirWebserviceClientProvider().getLocalWebserviceClient().withMinimalReturn().update(task); + } + catch (Exception e) + { + logger.debug("Unable to update Task {}", api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), e); + logger.error("Unable to update Task {}: {} - {}", api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), + e.getClass().getName(), e.getMessage()); + } + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/AbstractTaskMessageSend.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/AbstractTaskMessageSend.java new file mode 100644 index 000000000..4b2cf0944 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/AbstractTaskMessageSend.java @@ -0,0 +1,521 @@ +package dev.dsf.bpe.v2.activity; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Stream; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.delegate.JavaDelegate; +import org.camunda.bpm.engine.impl.el.FixedValue; +import org.camunda.bpm.model.bpmn.instance.EndEvent; +import org.camunda.bpm.model.bpmn.instance.IntermediateThrowEvent; +import org.camunda.bpm.model.bpmn.instance.SendTask; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Meta; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.ParameterComponent; +import org.hl7.fhir.r4.model.Task.TaskIntent; +import org.hl7.fhir.r4.model.Task.TaskOutputComponent; +import org.hl7.fhir.r4.model.Task.TaskStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import dev.dsf.bpe.v2.ProcessPluginApi; +import dev.dsf.bpe.v2.ProcessPluginDefinition; +import dev.dsf.bpe.v2.client.FhirWebserviceClient; +import dev.dsf.bpe.v2.constants.BpmnExecutionVariables; +import dev.dsf.bpe.v2.constants.CodeSystems.BpmnMessage; +import dev.dsf.bpe.v2.constants.NamingSystems.OrganizationIdentifier; +import dev.dsf.bpe.v2.variables.Target; +import dev.dsf.bpe.v2.variables.Targets; +import dev.dsf.bpe.v2.variables.Variables; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response.StatusType; + +/** + * Base class for implementing BPMN message send tasks, intermediate message throw events and message end events using + * FHIR Task resources. Requires three String fields to be injected via BPMN: + *

    + *
  • instantiatesCanonical with the URL (including version) of the Activity to start or continue. + *
  • messageName with the with the BPMN message-name of the start event, intermediate message catch event or + * message receive task. + *
  • profile with the URL (including version) of the profile (StructureDefinition) that the Task resource used + * should conform to. + *
+ *

+ * Configure BPMN message send tasks, intermediate message throw events and message end event with an implementation of + * type 'Java class' with the fully qualified class name of the class extending this abstract implementation. + *

+ * Configure your service task implementation as a {@link Bean} in your spring {@link Configuration} class with scope + * "prototype". + * + * @see ProcessPluginDefinition#getSpringConfigurations() + */ +public abstract class AbstractTaskMessageSend implements JavaDelegate, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractTaskMessageSend.class); + + protected final ProcessPluginApi api; + + // set via field injection + private FixedValue instantiatesCanonical; + private FixedValue messageName; + private FixedValue profile; + + /** + * @param api + * not null + */ + public AbstractTaskMessageSend(ProcessPluginApi api) + { + this.api = api; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(api, "api"); + } + + /** + * @param instantiatesCanonical + * not null + * @deprecated only for process engine field injection + */ + @Deprecated + public final void setInstantiatesCanonical(FixedValue instantiatesCanonical) + { + this.instantiatesCanonical = instantiatesCanonical; + } + + /** + * Retrieves the instantiatesCanonical value used for Task resources send by this class via the injected field + * instantiatesCanonical. + *

+ * Override this method to use a different mechanism for retrieving the value for instantiatesCanonical. For + * example via a process variable. Note: A non empty value e.g 'disable' still needs to be injected in the BPMN file + * in order to comply with the validation performed during plugin loading. + * + * @param execution + * not null + * @param variables + * not null + * @return instantiatesCanonical value used for Task resources send by this class + */ + protected String getInstantiatesCanonical(DelegateExecution execution, Variables variables) + { + return instantiatesCanonical == null ? null : instantiatesCanonical.getExpressionText(); + } + + /** + * @param messageName + * not null + * @deprecated only for process engine field injection + */ + @Deprecated + public final void setMessageName(FixedValue messageName) + { + this.messageName = messageName; + } + + /** + * Retrieves the messageName value used for Task resources send by this class via the injected field + * messageName. + *

+ * Override this method to use a different mechanism for retrieving the value for messageName. For example via a + * process variable. Note: A non empty value e.g 'disable' still needs to be injected in the BPMN file in order to + * comply with the validation performed during plugin loading. + * + * @param execution + * not null + * @param variables + * not null + * @return messageName value used for Task resources send by this class + */ + protected String getMessageName(DelegateExecution execution, Variables variables) + { + return messageName == null ? null : messageName.getExpressionText(); + } + + /** + * @param profile + * not null + * @deprecated only for process engine field injection + */ + @Deprecated + public final void setProfile(FixedValue profile) + { + this.profile = profile; + } + + /** + * Retrieves the profile value used for Task resources send by this class via the injected field profile. + *

+ * Override this method to use a different mechanism for retrieving the value for profile. For example via a + * process variable. Note: A non empty value e.g 'disable' still needs to be injected in the BPMN file in order to + * comply with the validation performed during plugin loading. + * + * @param execution + * not null + * @param variables + * not null + * @return profile value used for Task resources send by this class + */ + protected String getProfile(DelegateExecution execution, Variables variables) + { + return profile == null ? null : profile.getExpressionText(); + } + + @Override + public final void execute(DelegateExecution execution) throws Exception + { + doExecute(execution, api.getVariables(execution)); + } + + protected void doExecute(DelegateExecution execution, Variables variables) throws Exception + { + final String instantiatesCanonical = getInstantiatesCanonical(execution, variables); + final String messageName = getMessageName(execution, variables); + final String profile = getProfile(execution, variables); + final String businessKey = execution.getBusinessKey(); + final Target target = variables.getTarget(); + + try + { + sendTask(execution, variables, target, instantiatesCanonical, messageName, businessKey, profile, + getAdditionalInputParameters(execution, variables)); + } + catch (Exception e) + { + String exceptionMessage = e.getMessage(); + if (e instanceof WebApplicationException w && (e.getMessage() == null || e.getMessage().isBlank())) + { + StatusType statusInfo = w.getResponse().getStatusInfo(); + exceptionMessage = statusInfo.getStatusCode() + " " + statusInfo.getReasonPhrase(); + } + + logger.debug("Error while sending Task", e); + String errorMessage = "Task " + instantiatesCanonical + " send failed [recipient: " + + target.getOrganizationIdentifierValue() + ", endpoint: " + target.getEndpointIdentifierValue() + + ", businessKey: " + businessKey + + (target.getCorrelationKey() == null ? "" : ", correlationKey: " + target.getCorrelationKey()) + + ", message: " + messageName + ", error: " + e.getClass().getName() + " - " + exceptionMessage + + "]"; + logger.warn(errorMessage); + + if (execution.getBpmnModelElementInstance() instanceof IntermediateThrowEvent) + handleIntermediateThrowEventError(execution, variables, e, errorMessage); + else if (execution.getBpmnModelElementInstance() instanceof EndEvent) + handleEndEventError(execution, variables, e, errorMessage); + else if (execution.getBpmnModelElementInstance() instanceof SendTask) + handleSendTaskError(execution, variables, e, errorMessage); + else + logger.warn("Error handling for {} not implemented", + execution.getBpmnModelElementInstance().getClass().getName()); + } + } + + protected void handleIntermediateThrowEventError(DelegateExecution execution, Variables variables, + Exception exception, String errorMessage) + { + logger.debug("Error while executing Task message send {}", getClass().getName(), exception); + logger.error("Process {} has fatal error in step {} for task {}, reason: {} - {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), + api.getTaskHelper().getLocalVersionlessAbsoluteUrl(variables.getStartTask()), + exception.getClass().getName(), exception.getMessage()); + + updateFailedIfInprogress(variables.getTasks(), errorMessage); + + execution.getProcessEngine().getRuntimeService().deleteProcessInstance(execution.getProcessInstanceId(), + exception.getMessage()); + } + + protected void handleEndEventError(DelegateExecution execution, Variables variables, Exception exception, + String errorMessage) + { + logger.debug("Error while executing Task message send {}", getClass().getName(), exception); + logger.error("Process {} has fatal error in step {} for task {}, reason: {} - {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), + api.getTaskHelper().getLocalVersionlessAbsoluteUrl(variables.getStartTask()), + exception.getClass().getName(), exception.getMessage()); + + updateFailedIfInprogress(variables.getTasks(), errorMessage); + + // End event: No need to delete process instance + } + + protected void handleSendTaskError(DelegateExecution execution, Variables variables, Exception exception, + String errorMessage) + { + Targets targets = variables.getTargets(); + + // if we are a multi instance message send task, remove target + if (targets != null && !targets.isEmpty()) + { + Target target = variables.getTarget(); + targets = targets.removeByEndpointIdentifierValue(target); + variables.setTargets(targets); + + addErrorIfInprogress(variables.getTasks(), errorMessage); + + logger.debug("Target organization {}, endpoint {} with error {} removed from target list", + target.getOrganizationIdentifierValue(), target.getEndpointIdentifierValue(), + exception.getMessage()); + } + + // if we are not a multi instance message send task or all sends have failed (targets emtpy) + else + { + logger.debug("Error while executing Task message send {}", getClass().getName(), exception); + logger.error("Process {} has fatal error in step {} for task {}, last reason: {} - {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), + api.getTaskHelper().getLocalVersionlessAbsoluteUrl(variables.getStartTask()), + exception.getClass().getName(), exception.getMessage()); + + updateFailedIfInprogress(variables.getTasks(), errorMessage); + + execution.getProcessEngine().getRuntimeService().deleteProcessInstance(execution.getProcessInstanceId(), + exception.getMessage()); + } + } + + private void addErrorIfInprogress(List tasks, String errorMessage) + { + for (int i = tasks.size() - 1; i >= 0; i--) + { + Task task = tasks.get(i); + + if (TaskStatus.INPROGRESS.equals(task.getStatus())) + { + addErrorMessage(task, errorMessage); + } + else + { + logger.debug("Not adding error to Task {} with status: {}", + api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), task.getStatus()); + } + } + } + + private void updateFailedIfInprogress(List tasks, String errorMessage) + { + for (int i = tasks.size() - 1; i >= 0; i--) + { + Task task = tasks.get(i); + + if (TaskStatus.INPROGRESS.equals(task.getStatus())) + { + task.setStatus(Task.TaskStatus.FAILED); + addErrorMessage(task, errorMessage); + updateAndHandleException(task); + } + else + { + logger.debug("Not updating Task {} with status: {}", + api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), task.getStatus()); + } + } + } + + protected void addErrorMessage(Task task, String errorMessage) + { + task.addOutput(new TaskOutputComponent(new CodeableConcept(BpmnMessage.error()), new StringType(errorMessage))); + } + + private void updateAndHandleException(Task task) + { + try + { + logger.debug("Updating Task {}, new status: {}", api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), + task.getStatus().toCode()); + + api.getFhirWebserviceClientProvider().getLocalWebserviceClient().withMinimalReturn().update(task); + } + catch (Exception e) + { + logger.debug("Unable to update Task {}", api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), e); + logger.error("Unable to update Task {}: {} - {}", api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), + e.getClass().getName(), e.getMessage()); + } + } + + /** + * Override this method to add additional input parameters to the task resource being send. + * + * @param execution + * the delegate execution of this process instance + * @return {@link Stream} of {@link ParameterComponent}s to be added as input parameters + */ + protected Stream getAdditionalInputParameters(DelegateExecution execution, Variables variables) + { + return Stream.empty(); + } + + /** + * Generates an alternative business-key and stores it as a process variable with name + * {@link BpmnExecutionVariables#ALTERNATIVE_BUSINESS_KEY} + *

+ * Use this method in combination with overriding + * {@link #sendTask(DelegateExecution, Variables, Target, String, String, String, String, Stream)} to use an + * alternative business-key with the communication target. + * + *

+	 * @Override
+	 * protected void sendTasksendTask(DelegateExecution execution, Variables variables, Target target,
+	 * 		String instantiatesCanonical, String messageName, String businessKey, String profile,
+	 * 		Stream<ParameterComponent> additionalInputParameters)
+	 * {
+	 * 	String alternativeBusinesKey = createAndSaveAlternativeBusinessKey();
+	 * 	super.sendTask(execution, target, instantiatesUri, messageName, alternativeBusinesKey, profile,
+	 * 			additionalInputParameters);
+	 * }
+	 * 
+ * + * Return tasks from the target using the alternative business-key will correlate with this process instance. + *

+ * + * + * @param execution + * not null + * @return the alternative business-key stored as variable {@link BpmnExecutionVariables#ALTERNATIVE_BUSINESS_KEY} + * @see Variables#setAlternativeBusinessKey(String) + */ + protected final String createAndSaveAlternativeBusinessKey(DelegateExecution execution, Variables variables) + { + String alternativeBusinessKey = UUID.randomUUID().toString(); + variables.setAlternativeBusinessKey(alternativeBusinessKey); + return alternativeBusinessKey; + } + + /** + * @param execution + * not null + * @param variables + * not null + * @param target + * not null + * @param instantiatesCanonical + * not null, not empty + * @param messageName + * not null, not empty + * @param businessKey + * not null, not empty + * @param profile + * not null, not empty + * @param additionalInputParameters + * may be null + */ + protected void sendTask(DelegateExecution execution, Variables variables, Target target, + String instantiatesCanonical, String messageName, String businessKey, String profile, + Stream additionalInputParameters) + { + Objects.requireNonNull(target, "target"); + Objects.requireNonNull(instantiatesCanonical, "instantiatesCanonical"); + if (instantiatesCanonical.isEmpty()) + throw new IllegalArgumentException("instantiatesCanonical empty"); + Objects.requireNonNull(messageName, "messageName"); + if (messageName.isEmpty()) + throw new IllegalArgumentException("messageName empty"); + Objects.requireNonNull(businessKey, "businessKey"); + if (businessKey.isEmpty()) + throw new IllegalArgumentException("profile empty"); + Objects.requireNonNull(profile, "profile"); + if (profile.isEmpty()) + throw new IllegalArgumentException("profile empty"); + + Task task = new Task(); + task.setMeta(new Meta().addProfile(profile)); + task.setStatus(TaskStatus.REQUESTED); + task.setIntent(TaskIntent.ORDER); + task.setAuthoredOn(new Date()); + task.setRequester(getRequester()); + task.getRestriction().addRecipient(getRecipient(target)); + task.setInstantiatesCanonical(instantiatesCanonical); + + ParameterComponent messageNameInput = new ParameterComponent(new CodeableConcept(BpmnMessage.messageName()), + new StringType(messageName)); + task.getInput().add(messageNameInput); + + ParameterComponent businessKeyInput = new ParameterComponent(new CodeableConcept(BpmnMessage.businessKey()), + new StringType(businessKey)); + task.getInput().add(businessKeyInput); + + String correlationKey = target.getCorrelationKey(); + if (correlationKey != null) + { + ParameterComponent correlationKeyInput = new ParameterComponent( + new CodeableConcept(BpmnMessage.correlationKey()), new StringType(correlationKey)); + task.getInput().add(correlationKeyInput); + } + + if (additionalInputParameters != null) + additionalInputParameters.forEach(task.getInput()::add); + + FhirWebserviceClient client = api.getFhirWebserviceClientProvider() + .getWebserviceClient(target.getEndpointUrl()); + + if (correlationKey != null) + logger.info( + "Sending task {} [recipient: {}, endpoint: {}, businessKey: {}, correlationKey: {}, message: {}] ...", + task.getInstantiatesCanonical(), target.getOrganizationIdentifierValue(), + target.getEndpointIdentifierValue(), businessKey, correlationKey, messageName); + else + logger.info("Sending task {} [recipient: {}, endpoint: {}, businessKey: {}, message: {}] ...", + task.getInstantiatesCanonical(), target.getOrganizationIdentifierValue(), + target.getEndpointIdentifierValue(), businessKey, messageName); + + logger.trace("Task resource to send: {}", + api.getFhirContext().newJsonParser().setStripVersionsFromReferences(false) + .setOverrideResourceIdWithBundleEntryFullUrl(false).encodeResourceToString(task)); + + IdType created = doSend(client, task); + + logger.info("Task {} send [task: {}]", task.getInstantiatesCanonical(), created.toVersionless().getValue()); + } + + /** + * Override this method to modify the remote task create behavior, e.g. to implement retries + * + *

+	 * 
+	 * @Override
+	 * protected void doSend(FhirWebserviceClient client, Task task)
+	 * {
+	 *     client.withMinimalReturn().withRetry(2).create(task);
+	 * }
+	 * 
+	 * 
+ * + * @param client + * not null + * @param task + * not null + * @return id of created task + */ + protected IdType doSend(FhirWebserviceClient client, Task task) + { + return client.withMinimalReturn().create(task); + } + + protected Reference getRecipient(Target target) + { + return new Reference().setType(ResourceType.Organization.name()) + .setIdentifier(OrganizationIdentifier.withValue(target.getOrganizationIdentifierValue())); + } + + protected Reference getRequester() + { + return new Reference().setType(ResourceType.Organization.name()) + .setIdentifier(api.getOrganizationProvider().getLocalOrganizationIdentifier() + .orElseThrow(() -> new IllegalStateException("Local organization identifier unknown"))); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/DefaultUserTaskListener.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/DefaultUserTaskListener.java new file mode 100644 index 000000000..4669272f2 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/activity/DefaultUserTaskListener.java @@ -0,0 +1,264 @@ +package dev.dsf.bpe.v2.activity; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.camunda.bpm.engine.delegate.DelegateExecution; +import org.camunda.bpm.engine.delegate.DelegateTask; +import org.camunda.bpm.engine.delegate.TaskListener; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskOutputComponent; +import org.hl7.fhir.r4.model.Task.TaskStatus; +import org.hl7.fhir.r4.model.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.Bean; + +import dev.dsf.bpe.v2.ProcessPluginApi; +import dev.dsf.bpe.v2.activity.DefaultUserTaskListener; +import dev.dsf.bpe.v2.constants.CodeSystems.BpmnMessage; +import dev.dsf.bpe.v2.constants.CodeSystems.BpmnUserTask; +import dev.dsf.bpe.v2.variables.Variables; + +/** + * Default {@link TaskListener} implementation. This listener will be added to user tasks if no other + * {@link TaskListener} is defined for the 'create' event type. + *

+ * BPMN user tasks need to define the form to be used with type 'Embedded or External Task Forms' and the canonical URL + * of the a {@link Questionnaire} resource as the form key. + *

+ * To modify the behavior of the listener, for example to set default values in the created 'in-progress' + * {@link QuestionnaireResponse}, extend this class, register it as a prototype {@link Bean} and specify the class name + * as a task listener with event type 'create' in the BPMN. + */ +public class DefaultUserTaskListener implements TaskListener, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(DefaultUserTaskListener.class); + + private final ProcessPluginApi api; + + /** + * @param api + * not null + */ + public DefaultUserTaskListener(ProcessPluginApi api) + { + this.api = api; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(api, "api"); + } + + @Override + public final void notify(DelegateTask userTask) + { + final DelegateExecution execution = userTask.getExecution(); + final Variables variables = api.getVariables(execution); + + try + { + logger.trace("Execution of user task with id='{}'", execution.getCurrentActivityId()); + + String questionnaireUrlWithVersion = userTask.getBpmnModelElementInstance().getCamundaFormKey(); + Questionnaire questionnaire = readQuestionnaire(questionnaireUrlWithVersion); + + String businessKey = execution.getBusinessKey(); + String userTaskId = userTask.getId(); + + QuestionnaireResponse questionnaireResponse = createDefaultQuestionnaireResponse( + questionnaireUrlWithVersion, businessKey, userTaskId); + transformQuestionnaireItemsToQuestionnaireResponseItems(questionnaireResponse, questionnaire); + + beforeQuestionnaireResponseCreate(userTask, questionnaireResponse); + checkQuestionnaireResponse(questionnaireResponse); + + QuestionnaireResponse created = api.getFhirWebserviceClientProvider().getLocalWebserviceClient() + .withRetryForever(60000).create(questionnaireResponse); + + logger.info("Created QuestionnaireResponse for user task at {}, process waiting for it's completion", + api.getQuestionnaireResponseHelper().getLocalVersionlessAbsoluteUrl(created)); + + afterQuestionnaireResponseCreate(userTask, created); + } + catch (Exception exception) + { + logger.debug("Error while executing user task listener {}", getClass().getName(), exception); + logger.error("Process {} has fatal error in step {} for task {}, reason: {} - {}", + execution.getProcessDefinitionId(), execution.getActivityInstanceId(), + api.getTaskHelper().getLocalVersionlessAbsoluteUrl(variables.getStartTask()), + exception.getClass().getName(), exception.getMessage()); + + String errorMessage = "Process " + execution.getProcessDefinitionId() + " has fatal error in step " + + execution.getActivityInstanceId() + ", reason: " + exception.getMessage(); + + updateFailedIfInprogress(variables.getTasks(), errorMessage); + + // TODO evaluate throwing exception as alternative to stopping the process instance + execution.getProcessEngine().getRuntimeService().deleteProcessInstance(execution.getProcessInstanceId(), + exception.getMessage()); + } + } + + private Questionnaire readQuestionnaire(String urlWithVersion) + { + Bundle search = api.getFhirWebserviceClientProvider().getLocalWebserviceClient().search(Questionnaire.class, + Map.of("url", Collections.singletonList(urlWithVersion))); + + List questionnaires = search.getEntry().stream().filter(Bundle.BundleEntryComponent::hasResource) + .map(Bundle.BundleEntryComponent::getResource).filter(r -> r instanceof Questionnaire) + .map(r -> (Questionnaire) r).collect(Collectors.toList()); + + if (questionnaires.size() < 1) + throw new RuntimeException("Could not find Questionnaire resource with url|version=" + urlWithVersion); + + if (questionnaires.size() > 1) + logger.info("Found {} Questionnaire resources with url|version={}, using the first", questionnaires.size(), + urlWithVersion); + + return questionnaires.get(0); + } + + + private QuestionnaireResponse createDefaultQuestionnaireResponse(String questionnaireUrlWithVersion, + String businessKey, String userTaskId) + { + QuestionnaireResponse questionnaireResponse = new QuestionnaireResponse(); + questionnaireResponse.setQuestionnaire(questionnaireUrlWithVersion); + questionnaireResponse.setStatus(QuestionnaireResponse.QuestionnaireResponseStatus.INPROGRESS); + + questionnaireResponse.setAuthor(new Reference().setType(ResourceType.Organization.name()) + .setIdentifier(api.getOrganizationProvider().getLocalOrganizationIdentifier() + .orElseThrow(() -> new IllegalStateException("Local organization identifier unknown")))); + + api.getQuestionnaireResponseHelper().addItemLeafWithAnswer(questionnaireResponse, + BpmnUserTask.Codes.BUSINESS_KEY, "The business-key of the process execution", + new StringType(businessKey)); + + api.getQuestionnaireResponseHelper().addItemLeafWithAnswer(questionnaireResponse, + BpmnUserTask.Codes.USER_TASK_ID, "The user-task-id of the process execution", + new StringType(userTaskId)); + + return questionnaireResponse; + } + + private void transformQuestionnaireItemsToQuestionnaireResponseItems(QuestionnaireResponse questionnaireResponse, + Questionnaire questionnaire) + { + questionnaire.getItem().stream().filter(i -> !BpmnUserTask.Codes.BUSINESS_KEY.equals(i.getLinkId())) + .filter(i -> !BpmnUserTask.Codes.USER_TASK_ID.equals(i.getLinkId())) + .forEach(i -> transformItem(questionnaireResponse, i)); + } + + private void transformItem(QuestionnaireResponse questionnaireResponse, + Questionnaire.QuestionnaireItemComponent question) + { + if (Questionnaire.QuestionnaireItemType.DISPLAY.equals(question.getType())) + { + api.getQuestionnaireResponseHelper().addItemLeafWithoutAnswer(questionnaireResponse, question.getLinkId(), + question.getText()); + } + else + { + Type answer = api.getQuestionnaireResponseHelper().transformQuestionTypeToAnswerType(question); + api.getQuestionnaireResponseHelper().addItemLeafWithAnswer(questionnaireResponse, question.getLinkId(), + question.getText(), answer); + } + } + + private void checkQuestionnaireResponse(QuestionnaireResponse questionnaireResponse) + { + questionnaireResponse.getItem().stream().filter(i -> BpmnUserTask.Codes.BUSINESS_KEY.equals(i.getLinkId())) + .findFirst() + .orElseThrow(() -> new RuntimeException("QuestionnaireResponse does not contain an item with linkId='" + + BpmnUserTask.Codes.BUSINESS_KEY + "'")); + + questionnaireResponse.getItem().stream().filter(i -> BpmnUserTask.Codes.USER_TASK_ID.equals(i.getLinkId())) + .findFirst() + .orElseThrow(() -> new RuntimeException("QuestionnaireResponse does not contain an item with linkId='" + + BpmnUserTask.Codes.USER_TASK_ID + "'")); + + if (!QuestionnaireResponse.QuestionnaireResponseStatus.INPROGRESS.equals(questionnaireResponse.getStatus())) + throw new RuntimeException("QuestionnaireResponse must be in status 'in-progress'"); + } + + /** + * Override this method to modify the {@link QuestionnaireResponse} before it will be created in state + * {@link QuestionnaireResponse.QuestionnaireResponseStatus#INPROGRESS} on the DSF FHIR server + * + * @param userTask + * not null, user task on which this {@link QuestionnaireResponse} is based + * @param beforeCreate + * not null, containing an answer placeholder for every item in the corresponding + * {@link Questionnaire} + */ + protected void beforeQuestionnaireResponseCreate(DelegateTask userTask, QuestionnaireResponse beforeCreate) + { + // Nothing to do in default behavior + } + + /** + * Override this method to execute code after the {@link QuestionnaireResponse} resource has been created on the + * DSF FHIR server + * + * @param userTask + * not null, user task on which this {@link QuestionnaireResponse} is based + * @param afterCreate + * not null, created on the DSF FHIR server + */ + protected void afterQuestionnaireResponseCreate(DelegateTask userTask, QuestionnaireResponse afterCreate) + { + // Nothing to do in default behavior + } + + private void updateFailedIfInprogress(List tasks, String errorMessage) + { + for (int i = tasks.size() - 1; i >= 0; i--) + { + Task task = tasks.get(i); + + if (TaskStatus.INPROGRESS.equals(task.getStatus())) + { + task.setStatus(Task.TaskStatus.FAILED); + task.addOutput(new TaskOutputComponent(new CodeableConcept(BpmnMessage.error()), + new StringType(errorMessage))); + updateAndHandleException(task); + } + else + { + logger.debug("Not updating Task {} with status: {}", + api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), task.getStatus()); + } + } + } + + private void updateAndHandleException(Task task) + { + try + { + logger.debug("Updating Task {}, new status: {}", api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), + task.getStatus().toCode()); + + api.getFhirWebserviceClientProvider().getLocalWebserviceClient().withMinimalReturn().update(task); + } + catch (Exception e) + { + logger.debug("Unable to update Task {}", api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), e); + logger.error("Unable to update Task {}: {} - {}", api.getTaskHelper().getLocalVersionlessAbsoluteUrl(task), + e.getClass().getName(), e.getMessage()); + } + } +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/BasicFhirWebserviceClient.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/BasicFhirWebserviceClient.java new file mode 100644 index 000000000..685e74795 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/BasicFhirWebserviceClient.java @@ -0,0 +1,121 @@ +package dev.dsf.bpe.v2.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 resourceClass, String id); + + void deleteConditionaly(Class resourceClass, Map> criteria); + + void deletePermanently(Class 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 resourceType, Map> parameters); + + Bundle searchWithStrictHandling(Class 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 resourceType) + { + return history(resourceType, null); + } + + default Bundle history(Class resourceType, int page, int count) + { + return history(resourceType, null, page, count); + } + + default Bundle history(Class resourceType, String id) + { + return history(resourceType, id, Integer.MIN_VALUE, Integer.MIN_VALUE); + } + + Bundle history(Class resourceType, String id, int page, int count); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/FhirWebserviceClient.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/FhirWebserviceClient.java new file mode 100644 index 000000000..e93d5704b --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/FhirWebserviceClient.java @@ -0,0 +1,10 @@ +package dev.dsf.bpe.v2.client; + +public interface FhirWebserviceClient extends BasicFhirWebserviceClient, RetryClient +{ + String getBaseUrl(); + + PreferReturnOutcomeWithRetry withOperationOutcomeReturn(); + + PreferReturnMinimalWithRetry withMinimalReturn(); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimal.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimal.java new file mode 100644 index 000000000..9c6191add --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimal.java @@ -0,0 +1,28 @@ +package dev.dsf.bpe.v2.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-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimalWithRetry.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimalWithRetry.java new file mode 100644 index 000000000..58879bd90 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnMinimalWithRetry.java @@ -0,0 +1,5 @@ +package dev.dsf.bpe.v2.client; + +public interface PreferReturnMinimalWithRetry extends PreferReturnMinimal, RetryClient +{ +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcome.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcome.java new file mode 100644 index 000000000..98cd01588 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcome.java @@ -0,0 +1,30 @@ +package dev.dsf.bpe.v2.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-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcomeWithRetry.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcomeWithRetry.java new file mode 100644 index 000000000..9a18685c6 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnOutcomeWithRetry.java @@ -0,0 +1,5 @@ +package dev.dsf.bpe.v2.client; + +public interface PreferReturnOutcomeWithRetry extends PreferReturnOutcome, RetryClient +{ +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnResource.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnResource.java new file mode 100644 index 000000000..2b0d059bd --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/PreferReturnResource.java @@ -0,0 +1,30 @@ +package dev.dsf.bpe.v2.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 +{ + R create(R resource); + + R createConditionaly(R resource, String ifNoneExistCriteria); + + Binary createBinary(InputStream in, MediaType mediaType, String securityContextReference); + + + R update(R resource); + + R updateConditionaly(R resource, Map> criteria); + + Binary 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-v2/src/main/java/dev/dsf/bpe/v2/client/RetryClient.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/RetryClient.java new file mode 100644 index 000000000..e2fc409b3 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/client/RetryClient.java @@ -0,0 +1,68 @@ +package dev.dsf.bpe.v2.client; + +public interface RetryClient +{ + int RETRY_ONCE = 1; + int RETRY_FOREVER = -1; + long FIVE_SECONDS = 5_000L; + + /** + * retries once after a delay of {@value RetryClient#FIVE_SECONDS} ms + * + * @return T + */ + default T withRetry() + { + return withRetry(RETRY_ONCE, FIVE_SECONDS); + } + + /** + * retries nTimes and waits {@value RetryClient#FIVE_SECONDS} ms between tries + * + * @param nTimes + * {@code >= 0} + * @return T + * + * @throws IllegalArgumentException + * if param nTimes is {@code <0} + */ + default T withRetry(int nTimes) + { + return withRetry(nTimes, FIVE_SECONDS); + } + + /** + * retries once after a delay of delayMillis ms + * + * @param delayMillis + * {@code >= 0} + * @return T + * @throws IllegalArgumentException + * if param delayMillis is {@code <0} + */ + default T withRetry(long delayMillis) + { + return withRetry(RETRY_ONCE, delayMillis); + } + + /** + * @param nTimes + * {@code >= 0} + * @param delayMillis + * {@code >= 0} + * @return T + * + * @throws IllegalArgumentException + * if param nTimes or delayMillis is {@code <0} + */ + T withRetry(int nTimes, long delayMillis); + + /** + * @param delayMillis + * {@code >= 0} + * @return T + * @throws IllegalArgumentException + * if param delayMillis is {@code <0} + */ + T withRetryForever(long delayMillis); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/config/ProxyConfig.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/config/ProxyConfig.java new file mode 100644 index 000000000..3ea14507d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/config/ProxyConfig.java @@ -0,0 +1,78 @@ +package dev.dsf.bpe.v2.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
ConfiguredGivenResult
foo.bar, test.com:8080https://foo.bar/fhirtrue
foo.bar, test.com:8080https://baz.foo.bar/testtrue
foo.bar, test.com:8080https://test.com:8080/fhirtrue
foo.bar, test.com:8080https://test.com/fhirfalse
foo.bar:443https://foo.bar/fhirtrue
+ * + * @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-v2/src/main/java/dev/dsf/bpe/v2/constants/BpmnExecutionVariables.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/constants/BpmnExecutionVariables.java new file mode 100644 index 000000000..6b1de0060 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/constants/BpmnExecutionVariables.java @@ -0,0 +1,57 @@ +package dev.dsf.bpe.v2.constants; + +import dev.dsf.bpe.v2.activity.AbstractTaskMessageSend; +import dev.dsf.bpe.v2.variables.Target; +import dev.dsf.bpe.v2.variables.Variables; + +/** + * Defines names of standard process engine variables used by the bpe + * + * @see Variables + */ +public final class BpmnExecutionVariables +{ + private BpmnExecutionVariables() + { + } + + /** + * Values from the target variable are used to configure {@link AbstractTaskMessageSend} activities for + * sending Task resource messages + * + * @see Variables#createTarget(String, String, String, String) + * @see Variables#createTarget(String, String, String) + * @see Variables#setTarget(dev.dsf.bpe.v2.variables.Target) + * @see 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 Variables#createTargets(java.util.List) + * @see Variables#createTargets(dev.dsf.bpe.v2.variables.Target...) + * @see Variables#setTargets(dev.dsf.bpe.v2.variables.Targets) + * @see 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 AbstractTaskMessageSend} on how to create and use an alternative + * business-key. + * + * @see AbstractTaskMessageSend + */ + public static final String ALTERNATIVE_BUSINESS_KEY = "alternativeBusinessKey"; +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/constants/CodeSystems.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/constants/CodeSystems.java new file mode 100644 index 000000000..035c50f16 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/constants/CodeSystems.java @@ -0,0 +1,205 @@ +package dev.dsf.bpe.v2.constants; + +import org.hl7.fhir.r4.model.Coding; + +/** + * Constants defining standard DSF CodeSystems + */ +public final class CodeSystems +{ + private CodeSystems() + { + } + + private static boolean isSame(String system, String code, Coding coding) + { + return system != null && code != null && coding != null && coding.hasSystem() + && system.equals(coding.getSystem()) && coding.hasCode() && code.equals(coding.getCode()); + } + + 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); + } + } + + public static final class ProcessAuthorization + { + private ProcessAuthorization() + { + } + + public static final String URL = "http://dsf.dev/fhir/CodeSystem/process-authorization"; + + public static final class Codes + { + private Codes() + { + } + + public static final String LOCAL_ORGANIZATION = "LOCAL_ORGANIZATION"; + public static final String LOCAL_ORGANIZATION_PRACTITIONER = "LOCAL_ORGANIZATION_PRACTITIONER"; + public static final String REMOTE_ORGANIZATION = "REMOTE_ORGANIZATION"; + public static final String LOCAL_ROLE = "LOCAL_ROLE"; + public static final String LOCAL_ROLE_PRACTITIONER = "LOCAL_ROLE_PRACTITIONER"; + public static final String REMOTE_ROLE = "REMOTE_ROLE"; + public static final String LOCAL_ALL = "LOCAL_ALL"; + public static final String LOCAL_ALL_PRACTITIONER = "LOCAL_ALL_PRACTITIONER"; + public static final String REMOTE_ALL = "REMOTE_ALL"; + } + + public static final Coding localOrganization() + { + return new Coding(URL, Codes.LOCAL_ORGANIZATION, null); + } + + public static final Coding localOrganizationPractitioner() + { + return new Coding(URL, Codes.LOCAL_ORGANIZATION_PRACTITIONER, null); + } + + public static final Coding remoteOrganization() + { + return new Coding(URL, Codes.REMOTE_ORGANIZATION, null); + } + + public static final Coding localRole() + { + return new Coding(URL, Codes.LOCAL_ROLE, null); + } + + public static final Coding localRolePractitioner() + { + return new Coding(URL, Codes.LOCAL_ROLE_PRACTITIONER, null); + } + + public static final Coding remoteRole() + { + return new Coding(URL, Codes.REMOTE_ROLE, null); + } + + public static final Coding localAll() + { + return new Coding(URL, Codes.LOCAL_ALL, null); + } + + public static final Coding localAllPractitioner() + { + return new Coding(URL, Codes.LOCAL_ALL_PRACTITIONER, null); + } + + public static final Coding remoteAll() + { + return new Coding(URL, Codes.REMOTE_ALL, null); + } + + public static final boolean isLocalOrganization(Coding coding) + { + return isSame(URL, Codes.LOCAL_ORGANIZATION, coding); + } + + public static final boolean isLocalOrganizationPractitioner(Coding coding) + { + return isSame(URL, Codes.LOCAL_ORGANIZATION_PRACTITIONER, coding); + } + + public static final boolean isRemoteOrganization(Coding coding) + { + return isSame(URL, Codes.REMOTE_ORGANIZATION, coding); + } + + public static final boolean isLocalRole(Coding coding) + { + return isSame(URL, Codes.LOCAL_ROLE, coding); + } + + public static final boolean isLocalRolePractitioner(Coding coding) + { + return isSame(URL, Codes.LOCAL_ROLE_PRACTITIONER, coding); + } + + public static final boolean isRemoteRole(Coding coding) + { + return isSame(URL, Codes.REMOTE_ROLE, coding); + } + + public static final boolean isLocalAll(Coding coding) + { + return isSame(URL, Codes.LOCAL_ALL, coding); + } + + public static final boolean isLocalAllPractitioner(Coding coding) + { + return isSame(URL, Codes.LOCAL_ALL_PRACTITIONER, coding); + } + + public static final boolean isRemoteAll(Coding coding) + { + return isSame(URL, Codes.REMOTE_ALL, coding); + } + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/constants/NamingSystems.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/constants/NamingSystems.java new file mode 100644 index 000000000..f14334e0b --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/constants/NamingSystems.java @@ -0,0 +1,154 @@ +package dev.dsf.bpe.v2.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; + +import dev.dsf.bpe.v2.constants.NamingSystems; + +/** + * 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-v2/src/main/java/dev/dsf/bpe/v2/documentation/ProcessDocumentation.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/documentation/ProcessDocumentation.java new file mode 100644 index 000000000..9a5b7812b --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/documentation/ProcessDocumentation.java @@ -0,0 +1,58 @@ +package dev.dsf.bpe.v2.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.v2.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-v2/src/main/java/dev/dsf/bpe/v2/service/EndpointProvider.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/EndpointProvider.java new file mode 100644 index 000000000..6902043e9 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/EndpointProvider.java @@ -0,0 +1,209 @@ +package dev.dsf.bpe.v2.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 dev.dsf.bpe.v2.constants.NamingSystems.EndpointIdentifier; +import dev.dsf.bpe.v2.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 {@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 {@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 {@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 {@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 {@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 + */ + Optional getEndpoint(Identifier parentOrganizationIdentifier, Identifier memberOrganizationIdentifier, + Coding memberOrganizationRole); + + /** + * @param parentOrganizationIdentifierValue + * may be null + * @param memberOrganizationIdentifierValue + * may be null + * @param memberOrganizationRole + * may be null + * @return {@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 + * @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 {@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 + */ + 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 {@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 + * @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 {@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 + */ + List getEndpoints(Identifier parentOrganizationIdentifier, Coding memberOrganizationRole); + + /** + * @param parentOrganizationIdentifierValue + * may be null + * @param memberOrganizationRole + * may be null + * @return {@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 + * @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-v2/src/main/java/dev/dsf/bpe/v2/service/FhirWebserviceClientProvider.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/FhirWebserviceClientProvider.java new file mode 100644 index 000000000..a3f710928 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/FhirWebserviceClientProvider.java @@ -0,0 +1,10 @@ +package dev.dsf.bpe.v2.service; + +import dev.dsf.bpe.v2.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-v2/src/main/java/dev/dsf/bpe/v2/service/MailService.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/MailService.java new file mode 100644 index 000000000..70f25aa7c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/MailService.java @@ -0,0 +1,155 @@ +package dev.dsf.bpe.v2.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-v2/src/main/java/dev/dsf/bpe/v2/service/OrganizationProvider.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/OrganizationProvider.java new file mode 100644 index 000000000..6f38b827c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/OrganizationProvider.java @@ -0,0 +1,134 @@ +package dev.dsf.bpe.v2.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.v2.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 {@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 {@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 Organizations configured as participatingOrganization for a 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 Organizations configured as participatingOrganization for a 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 Organizations configured as participatingOrganization for a 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 Organizations configured as participatingOrganization for a 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 {@link Organization} resources except the local {@link Organization} + * @see #getLocalOrganization() + */ + List getRemoteOrganizations(); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/QuestionnaireResponseHelper.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/QuestionnaireResponseHelper.java new file mode 100644 index 000000000..d17b2d3f9 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/QuestionnaireResponseHelper.java @@ -0,0 +1,45 @@ +package dev.dsf.bpe.v2.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-v2/src/main/java/dev/dsf/bpe/v2/service/ReadAccessHelper.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/ReadAccessHelper.java new file mode 100644 index 000000000..e5163cde8 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/ReadAccessHelper.java @@ -0,0 +1,170 @@ +package dev.dsf.bpe.v2.service; + +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 +{ + /** + * 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-v2/src/main/java/dev/dsf/bpe/v2/service/TaskHelper.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/TaskHelper.java new file mode 100644 index 000000000..6ea0eae68 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/TaskHelper.java @@ -0,0 +1,421 @@ +package dev.dsf.bpe.v2.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 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 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 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 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 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 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 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 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-v2/src/main/java/dev/dsf/bpe/v2/service/process/Identity.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/Identity.java new file mode 100644 index 000000000..b13713ae9 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/Identity.java @@ -0,0 +1,13 @@ +package dev.dsf.bpe.v2.service.process; + +import org.hl7.fhir.r4.model.Organization; + +public interface Identity +{ + boolean isLocalIdentity(); + + /** + * @return never null + */ + Organization getOrganization(); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/OrganizationIdentity.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/OrganizationIdentity.java new file mode 100644 index 000000000..8e2c0c53d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/OrganizationIdentity.java @@ -0,0 +1,5 @@ +package dev.dsf.bpe.v2.service.process; + +public interface OrganizationIdentity extends Identity +{ +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/PractitionerIdentity.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/PractitionerIdentity.java new file mode 100644 index 000000000..374416b34 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/PractitionerIdentity.java @@ -0,0 +1,13 @@ +package dev.dsf.bpe.v2.service.process; + +import java.util.Set; + +import org.hl7.fhir.r4.model.Coding; + +public interface PractitionerIdentity extends Identity +{ + /** + * @return never null + */ + Set getPractionerRoles(); +} diff --git a/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/ProcessAuthorizationHelper.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/ProcessAuthorizationHelper.java new file mode 100644 index 000000000..6d198a041 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/ProcessAuthorizationHelper.java @@ -0,0 +1,82 @@ +package dev.dsf.bpe.v2.service.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 +{ + interface RecipientFactory + { + Recipient localAll(); + + Recipient localOrganization(String organizationIdentifier); + + Recipient localRole(String parentOrganizationIdentifier, String roleSystem, String roleCode); + } + + interface RequesterFactory + { + Requester localAll(); + + Requester localAllPractitioner(String practitionerRoleSystem, String practitionerRoleCode); + + Requester remoteAll(); + + Requester localOrganization(String organizationIdentifier); + + Requester localOrganizationPractitioner(String organizationIdentifier, String practitionerRoleSystem, + String practitionerRoleCode); + + Requester remoteOrganization(String organizationIdentifier); + + Requester localRole(String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode); + + Requester localRolePractitioner(String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode, String practitionerRoleSystem, String practitionerRoleCode); + + Requester remoteRole(String parentOrganizationIdentifier, String organizatioRoleSystem, + String organizatioRoleCode); + } + + RecipientFactory getRecipientFactory(); + + RequesterFactory getRequesterFactory(); + + ActivityDefinition add(ActivityDefinition activityDefinition, String messageName, String taskProfile, + Requester requester, Recipient recipient); + + ActivityDefinition add(ActivityDefinition activityDefinition, String messageName, String taskProfile, + Collection requesters, Collection 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-v2/src/main/java/dev/dsf/bpe/v2/service/process/Recipient.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/Recipient.java new file mode 100644 index 000000000..7d81a9ef7 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/Recipient.java @@ -0,0 +1,23 @@ +package dev.dsf.bpe.v2.service.process; + +import java.util.Collection; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.OrganizationAffiliation; + +public interface Recipient extends WithAuthorization +{ + 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-v2/src/main/java/dev/dsf/bpe/v2/service/process/Requester.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/Requester.java new file mode 100644 index 000000000..6cf3f16a5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/Requester.java @@ -0,0 +1,23 @@ +package dev.dsf.bpe.v2.service.process; + +import java.util.Collection; +import java.util.stream.Stream; + +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.OrganizationAffiliation; + +public interface Requester extends WithAuthorization +{ + 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-v2/src/main/java/dev/dsf/bpe/v2/service/process/WithAuthorization.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/WithAuthorization.java new file mode 100644 index 000000000..f2404fb3c --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/service/process/WithAuthorization.java @@ -0,0 +1,10 @@ +package dev.dsf.bpe.v2.service.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-v2/src/main/java/dev/dsf/bpe/v2/variables/Target.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/variables/Target.java new file mode 100644 index 000000000..b0c3a06ef --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/variables/Target.java @@ -0,0 +1,34 @@ +package dev.dsf.bpe.v2.variables; + +import dev.dsf.bpe.v2.constants.BpmnExecutionVariables; + +/** + * Specifies a communication target for FHIR Task resources. + * + * @see 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-v2/src/main/java/dev/dsf/bpe/v2/variables/Targets.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/variables/Targets.java new file mode 100644 index 000000000..4367140f8 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/variables/Targets.java @@ -0,0 +1,52 @@ +package dev.dsf.bpe.v2.variables; + +import java.util.Collection; +import java.util.List; + +import dev.dsf.bpe.v2.constants.BpmnExecutionVariables; + +/** + * Specifies a list of communication targets for FHIR Task resources. + * + * @see 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-v2/src/main/java/dev/dsf/bpe/v2/variables/Variables.java b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/variables/Variables.java new file mode 100644 index 000000000..5f374d67a --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api-v2/src/main/java/dev/dsf/bpe/v2/variables/Variables.java @@ -0,0 +1,605 @@ +package dev.dsf.bpe.v2.variables; + +import java.io.File; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.camunda.bpm.engine.variable.value.TypedValue; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.Task; + +import dev.dsf.bpe.v2.constants.BpmnExecutionVariables; + +/** + * Gives access to process execution variables. Includes factory methods for {@link Target} and {@link Targets} values. + */ +public interface Variables +{ + /** + * Sets execution variable {@link BpmnExecutionVariables#ALTERNATIVE_BUSINESS_KEY} + * + * @param alternativeBusinessKey + * may be null + */ + void setAlternativeBusinessKey(String alternativeBusinessKey); + + /** + * Creates a new {@link Target} object. + *

+ * A not null correlationKey should be used if return messages aka. Task resources + * from multiple organizations with the same message-name are expected in a following multi instance message receive + * task or intermediate message catch event in a multi instance subprocess.
+ * Note: The correlationKey needs to be set as a {@link BpmnExecutionVariables#CORRELATION_KEY} variable in the + * message receive task or intermediate message catch event of a subprocess before incoming messages aka. Task + * resources can be correlated. Within a BPMN file this can be accomplished by setting an input variable with name: + * {@link BpmnExecutionVariables#CORRELATION_KEY}, type:
string or expression, and value: + * ${target.correlationKey}. + *

+ * A not null correlationKey should also be used when sending a message aka. Task + * resource back to an organization waiting for multiple returns. + * + * @param organizationIdentifierValue + * not null + * @param endpointIdentifierValue + * not null + * @param endpointAddress + * not null + * @param correlationKey + * not null if used for sending multiple messages and multiple messages with the same + * message-name are expected in return + * @return new {@link Target} object + * @see #createTarget(String, String, String) + * @see #setTarget(Target) + */ + Target createTarget(String organizationIdentifierValue, String endpointIdentifierValue, String endpointAddress, + String correlationKey); + + /** + * Creates a new {@link Target} object. + * + * See {@link #createTarget(String, String, String, String)} for sending a correlation-key for 1:n or n:1 + * relationships. + * + * @param organizationIdentifierValue + * not null + * @param endpointIdentifierValue + * not null + * @param endpointAddress + * not null + * @return new {@link Target} object + * @see #createTarget(String, String, String, String) + * @see #setTarget(Target) + */ + default Target createTarget(String organizationIdentifierValue, String endpointIdentifierValue, + String endpointAddress) + { + return createTarget(organizationIdentifierValue, endpointIdentifierValue, endpointAddress, null); + } + + /** + * Sets execution variable {@link BpmnExecutionVariables#TARGET} + * + * @param target + * may be null + * @throws IllegalArgumentException + * if the given target object is not supported, meaning the object was not created by this + * {@link Variables} implementation + * @see #createTarget(String, String, String) + * @see #createTarget(String, String, String, String) + * @see #getTarget() + */ + void setTarget(Target target) throws IllegalArgumentException; + + /** + * Retrieves execution variable {@link BpmnExecutionVariables#TARGET} + * + * @return Execution variable {@link BpmnExecutionVariables#TARGET}, may be null + */ + Target getTarget(); + + /** + * Creates a new target list. + * + * Use ${targets.entries} as a multi instance collection and target as + * the element variable to loop over this list in a multi instance task or subprocess. + * + * @param targets + * {@link Target} objects to incorporate into the created list + * @return a new target list + * @throws IllegalArgumentException + * if one of the given target objects is not supported, meaning the object was not created by + * this {@link Variables} implementation + * @see #createTarget(String, String, String) + * @see #createTarget(String, String, String, String) + * @see #setTargets(Targets) + */ + default Targets createTargets(Target... targets) + { + return createTargets(Arrays.asList(targets)); + } + + /** + * Creates a new target list. + * + * Use ${targets.entries} as a multi instance collection and target as + * the element variable to loop over this list in a multi instance task or subprocess. + * + * @param targets + * {@link Target} objects to incorporate into the created list, may be null + * @return a new target list + * @throws IllegalArgumentException + * if one of the given target objects is not supported, meaning the object was not created by + * this {@link Variables} implementation + * @see #createTarget(String, String, String) + * @see #createTarget(String, String, String, String) + * @see #setTargets(Targets) + */ + Targets createTargets(List targets); + + /** + * Sets execution variable {@link BpmnExecutionVariables#TARGETS}. + * + * Use ${targets.entries} as a multi instance collection and + * + * @param targets + * may be null + * @see #createTargets(List) + * @see #createTargets(Target...) + * @see #getTargets() + */ + void setTargets(Targets targets); + + /** + * Retrieves execution variable {@link BpmnExecutionVariables#TARGETS} + * + * @return Execution variable {@link BpmnExecutionVariables#TARGETS}, may be null + * @see #setTargets(Targets) + */ + Targets getTargets(); + + /** + * Sets execution variable with the given variableName to the given FHIR {@link Resource} list + * + * @param variableName + * not null + * @param resources + */ + void setResourceList(String variableName, List resources); + + /** + * Retrieves FHIR {@link Resource} list execution variable with the given variableName + * + * @param + * FHIR resource type + * @param variableName + * not null + * @return list of FHIR resources from execution variables for the given variableName, may be + * null + */ + List getResourceList(String variableName); + + /** + * Sets execution variable with the given variableName to the given FHIR {@link Resource} + * + * @param variableName + * not null + * @param resource + * may be null + */ + void setResource(String variableName, Resource resource); + + /** + * Retrieves FHIR {@link Resource} execution variable with the given variableName + * + * @param + * FHIR resource type + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + */ + R getResource(String variableName); + + /** + * Returns the {@link Task} associated with the message start event of the process. + * + * @return {@link Task} that started the process instance, not null + * @see #updateTask(Task) + * @see #getLatestTask() + * @see #getTasks() + */ + Task getStartTask(); + + /** + * Returns the latest {@link Task} received by this process or subprocess via a intermediate message catch event or + * message receive task. + * + * @return Last received {@link Task} of the current process or subprocess, not null + * @see #updateTask(Task) + * @see #getStartTask() + * @see #getCurrentTasks() + */ + Task getLatestTask(); + + /** + * @return All {@link Task} resources received + * @see #getCurrentTasks() + */ + List getTasks(); + + /** + * @return All {@link Task} resources received by the current process or subprocess + * @see #getTasks() + */ + List getCurrentTasks(); + + /** + * Does nothing if the given task is null. Forces an update to the Task list variable used + * internally to track all received Task resources if the given task object is already part of this list. + * + * @param task + * may be null + * @see #getStartTask() + * @see #getLatestTask() + * @see #getTasks() + * @see #getCurrentTasks() + */ + void updateTask(Task task); + + /** + * @return Last received {@link QuestionnaireResponse}, null if nothing received yet + */ + QuestionnaireResponse getLatestReceivedQuestionnaireResponse(); + + /** + * Sets execution variable with the given variableName to the given {@link TypedValue} + * + * @param variableName + * not null + * @param value + * may be null + * @see #getVariable(String) + * @see #setInteger(String, Integer) + * @see #setString(String, String) + * @see #setBoolean(String, Boolean) + * @see #setByteArray(String, byte[]) + * @see #setDate(String, Date) + * @see #setLong(String, Long) + * @see #setShort(String, Short) + * @see #setDouble(String, Double) + * @see #setNumber(String, Number) + * @see #setFile(String, File) + */ + void setVariable(String variableName, TypedValue value); + + /** + * Retrieves execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @see #setVariable(String, TypedValue) + * @see #getInteger(String) + * @see #getString(String) + * @see #getBoolean(String) + * @see #getByteArray(String) + * @see #getDate(String) + * @see #getLong(String) + * @see #getShort(String) + * @see #getDouble(String) + * @see #getNumber(String) + * @see #getFile(String) + */ + Object getVariable(String variableName); + + /** + * Sets execution variable with the given variableName to the given {@link Integer} + * + * @param variableName + * not null + * @param value + * may be null + * @see #getInteger(String) + * @see #setVariable(String, TypedValue) + */ + default void setInteger(String variableName, Integer value) + { + setVariable(variableName, org.camunda.bpm.engine.variable.Variables.integerValue(value)); + } + + /** + * Retrieves {@link Integer} execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @throws ClassCastException + * if the stored value is not a {@link Integer} + * @see #setInteger(String, Integer) + * @see #getVariable(String) + */ + default Integer getInteger(String variableName) + { + return (Integer) getVariable(variableName); + } + + /** + * Sets execution variable with the given variableName to the given {@link String} + * + * @param variableName + * not null + * @param value + * may be null + * @see #getString(String) + * @see #setVariable(String, TypedValue) + */ + default void setString(String variableName, String value) + { + setVariable(variableName, org.camunda.bpm.engine.variable.Variables.stringValue(value)); + } + + /** + * Retrieves {@link String} execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @throws ClassCastException + * if the stored value is not a {@link String} + * @see #setString(String, String) + * @see #getVariable(String) + */ + default String getString(String variableName) + { + return (String) getVariable(variableName); + } + + /** + * Sets execution variable with the given variableName to the given {@link Boolean} + * + * @param variableName + * not null + * @param value + * may be null + * @see #getBoolean(String) + * @see #setVariable(String, TypedValue) + */ + default void setBoolean(String variableName, Boolean value) + { + setVariable(variableName, org.camunda.bpm.engine.variable.Variables.booleanValue(value)); + } + + /** + * Retrieves {@link Boolean} execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @throws ClassCastException + * if the stored value is not a {@link Boolean} + * @see #setBoolean(String, Boolean) + * @see #getVariable(String) + */ + default Boolean getBoolean(String variableName) + { + return (Boolean) getVariable(variableName); + } + + /** + * Sets execution variable with the given variableName to the given byte[] + * + * @param variableName + * not null + * @param value + * may be null + * @see #getByteArray(String) + * @see #setVariable(String, TypedValue) + */ + default void setByteArray(String variableName, byte[] value) + { + setVariable(variableName, org.camunda.bpm.engine.variable.Variables.byteArrayValue(value)); + } + + /** + * Retrieves byte[] execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @throws ClassCastException + * if the stored value is not a byte[] + * @see #setByteArray(String, byte[]) + * @see #getVariable(String) + */ + default byte[] getByteArray(String variableName) + { + return (byte[]) getVariable(variableName); + } + + /** + * Sets execution variable with the given variableName to the given {@link Date} + * + * @param variableName + * not null + * @param value + * may be null + * @see #getDate(String) + * @see #setVariable(String, TypedValue) + */ + default void setDate(String variableName, Date value) + { + setVariable(variableName, org.camunda.bpm.engine.variable.Variables.dateValue(value)); + } + + /** + * Retrieves {@link Date} execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @throws ClassCastException + * if the stored value is not a {@link Date} + * @see #setDate(String, Date) + * @see #getVariable(String) + */ + default Date getDate(String variableName) + { + return (Date) getVariable(variableName); + } + + /** + * Sets execution variable with the given variableName to the given {@link Long} + * + * @param variableName + * not null + * @param value + * may be null + * @see #getLong(String) + * @see #setVariable(String, TypedValue) + */ + default void setLong(String variableName, Long value) + { + setVariable(variableName, org.camunda.bpm.engine.variable.Variables.longValue(value)); + } + + /** + * Retrieves {@link Long} execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @throws ClassCastException + * if the stored value is not a {@link Long} + * @see #setLong(String, Long) + * @see #getVariable(String) + */ + default Long getLong(String variableName) + { + return (Long) getVariable(variableName); + } + + /** + * Sets execution variable with the given variableName to the given {@link Short} + * + * @param variableName + * not null + * @param value + * may be null + * @see #getShort(String) + * @see #setVariable(String, TypedValue) + */ + default void setShort(String variableName, Short value) + { + setVariable(variableName, org.camunda.bpm.engine.variable.Variables.shortValue(value)); + } + + /** + * Retrieves {@link Short} execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @throws ClassCastException + * if the stored value is not a {@link Short} + * @see #setShort(String, Short) + * @see #getVariable(String) + */ + default Short getShort(String variableName) + { + return (Short) getVariable(variableName); + } + + /** + * Sets execution variable with the given variableName to the given {@link Double} + * + * @param variableName + * not null + * @param value + * may be null + * @see #getDouble(String) + * @see #setVariable(String, TypedValue) + */ + default void setDouble(String variableName, Double value) + { + setVariable(variableName, org.camunda.bpm.engine.variable.Variables.doubleValue(value)); + } + + /** + * Retrieves {@link Double} execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @throws ClassCastException + * if the stored value is not a {@link Double} + * @see #setDouble(String, Double) + * @see #getVariable(String) + */ + default Double getDouble(String variableName) + { + return (Double) getVariable(variableName); + } + + /** + * Sets execution variable with the given variableName to the given {@link Number} + * + * @param variableName + * not null + * @param value + * may be null + * @see #getNumber(String) + * @see #setVariable(String, TypedValue) + */ + default void setNumber(String variableName, Number value) + { + setVariable(variableName, org.camunda.bpm.engine.variable.Variables.numberValue(value)); + } + + /** + * Retrieves {@link Number} execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @throws ClassCastException + * if the stored value is not a {@link Number} + * @see #setNumber(String, Number) + * @see #getVariable(String) + */ + default Number getNumber(String variableName) + { + return (Number) getVariable(variableName); + } + + /** + * Sets execution variable with the given variableName to the given {@link File} + * + * @param variableName + * not null + * @param value + * may be null + * @see #getFile(String) + * @see #setVariable(String, TypedValue) + */ + default void setFile(String variableName, File value) + { + setVariable(variableName, org.camunda.bpm.engine.variable.Variables.fileValue(value)); + } + + /** + * Retrieves {@link File} execution variable with the given variableName + * + * @param variableName + * not null + * @return value from execution variables for the given variableName, may be null + * @throws ClassCastException + * if the stored value is not a {@link File} + * @see #setFile(String, File) + * @see #getVariable(String) + */ + default File getFile(String variableName) + { + return (File) getVariable(variableName); + } +} diff --git a/dsf-bpe/dsf-bpe-process-api/pom.xml b/dsf-bpe/dsf-bpe-process-api/pom.xml new file mode 100644 index 000000000..2be786d6e --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/pom.xml @@ -0,0 +1,32 @@ + + 4.0.0 + + dsf-bpe-process-api + + + dev.dsf + dsf-bpe-pom + 2.0.0-SNAPSHOT + + + + + com.sun.mail + jakarta.mail + + + org.camunda.bpm + camunda-engine + + + org.springframework + spring-context + + + commons-io + commons-io + + + \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/Constants.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/Constants.java new file mode 100644 index 000000000..1d9963b89 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/Constants.java @@ -0,0 +1,27 @@ +package dev.dsf.bpe.api; + +public final class Constants +{ + public static final String BPMN_MESSAGE_URL = "http://dsf.dev/fhir/CodeSystem/bpmn-message"; + + public static final String BPMN_MESSAGE_MESSAGE_NAME = "message-name"; + public static final String BPMN_MESSAGE_BUSINESS_KEY = "business-key"; + public static final String BPMN_MESSAGE_CORRELATION_KEY = "correlation-key"; + public static final String BPMN_MESSAGE_ERROR = "error"; + + public static final String TASK_VARIABLE = "dev.dsf.bpe.subscription.TaskHandler.task"; + + public static final String CORRELATION_KEY = "correlationKey"; + public static final String ALTERNATIVE_BUSINESS_KEY = "alternativeBusinessKey"; + + public static final String QUESTIONNAIRE_RESPONSE_VARIABLE = "dev.dsf.bpe.subscription.QuestionnaireResponseHandler.questionnaireResponse"; + + public static final String ITEM_LINK_ID_BUSINESS_KEY = "business-key"; + public static final String ITEM_LINK_ID_USER_TASK_ID = "user-task-id"; + + public static final String TASK_IDENTIFIER_SID = "http://dsf.dev/sid/task-identifier"; + + private Constants() + { + } +} diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/config/ClientConfig.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/config/ClientConfig.java new file mode 100644 index 000000000..16b45e7e1 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/config/ClientConfig.java @@ -0,0 +1,24 @@ +package dev.dsf.bpe.api.config; + +import java.security.KeyStore; + +public interface ClientConfig +{ + String getFhirServerBaseUrl(); + + KeyStore getWebserviceKeyStore(char[] keyStorePassword); + + KeyStore getWebserviceTrustStore(); + + int getWebserviceClientLocalReadTimeout(); + + int getWebserviceClientLocalConnectTimeout(); + + boolean getWebserviceClientLocalVerbose(); + + int getWebserviceClientRemoteReadTimeout(); + + int getWebserviceClientRemoteConnectTimeout(); + + boolean getWebserviceClientRemoteVerbose(); +} diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/config/ProxyConfig.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/config/ProxyConfig.java new file mode 100644 index 000000000..29cfcebc8 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/config/ProxyConfig.java @@ -0,0 +1,89 @@ +package dev.dsf.bpe.api.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(); + + /** + * @param targetUrl + * may be null + * @return true if a proxy url is configured, '*' is not set as a no-proxy url and the given + * targetUrl is not set as a no-proxy url, false if the given targetUrl is + * null or blank + * @see #getNoProxyUrls() + * @see String#isBlank() + */ + boolean isEnabled(String targetUrl); + + /** + * @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 targetUrl is not null and the domain + port of the + * given targetUrl 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
ConfiguredGivenResult
foo.bar, test.com:8080https://foo.bar/fhirtrue
foo.bar, test.com:8080https://baz.foo.bar/testtrue
foo.bar, test.com:8080https://test.com:8080/fhirtrue
foo.bar, test.com:8080https://test.com/fhirfalse
foo.bar:443https://foo.bar/fhirtrue
+ * + * @param targetUrl + * may be null + * @return true if the given targetUrl is not null and is configured as a no-proxy url + */ + boolean isNoProxyUrl(String targetUrl); +} diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/listener/ListenerFactory.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/listener/ListenerFactory.java new file mode 100644 index 000000000..a4cd23bee --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/listener/ListenerFactory.java @@ -0,0 +1,14 @@ +package dev.dsf.bpe.api.listener; + +import org.camunda.bpm.engine.delegate.ExecutionListener; + +public interface ListenerFactory +{ + int getApiVersion(); + + ExecutionListener getStartListener(); + + ExecutionListener getEndListener(); + + ExecutionListener getContinueListener(); +} diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/listener/ListenerFactoryImpl.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/listener/ListenerFactoryImpl.java new file mode 100644 index 000000000..a7d0550f4 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/listener/ListenerFactoryImpl.java @@ -0,0 +1,44 @@ +package dev.dsf.bpe.api.listener; + +import org.camunda.bpm.engine.delegate.ExecutionListener; + +public class ListenerFactoryImpl implements ListenerFactory +{ + private final int apiVersion; + private final ExecutionListener startListener; + private final ExecutionListener endListener; + private final ExecutionListener continueListener; + + public ListenerFactoryImpl(int apiVersion, ExecutionListener startListener, ExecutionListener endListener, + ExecutionListener continueListener) + { + this.apiVersion = apiVersion; + this.startListener = startListener; + this.endListener = endListener; + this.continueListener = continueListener; + } + + @Override + public int getApiVersion() + { + return apiVersion; + } + + @Override + public ExecutionListener getStartListener() + { + return startListener; + } + + @Override + public ExecutionListener getEndListener() + { + return endListener; + } + + @Override + public ExecutionListener getContinueListener() + { + return continueListener; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/AbstractProcessPlugin.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/AbstractProcessPlugin.java similarity index 80% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/AbstractProcessPlugin.java rename to dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/AbstractProcessPlugin.java index 6538fea8a..75a488f01 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/AbstractProcessPlugin.java +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/AbstractProcessPlugin.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.plugin; +package dev.dsf.bpe.api.plugin; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -45,75 +45,54 @@ import org.camunda.bpm.model.bpmn.instance.camunda.CamundaProperty; import org.camunda.bpm.model.bpmn.instance.camunda.CamundaTaskListener; import org.camunda.bpm.model.xml.instance.ModelElementInstance; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.ActivityDefinition; -import org.hl7.fhir.r4.model.CodeSystem; -import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.Library; -import org.hl7.fhir.r4.model.Measure; -import org.hl7.fhir.r4.model.MetadataResource; -import org.hl7.fhir.r4.model.NamingSystem; -import org.hl7.fhir.r4.model.Questionnaire; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.Resource; -import org.hl7.fhir.r4.model.ResourceType; -import org.hl7.fhir.r4.model.StructureDefinition; -import org.hl7.fhir.r4.model.Task; -import org.hl7.fhir.r4.model.Task.TaskStatus; -import org.hl7.fhir.r4.model.ValueSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.parser.IParser; -import dev.dsf.bpe.v1.constants.CodeSystems; -import dev.dsf.bpe.v1.constants.NamingSystems.OrganizationIdentifier; -import dev.dsf.bpe.v1.constants.NamingSystems.TaskIdentifier; +import dev.dsf.bpe.api.plugin.ProcessPluginFhirConfig.Identifier; +import dev.dsf.bpe.api.plugin.ProcessPluginFhirConfig.Reference; -public abstract class AbstractProcessPlugin implements ProcessPlugin +public abstract class AbstractProcessPlugin implements ProcessPlugin { private static final class FileAndResource { final String file; - final Resource resource; + final Object resource; - FileAndResource(String file, Resource resource) + FileAndResource(String file, Object resource) { Objects.requireNonNull(file, "file"); Objects.requireNonNull(resource, "resource"); - this.file = file; this.resource = resource; + this.file = file; } - static FileAndResource of(String file, Resource resource) + static FileAndResource of(String file, Object resource) { return new FileAndResource(file, resource); } - String getFile() + Object getResource() { - return file; + return resource; } - Resource getResource() + String getFile() { - return resource; + return file; } } private static final Logger logger = LoggerFactory.getLogger(AbstractProcessPlugin.class); private static final String BPMN_SUFFIX = ".bpmn"; - private static final String JSON_SUFFIX = ".json"; - private static final String XML_SUFFIX = ".xml"; + protected static final String JSON_SUFFIX = ".json"; + protected static final String XML_SUFFIX = ".xml"; private static final String RESOURCE_VERSION_PATTERN_STRING = "(?\\d+\\.\\d+)"; private static final Pattern RESOURCE_VERSION_PATTERN = Pattern.compile(RESOURCE_VERSION_PATTERN_STRING); @@ -160,38 +139,50 @@ Resource getResource() private static final String DEFAULT_PROCESS_HISTORY_TIME_TO_LIVE = "P30D"; - private final D processPluginDefinition; - private final A processPluginApi; + private static final String ORGANIZATION_RESOURCE_TYPE_NAME = "Organization"; + + private final String processPluginDefinitionTypeName; + private final int processPluginApiVersion; private final boolean draft; private final Path jarFile; private final ClassLoader processPluginClassLoader; - private final FhirContext fhirContext; private final ConfigurableEnvironment environment; + private final ApplicationContext apiApplicationContext; + private final Class apiServicesSpringConfiguration; + + private final ProcessPluginFhirConfig fhirConfig; private boolean initialized; private AnnotationConfigApplicationContext applicationContext; private List processModels; private Map> fhirResources; - public AbstractProcessPlugin(D processPluginDefinition, A processPluginApi, boolean draft, Path jarFile, - ClassLoader processPluginClassLoader, FhirContext fhirContext, ConfigurableEnvironment environment) + public AbstractProcessPlugin(Class processPluginDefinitionType, int processPluginApiVersion, boolean draft, + Path jarFile, ClassLoader processPluginClassLoader, ConfigurableEnvironment environment, + ApplicationContext apiApplicationContext, Class apiServicesSpringConfiguration) { - Objects.requireNonNull(processPluginDefinition, "definition"); - Objects.requireNonNull(processPluginApi, "processPluginApi"); + Objects.requireNonNull(processPluginDefinitionType, "processPluginDefinitionType"); + Objects.requireNonNull(processPluginApiVersion, "processPluginApiVersion"); Objects.requireNonNull(jarFile, "jarFile"); Objects.requireNonNull(processPluginClassLoader, "processPluginClassLoader"); - Objects.requireNonNull(fhirContext, "fhirContext"); Objects.requireNonNull(environment, "environment"); + Objects.requireNonNull(apiApplicationContext, "apiApplicationContext"); + Objects.requireNonNull(apiServicesSpringConfiguration, "apiServicesSpringConfiguration"); - this.processPluginDefinition = processPluginDefinition; - this.processPluginApi = processPluginApi; + this.processPluginDefinitionTypeName = processPluginDefinitionType.getName(); + this.processPluginApiVersion = processPluginApiVersion; this.draft = draft; this.jarFile = jarFile; this.processPluginClassLoader = processPluginClassLoader; - this.fhirContext = fhirContext; this.environment = environment; + this.apiApplicationContext = apiApplicationContext; + this.apiServicesSpringConfiguration = apiServicesSpringConfiguration; + + this.fhirConfig = createFhirConfig(); } + protected abstract ProcessPluginFhirConfig createFhirConfig(); + protected abstract List> getDefinitionSpringConfigurations(); protected abstract String getDefinitionName(); @@ -208,10 +199,6 @@ public AbstractProcessPlugin(D processPluginDefinition, A processPluginApi, bool protected abstract List getDefinitionProcessModels(); - protected abstract Class getDefaultSpringConfiguration(); - - protected abstract String getProcessPluginApiVersion(); - @Override public boolean initializeAndValidateResources(String localOrganizationIdentifierValue) { @@ -296,16 +283,14 @@ private boolean validateSpringConfigurations() if (springConfigurations == null) { logger.warn("Ignoring process plugin {}-{} from {}: {} spring configurations null", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } if (springConfigurations.isEmpty()) { logger.warn("Ignoring process plugin {}-{} from {}: {} spring configurations empty", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } @@ -316,7 +301,7 @@ private boolean validateSpringConfigurations() logger.warn( "Ignoring process plugin {}-{} from {}: {} spring configuration classes without {} annotation: {}", getDefinitionName(), getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName(), Configuration.class.getName(), + processPluginDefinitionTypeName, Configuration.class.getName(), invalidConfigurationClasses.toString()); return false; } @@ -331,16 +316,14 @@ private boolean validateName() if (name == null) { logger.warn("Ignoring process plugin {}-{} from {}: {} name null", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } if (name.isBlank()) { logger.warn("Ignoring process plugin {}-{} from {}: {} name blank", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } @@ -354,23 +337,21 @@ private boolean validateVersion() if (version == null) { logger.warn("Ignoring process plugin {}-{} from {}: {} version null", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } if (version.isBlank()) { logger.warn("Ignoring process plugin {}-{} from {}: {} version blank", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } if (!VERSION_PATTERN.matcher(version).matches()) { logger.warn("Ignoring process plugin {}-{} from {}: {} version not matching {}", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), processPluginDefinition.getClass().getSimpleName(), + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName, VERSION_PATTERN_STRING); return false; } @@ -385,23 +366,21 @@ private boolean validateResourceVersion() if (resourceVersion == null) { logger.warn("Ignoring process plugin {}-{} from {}: {} resource version null", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } if (resourceVersion.isBlank()) { logger.warn("Ignoring process plugin {}-{} from {}: {} resource version blank", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } if (!RESOURCE_VERSION_PATTERN.matcher(resourceVersion).matches()) { logger.warn("Ignoring process plugin {}-{} from {}: {} version not matching {}", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), processPluginDefinition.getClass().getSimpleName(), + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName, RESOURCE_VERSION_PATTERN_STRING); return false; } @@ -416,8 +395,7 @@ private boolean validateReleaseDate() if (releaseDate == null) { logger.warn("Ignoring process plugin {}-{} from {}: {} release date null", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } @@ -430,8 +408,7 @@ private boolean validateResourceReleaseDate() if (resourceReleaseDate == null) { logger.warn("Ignoring process plugin {}-{} from {}: {} resource release date null", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } @@ -445,16 +422,14 @@ private boolean validateFhirResources() if (fhirResources == null) { logger.warn("Ignoring process plugin {}-{} from {}: {} fhir resources map null", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } if (fhirResources.isEmpty()) { logger.warn("Ignoring process plugin {}-{} from {}: {} fhir resources map empty", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } @@ -468,40 +443,20 @@ private boolean validateProcessModels() if (processModels == null) { logger.warn("Ignoring process plugin {}-{} from {}: {} process models null", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } if (processModels.isEmpty()) { logger.warn("Ignoring process plugin {}-{} from {}: {} process models empty", getDefinitionName(), - getDefinitionVersion(), getJarFile().toString(), - processPluginDefinition.getClass().getSimpleName()); + getDefinitionVersion(), getJarFile().toString(), processPluginDefinitionTypeName); return false; } return true; } - @Override - public D getProcessPluginDefinition() - { - return processPluginDefinition; - } - - @Override - public A getProcessPluginApi() - { - return processPluginApi; - } - - @Override - public boolean isDraft() - { - return draft; - } - @Override public Path getJarFile() { @@ -525,12 +480,12 @@ public ApplicationContext getApplicationContext() @Override @SuppressWarnings("rawtypes") - public List getTypedValueSerializers() + public Stream getTypedValueSerializers() { if (!initialized) throw new IllegalStateException("not initialized"); - return applicationContext.getBeansOfType(TypedValueSerializer.class).values().stream().distinct().toList(); + return applicationContext.getBeansOfType(TypedValueSerializer.class).values().stream().distinct(); } @Override @@ -549,22 +504,13 @@ public List getProcessModels() } @Override - public Map> getFhirResources() + public Map> getFhirResources() { if (!initialized) throw new IllegalStateException("not initialized"); return fhirResources.entrySet().stream().collect(Collectors.toUnmodifiableMap(Entry::getKey, - e -> e.getValue().stream().map(FileAndResource::getResource).toList())); - } - - private ApplicationContext createParentApplicationContext() - { - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - factory.registerSingleton("processPluginApi", getProcessPluginApi()); - GenericApplicationContext context = new GenericApplicationContext(factory); - context.refresh(); - return context; + e -> e.getValue().stream().map(FileAndResource::getResource).map(fhirConfig::encodeResource).toList())); } private AnnotationConfigApplicationContext createApplicationContext() @@ -572,10 +518,10 @@ private AnnotationConfigApplicationContext createApplicationContext() try { var context = new AnnotationConfigApplicationContext(); - context.setParent(createParentApplicationContext()); + context.setParent(apiApplicationContext); context.setClassLoader(getProcessPluginClassLoader()); context.register(Stream - .concat(Stream.of(getDefaultSpringConfiguration()), getDefinitionSpringConfigurations().stream()) + .concat(Stream.of(apiServicesSpringConfiguration), getDefinitionSpringConfigurations().stream()) .toArray(Class[]::new)); context.setEnvironment(environment); context.refresh(); @@ -683,22 +629,22 @@ file, getDefinitionName(), getDefinitionVersion(), VERSION_PLACEHOLDER_PATTERN_S }); property.setCamundaName(MODEL_ATTRIBUTE_PROCESS_API_VERSION); - property.setCamundaValue(getProcessPluginApiVersion()); + property.setCamundaValue(String.valueOf(processPluginApiVersion)); if (process.getCamundaHistoryTimeToLiveString() == null || process.getCamundaHistoryTimeToLiveString().isBlank()) { - if (isDraft()) + if (draft) logger.info("Setting process history time to live for process {} from {} to {}", - process.getId(), jarFile.toString(), DEFAULT_PROCESS_HISTORY_TIME_TO_LIVE); + process.getId(), getJarFile().toString(), DEFAULT_PROCESS_HISTORY_TIME_TO_LIVE); else logger.debug("Setting process history time to live for process {} from {} to {}", - process.getId(), jarFile.toString(), DEFAULT_PROCESS_HISTORY_TIME_TO_LIVE); + process.getId(), getJarFile().toString(), DEFAULT_PROCESS_HISTORY_TIME_TO_LIVE); process.setCamundaHistoryTimeToLiveString(DEFAULT_PROCESS_HISTORY_TIME_TO_LIVE); } }); - return new BpmnFileAndModel(draft, file, model, getJarFile()); + return new BpmnFileAndModel(processPluginApiVersion, draft, file, model, getJarFile()); } catch (IOException e) { @@ -967,15 +913,15 @@ else if (beanNames.length > 1) private Map> loadFhirResources(String localOrganizationIdentifierValue) { - Map resourcesByFilename = getDefinitionFhirResourcesByProcessId().entrySet().stream() + Map resourcesByFilename = getDefinitionFhirResourcesByProcessId().entrySet().stream() .map(Entry::getValue).flatMap(List::stream).distinct() .map(loadFhirResourceOrNull(localOrganizationIdentifierValue)).filter(Objects::nonNull) - .collect(Collectors.toMap(FileAndResource::getFile, FileAndResource::getResource)); + .collect(Collectors.toMap(FileAndResource::getFile, Function.identity())); return getDefinitionFhirResourcesByProcessId().entrySet().stream() .collect(Collectors.toMap(e -> new ProcessIdAndVersion(e.getKey(), getDefinitionResourceVersion()), e -> e.getValue().stream().filter(resourcesByFilename::containsKey) - .map(file -> FileAndResource.of(file, resourcesByFilename.get(file))).toList())); + .map(resourcesByFilename::get).toList())); } private Function loadFhirResourceOrNull(String localOrganizationIdentifierValue) @@ -1019,26 +965,26 @@ file, getDefinitionName(), getDefinitionVersion(), VERSION_PLACEHOLDER_PATTERN_S content = PLACEHOLDER_PREFIX_PATTERN.matcher(content).replaceAll(PLACEHOLDER_PREFIX_SPRING_ESCAPED); content = environment.resolveRequiredPlaceholders(content); - IBaseResource resource = newParser(file).parseResource(content); - - if (resource instanceof ActivityDefinition a && isValid(a, file)) - return FileAndResource.of(file, (Resource) resource); - else if (resource instanceof CodeSystem c && isValid(c, file)) - return FileAndResource.of(file, (Resource) resource); - else if (resource instanceof Library l && isValid(l, file)) - return FileAndResource.of(file, (Resource) resource); - else if (resource instanceof Measure m && isValid(m, file)) - return FileAndResource.of(file, (Resource) resource); - else if (resource instanceof NamingSystem n && isValid(n, file)) - return FileAndResource.of(file, (Resource) resource); - else if (resource instanceof Questionnaire q && isValid(q, file)) - return FileAndResource.of(file, (Resource) resource); - else if (resource instanceof StructureDefinition s && isValid(s, file)) - return FileAndResource.of(file, (Resource) resource); - else if (resource instanceof Task t && isValid(t, file, localOrganizationIdentifierValue)) - return FileAndResource.of(file, (Resource) resource); - else if (resource instanceof ValueSet v && isValid(v, file)) - return FileAndResource.of(file, (Resource) resource); + Object resource = fhirConfig.parseResource(file, content); + + if (fhirConfig.isActivityDefinition(resource) && isValidActivityDefinition(resource, file)) + return FileAndResource.of(file, resource); + else if (fhirConfig.isCodeSystem(resource) && isValidCodeSystem(resource, file)) + return FileAndResource.of(file, resource); + else if (fhirConfig.isLibrary(resource) && isValidLibrary(resource, file)) + return FileAndResource.of(file, resource); + else if (fhirConfig.isMeasure(resource) && isValidMeasure(resource, file)) + return FileAndResource.of(file, resource); + else if (fhirConfig.isNamingSystem(resource) && isValidNamingSystem(resource, file)) + return FileAndResource.of(file, resource); + else if (fhirConfig.isQuestionnaire(resource) && isValidQuestionnaire(resource, file)) + return FileAndResource.of(file, resource); + else if (fhirConfig.isStructureDefinition(resource) && isValidStructureDefinition(resource, file)) + return FileAndResource.of(file, resource); + else if (fhirConfig.isTask(resource) && isValidTask(resource, file, localOrganizationIdentifierValue)) + return FileAndResource.of(file, resource); + else if (fhirConfig.isValueSet(resource) && isValidValueSet(resource, file)) + return FileAndResource.of(file, resource); else { logger.warn( @@ -1060,47 +1006,39 @@ else if (resource instanceof ValueSet v && isValid(v, file)) }; } - private IParser newParser(String file) - { - if (file.endsWith(JSON_SUFFIX)) - return fhirContext.newJsonParser(); - else if (file.endsWith(XML_SUFFIX)) - return fhirContext.newXmlParser(); - else - throw new IllegalArgumentException("FHIR resource filename not ending in .json or .xml"); - } - - private boolean isValidMetadataResouce(MetadataResource resource, String file) + private boolean isValidMetadataResouce(Object resource, String file) { - boolean urlOk = resource.hasUrl(); - boolean versionDefined = resource.hasVersion(); - boolean versionOk = versionDefined && resource.getVersion().equals(getDefinitionResourceVersion()); + boolean urlOk = fhirConfig.hasMetadataResourceUrl(resource); + boolean versionDefined = fhirConfig.hasMetadataresourceVersion(resource); + boolean versionOk = versionDefined && fhirConfig.getMetadataResourceVersion(resource) + .map(v -> v.equals(getDefinitionResourceVersion())).orElse(false); if (!urlOk) { logger.warn("Ignoring FHIR resource {} from process plugin {}-{}: {}.url empty", file, getDefinitionName(), - getDefinitionVersion(), resource.getResourceType().name()); + getDefinitionVersion(), fhirConfig.getResourceName(resource).orElse("")); } if (!versionDefined) { logger.warn("Ignoring FHIR resource {} from process plugin {}-{}: {}.version empty", file, - getDefinitionName(), getDefinitionVersion(), resource.getResourceType().name()); + getDefinitionName(), getDefinitionVersion(), fhirConfig.getResourceName(resource).orElse("")); } else if (!versionOk) { logger.warn("Ignoring FHIR resource {} from process plugin {}-{}: {}.version not equal to {} but {}", file, - getDefinitionName(), getDefinitionVersion(), resource.getResourceType().name(), - getDefinitionResourceVersion(), resource.getVersion()); + getDefinitionName(), getDefinitionVersion(), fhirConfig.getResourceName(resource).orElse(""), + getDefinitionResourceVersion(), fhirConfig.getMetadataResourceVersion(resource).orElse("")); } return urlOk && versionOk; } - private boolean isValid(ActivityDefinition resource, String file) + private boolean isValidActivityDefinition(Object resource, String file) { boolean metadataResourceOk = isValidMetadataResouce(resource, file); - boolean urlOk = ACTIVITY_DEFINITION_URL_PATTERN.matcher(resource.getUrl()).matches(); + boolean urlOk = fhirConfig.getActivityDefinitionUrl(resource) + .map(u -> ACTIVITY_DEFINITION_URL_PATTERN.matcher(u).matches()).orElse(false); if (!urlOk) { @@ -1111,27 +1049,27 @@ private boolean isValid(ActivityDefinition resource, String file) return metadataResourceOk && urlOk; } - private boolean isValid(CodeSystem resource, String file) + private boolean isValidCodeSystem(Object resource, String file) { // TODO add additional validation steps return isValidMetadataResouce(resource, file); } - private boolean isValid(Library resource, String file) + private boolean isValidLibrary(Object resource, String file) { // TODO add additional validation steps return isValidMetadataResouce(resource, file); } - private boolean isValid(Measure resource, String file) + private boolean isValidMeasure(Object resource, String file) { // TODO add additional validation steps return isValidMetadataResouce(resource, file); } - private boolean isValid(NamingSystem resource, String file) + private boolean isValidNamingSystem(Object resource, String file) { - boolean nameOk = resource.hasName(); + boolean nameOk = fhirConfig.hasNamingSystemName(resource); if (!nameOk) { @@ -1142,72 +1080,73 @@ private boolean isValid(NamingSystem resource, String file) return nameOk; } - private boolean isValid(Questionnaire resource, String file) + private boolean isValidQuestionnaire(Object resource, String file) { // TODO add additional validation steps return isValidMetadataResouce(resource, file); } - private boolean isValid(StructureDefinition resource, String file) + private boolean isValidStructureDefinition(Object resource, String file) { // TODO add additional validation steps return isValidMetadataResouce(resource, file); } - private boolean isValid(Task resource, String file, String localOrganizationIdentifierValue) + private boolean isValidTask(Object resource, String file, String localOrganizationIdentifierValue) { - Optional identifier = TaskIdentifier.findFirst(resource); + Optional identifier = fhirConfig.getTaskIdentifier(resource); boolean identifierOk = false; if (identifier.isEmpty()) { logger.warn("Ignoring FHIR resource {} from process plugin {}-{}: No Task.identifier with system '{}'", - file, getDefinitionName(), getDefinitionVersion(), TaskIdentifier.SID); + file, getDefinitionName(), getDefinitionVersion(), fhirConfig.getTaskIdentifierSid()); } else { - identifierOk = identifier.get().hasValue() && !identifier.get().getValue().contains("|"); + + identifierOk = identifier.flatMap(Identifier::value).isPresent() + && !identifier.flatMap(Identifier::value).get().contains("|"); if (!identifierOk) logger.warn( "Ignoring FHIR resource {} from process plugin {}-{}: No Task.identifier with system '{}' and value, or value contains | character", - file, getDefinitionName(), getDefinitionVersion(), TaskIdentifier.SID); + file, getDefinitionName(), getDefinitionVersion(), fhirConfig.getTaskIdentifierSid()); // Additional checks see instantiatesCanonicalMatchesProcessIdAndIdentifierValid(...) } - boolean statusOk = TaskStatus.DRAFT.equals(resource.getStatus()); + boolean statusOk = fhirConfig.isTaskStatusDraft(resource); if (!statusOk) { logger.warn("Ignoring FHIR resource {} from process plugin {}-{}: Task.status not '{}'", file, - getDefinitionName(), getDefinitionVersion(), TaskStatus.DRAFT.toCode()); + getDefinitionName(), getDefinitionVersion(), fhirConfig.getTaskStatusDraftCode()); } boolean requesterOk = false; - if (!resource.hasRequester()) + if (fhirConfig.getTaskRequester(resource).isEmpty()) { logger.warn("Ignoring FHIR resource {} from process plugin {}-{}: Task.requester not defined", file, getDefinitionName(), getDefinitionVersion()); } else { - requesterOk = isLocalOrganization(resource.getRequester(), "requester", file, + requesterOk = isLocalOrganization(fhirConfig.getTaskRequester(resource).get(), "requester", file, localOrganizationIdentifierValue); } boolean recipientOk = false; - if (!resource.hasRestriction() || !resource.getRestriction().hasRecipient() - || resource.getRestriction().getRecipient().size() != 1) + if (fhirConfig.getTaskRecipient(resource).isEmpty()) { logger.warn("Ignoring FHIR resource {} from process plugin {}-{}: Task.restriction.recipient not defined", file, getDefinitionName(), getDefinitionVersion()); } else { - recipientOk = isLocalOrganization(resource.getRestriction().getRecipientFirstRep(), "restriction.recipient", + recipientOk = isLocalOrganization(fhirConfig.getTaskRecipient(resource).get(), "restriction.recipient", file, localOrganizationIdentifierValue); } - boolean instantiatesCanonicalOk = INSTANTIATES_CANONICAL_PATTERN.matcher(resource.getInstantiatesCanonical()) - .matches(); + boolean instantiatesCanonicalOk = fhirConfig.getTaskInstantiatesCanonical(resource) + .map(ic -> INSTANTIATES_CANONICAL_PATTERN.matcher(ic).matches()).orElse(false); if (!instantiatesCanonicalOk) { logger.warn( @@ -1217,30 +1156,26 @@ private boolean isValid(Task resource, String file, String localOrganizationIden } boolean inputOk = false; - if (!resource.hasInput()) + if (!fhirConfig.hasTaskInput(resource)) { logger.warn( "Ignoring FHIR resource {} from process plugin {}-{}: Task.input empty, input parameter with {}|{} expected", - file, getDefinitionName(), getDefinitionVersion(), CodeSystems.BpmnMessage.URL, - CodeSystems.BpmnMessage.Codes.MESSAGE_NAME); + file, getDefinitionName(), getDefinitionVersion(), + fhirConfig.getTaskInputParameterMessageNameSystem(), + fhirConfig.getTaskInputParameterMessageNameCode()); } else { - inputOk = resource - .getInput().stream().filter( - i -> i.getType().getCoding().stream() - .anyMatch(c -> CodeSystems.BpmnMessage.URL.equals(c.getSystem()) - && CodeSystems.BpmnMessage.Codes.MESSAGE_NAME.equals(c.getCode()))) - .count() == 1; - + inputOk = fhirConfig.hasTaskInputMessageName(resource); if (!inputOk) logger.warn( "Ignoring FHIR resource {} from process plugin {}-{}: One input parameter with {}|{} expected", - file, getDefinitionName(), getDefinitionVersion(), CodeSystems.BpmnMessage.URL, - CodeSystems.BpmnMessage.Codes.MESSAGE_NAME); + file, getDefinitionName(), getDefinitionVersion(), + fhirConfig.getTaskInputParameterMessageNameSystem(), + fhirConfig.getTaskInputParameterMessageNameCode()); } - boolean outputOk = !resource.hasOutput(); + boolean outputOk = !fhirConfig.hasTaskOutput(resource); if (!outputOk) { logger.warn("Ignoring FHIR resource {} from process plugin {}-{}: Task.output not empty", file, @@ -1261,21 +1196,22 @@ private boolean isLocalOrganization(Reference reference, String refLocation, Str return false; } - boolean typeOk = ResourceType.Organization.name().equals(reference.getType()); - boolean identifierSystemOk = reference.hasIdentifier() - && OrganizationIdentifier.SID.equals(reference.getIdentifier().getSystem()); - boolean identifierValueOk = reference.hasIdentifier() - && localOrganizationIdentifierValue.equals(reference.getIdentifier().getValue()); + boolean typeOk = reference.types().map(t -> ORGANIZATION_RESOURCE_TYPE_NAME.equals(t)).orElse(false); + boolean identifierSystemOk = reference.system().map(s -> fhirConfig.getOrganizationIdentifierSid().equals(s)) + .orElse(false); + boolean identifierValueOk = reference.value().map(v -> localOrganizationIdentifierValue.equals(v)) + .orElse(false); if (!typeOk) { logger.warn("Ignoring FHIR resource {} from process plugin {}-{}: Task.{}.type not '{}'", file, - getDefinitionName(), getDefinitionVersion(), refLocation, ResourceType.Organization.name()); + getDefinitionName(), getDefinitionVersion(), refLocation, ORGANIZATION_RESOURCE_TYPE_NAME); } if (!identifierSystemOk) { logger.warn("Ignoring FHIR resource {} from process plugin {}-{}: Task.{}.identifier.system not '{}'", file, - getDefinitionName(), getDefinitionVersion(), refLocation, OrganizationIdentifier.SID); + getDefinitionName(), getDefinitionVersion(), refLocation, + fhirConfig.getOrganizationIdentifierSid()); } if (!identifierValueOk) { @@ -1286,7 +1222,7 @@ private boolean isLocalOrganization(Reference reference, String refLocation, Str return typeOk && identifierSystemOk && identifierValueOk; } - private boolean isValid(ValueSet resource, String file) + private boolean isValidValueSet(Object resource, String file) { // TODO add additional validation steps return isValidMetadataResouce(resource, file); @@ -1317,7 +1253,7 @@ private Predicate hasMatchingActivityDefinition( } List definitions = resources.stream() - .filter(r -> r.getResource() instanceof ActivityDefinition).toList(); + .filter(r -> fhirConfig.isActivityDefinition(r.getResource())).toList(); if (definitions.size() != 1) { @@ -1329,10 +1265,14 @@ private Predicate hasMatchingActivityDefinition( return false; } - String url = ((ActivityDefinition) definitions.get(0).getResource()).getUrl(); - Matcher urlMatcher = ACTIVITY_DEFINITION_URL_PATTERN.matcher(url); - if (urlMatcher.matches()) + return fhirConfig.getActivityDefinitionUrl(definitions.get(0).getResource()).map(url -> { + Matcher urlMatcher = ACTIVITY_DEFINITION_URL_PATTERN.matcher(url); + if (!urlMatcher.matches()) + throw new IllegalStateException("ActivityDefinition " + definitions.get(0).getFile() + + " from process plugin " + getDefinitionName() + "-" + getDefinitionVersion() + + " has url not matching " + ACTIVITY_DEFINITION_URL_PATTERN_STRING); + String processDomain = urlMatcher.group("domain").replace(".", ""); String processName = urlMatcher.group("processName"); String processId = processDomain + "_" + processName; @@ -1346,9 +1286,9 @@ private Predicate hasMatchingActivityDefinition( return false; } - } - return true; + return true; + }).orElse(false); }; } @@ -1366,7 +1306,7 @@ private List filterTasksNotMatchingProcessId( { return entry.getValue().stream().filter(fileAndResource -> { - if (fileAndResource.getResource() instanceof Task) + if (fhirConfig.isTask(fileAndResource.getResource())) return instantiatesCanonicalMatchesProcessIdAndIdentifierValid(entry.getKey(), fileAndResource); else return true; @@ -1376,9 +1316,10 @@ private List filterTasksNotMatchingProcessId( private boolean instantiatesCanonicalMatchesProcessIdAndIdentifierValid( ProcessIdAndVersion expectedProcessIdAndVersion, FileAndResource fileAndResource) { - String instantiatesCanonical = ((Task) fileAndResource.getResource()).getInstantiatesCanonical(); - String identifierValue = TaskIdentifier.findFirst((Task) fileAndResource.getResource()) - .map(Identifier::getValue).get(); + String instantiatesCanonical = fhirConfig.getTaskInstantiatesCanonical(fileAndResource.getResource()) + .orElse(""); + String identifierValue = fhirConfig.getTaskIdentifier(fileAndResource.getResource()).flatMap(Identifier::value) + .orElse(""); Matcher instantiatesCanonicalMatcher = INSTANTIATES_CANONICAL_PATTERN.matcher(instantiatesCanonical); if (instantiatesCanonicalMatcher.matches()) @@ -1425,4 +1366,28 @@ private boolean instantiatesCanonicalMatchesProcessIdAndIdentifierValid( // no log, already tested return false; } + + protected final List getActivePluginProcesses(Set allActiveProcesses) + { + return getProcessKeysAndVersions().stream().filter(allActiveProcesses::contains).map(ProcessIdAndVersion::getId) + .toList(); + } + + protected final void handleProcessPluginDeploymentStateListenerError(Runnable listener, Class interfaceType, + Class implementationType) + { + try + { + listener.run(); + } + catch (Exception e) + { + logger.debug("Error while executing {} bean of type {}, process plugin {}-{} from {}", + interfaceType.getName(), implementationType.getName(), getDefinitionName(), getDefinitionVersion(), + getJarFile().toString(), e); + logger.error("Error while executing {} bean of type {}, process plugin {}-{} from {}: {} - {}", + interfaceType.getName(), implementationType.getName(), getDefinitionName(), getDefinitionVersion(), + getJarFile(), e.getClass().getName(), e.getMessage()); + } + } } diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/AbstractProcessPluginFactory.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/AbstractProcessPluginFactory.java new file mode 100644 index 000000000..86c3e557d --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/AbstractProcessPluginFactory.java @@ -0,0 +1,116 @@ +package dev.dsf.bpe.api.plugin; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.ServiceLoader.Provider; +import java.util.stream.Collectors; + +import org.camunda.bpm.engine.delegate.TaskListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +public abstract class AbstractProcessPluginFactory implements ProcessPluginFactory, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractProcessPluginFactory.class); + + public static final String SNAPSHOT_FILE_SUFFIX = "-SNAPSHOT.jar"; + public static final String MILESTONE_FILE_PATTERN = ".*-M[0-9]+.jar"; + public static final String RELEASE_CANDIDATE_FILE_PATTERN = ".*-RC[0-9]+.jar"; + + private final int apiVersion; + private final ClassLoader apiClassLoader; + protected final ApplicationContext apiApplicationContext; + protected final ConfigurableEnvironment environment; + private final Class processPluginDefinitionType; + private final Class defaultUserTaskListener; + + public AbstractProcessPluginFactory(int apiVersion, ClassLoader apiClassLoader, + ApplicationContext apiApplicationContext, ConfigurableEnvironment environment, + Class processPluginDefinitionType, Class defaultUserTaskListener) + { + this.apiVersion = apiVersion; + this.apiClassLoader = apiClassLoader; + this.apiApplicationContext = apiApplicationContext; + this.environment = environment; + this.processPluginDefinitionType = processPluginDefinitionType; + this.defaultUserTaskListener = defaultUserTaskListener; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(apiClassLoader, "apiClassLoader"); + Objects.requireNonNull(apiApplicationContext, "apiApplicationContext"); + Objects.requireNonNull(environment, "environment"); + Objects.requireNonNull(processPluginDefinitionType, "processPluginDefinitionType"); + Objects.requireNonNull(defaultUserTaskListener, "defaultUserTaskListener"); + } + + @Override + public int getApiVersion() + { + return apiVersion; + } + + @Override + public Class getDefaultUserTaskListener() + { + return defaultUserTaskListener; + } + + @Override + public ProcessPlugin load(Path jar) + { + try + { + URLClassLoader pluginClassLoader = new URLClassLoader(jar.getFileName().toString(), + new URL[] { toUrl(jar) }, apiClassLoader); + + List> definitions = ServiceLoader.load(processPluginDefinitionType, pluginClassLoader).stream() + .collect(Collectors.toList()); + + if (definitions.size() != 1) + return null; + + String filename = jar.getFileName().toString(); + boolean isSnapshot = filename.endsWith(SNAPSHOT_FILE_SUFFIX); + boolean isMilestone = filename.matches(MILESTONE_FILE_PATTERN); + boolean isReleaseCandidate = filename.matches(RELEASE_CANDIDATE_FILE_PATTERN); + + boolean draft = isSnapshot || isMilestone || isReleaseCandidate; + + return createProcessPlugin(definitions.get(0).get(), draft, jar, pluginClassLoader); + } + catch (Exception e) + { + logger.debug("Ignoring {}: Unable to load process plugin", jar.toString(), e); + logger.warn("Ignoring {}: Unable to load process plugin: {} - {}", jar.toString(), e.getClass().getName(), + e.getMessage()); + + return null; + } + } + + protected abstract ProcessPlugin createProcessPlugin(Object processPluginDefinition, boolean draft, Path jarFile, + URLClassLoader pluginClassLoader); + + private URL toUrl(Path p) + { + try + { + return p.toUri().toURL(); + } + catch (MalformedURLException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnFileAndModel.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/BpmnFileAndModel.java similarity index 65% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnFileAndModel.java rename to dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/BpmnFileAndModel.java index 946b2eb06..0f2f01fbc 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnFileAndModel.java +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/BpmnFileAndModel.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.plugin; +package dev.dsf.bpe.api.plugin; import java.nio.file.Path; @@ -6,19 +6,26 @@ public final class BpmnFileAndModel { + private final int processPluginApiVersion; private final boolean draft; private final String file; private final BpmnModelInstance model; private final Path jar; - public BpmnFileAndModel(boolean draft, String file, BpmnModelInstance model, Path jar) + public BpmnFileAndModel(int processPluginApiVersion, boolean draft, String file, BpmnModelInstance model, Path jar) { + this.processPluginApiVersion = processPluginApiVersion; this.draft = draft; this.file = file; this.model = model; this.jar = jar; } + public int getProcessPluginApiVersion() + { + return processPluginApiVersion; + } + public boolean isDraft() { return draft; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessIdAndVersion.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessIdAndVersion.java similarity index 98% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessIdAndVersion.java rename to dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessIdAndVersion.java index 49db5f045..6ad1e37e7 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessIdAndVersion.java +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessIdAndVersion.java @@ -1,4 +1,4 @@ -package dev.dsf.bpe.plugin; +package dev.dsf.bpe.api.plugin; import java.util.Comparator; import java.util.List; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPlugin.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPlugin.java similarity index 55% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPlugin.java rename to dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPlugin.java index ed47d7d44..8f62575b3 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPlugin.java +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPlugin.java @@ -1,24 +1,23 @@ -package dev.dsf.bpe.plugin; +package dev.dsf.bpe.api.plugin; import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.camunda.bpm.engine.impl.variable.serializer.TypedValueSerializer; -import org.hl7.fhir.r4.model.Resource; +import org.camunda.bpm.engine.variable.value.PrimitiveValue; import org.springframework.context.ApplicationContext; -public interface ProcessPlugin +public interface ProcessPlugin { String MODEL_ATTRIBUTE_PROCESS_API_VERSION = "dsf.process.api.version"; boolean initializeAndValidateResources(String localOrganizationIdentifierValue); - D getProcessPluginDefinition(); + PrimitiveValue createFhirTaskVariable(String taskJson); - A getProcessPluginApi(); - - boolean isDraft(); + PrimitiveValue createFhirQuestionnaireResponseVariable(String questionnaireResponseJson); Path getJarFile(); @@ -27,12 +26,13 @@ public interface ProcessPlugin ApplicationContext getApplicationContext(); @SuppressWarnings("rawtypes") - List getTypedValueSerializers(); + Stream getTypedValueSerializers(); List getProcessKeysAndVersions(); - Map> getFhirResources(); + Map> getFhirResources(); List getProcessModels(); + ProcessPluginDeploymentListener getProcessPluginDeploymentListener(); } diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginApiBuilder.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginApiBuilder.java new file mode 100644 index 000000000..2eae5acae --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginApiBuilder.java @@ -0,0 +1,12 @@ +package dev.dsf.bpe.api.plugin; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +public interface ProcessPluginApiBuilder +{ + ProcessPluginFactory build(ClassLoader apiClassLoader, ApplicationContext apiApplicationContext, + ConfigurableEnvironment environment); + + Class getSpringServiceConfigClass(); +} diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginDeploymentListener.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginDeploymentListener.java new file mode 100644 index 000000000..6f3d0d0b2 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginDeploymentListener.java @@ -0,0 +1,9 @@ +package dev.dsf.bpe.api.plugin; + +import java.util.Set; + +@FunctionalInterface +public interface ProcessPluginDeploymentListener +{ + void onProcessesDeployed(Set allActiveProcesses); +} diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginDeploymentListenerImpl.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginDeploymentListenerImpl.java new file mode 100644 index 000000000..a8dab1583 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginDeploymentListenerImpl.java @@ -0,0 +1,61 @@ +package dev.dsf.bpe.api.plugin; + +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; + +public class ProcessPluginDeploymentListenerImpl implements ProcessPluginDeploymentListener +{ + private static final Logger logger = LoggerFactory.getLogger(ProcessPluginDeploymentListenerImpl.class); + + private final Supplier applicationContext; + private final Supplier> processKeysAndVersions; + private final Class listenerClass; + private final BiConsumer> onProcessesDeployed; + + public ProcessPluginDeploymentListenerImpl(Supplier applicationContext, + Supplier> processKeysAndVersions, Class listenerClass, + BiConsumer> onProcessesDeployed) + { + this.applicationContext = Objects.requireNonNull(applicationContext, "applicationContext"); + this.processKeysAndVersions = Objects.requireNonNull(processKeysAndVersions, "processKeysAndVersions"); + this.listenerClass = Objects.requireNonNull(listenerClass, "listenerClass"); + this.onProcessesDeployed = Objects.requireNonNull(onProcessesDeployed, "onProcessesDeployed"); + } + + @Override + public void onProcessesDeployed(Set allActiveProcesses) + { + List activePluginProcesses = processKeysAndVersions.get().stream().filter(allActiveProcesses::contains) + .map(ProcessIdAndVersion::getId).toList(); + + applicationContext.get().getBeansOfType(listenerClass).entrySet() + .forEach(executeOnProcessesDeployed(activePluginProcesses)); + } + + private Consumer> executeOnProcessesDeployed(List activePluginProcesses) + { + return entry -> + { + try + { + onProcessesDeployed.accept(entry.getValue(), activePluginProcesses); + } + catch (Exception e) + { + logger.debug("Error while executing {} bean of type {}", entry.getKey(), + entry.getValue().getClass().getName(), e); + logger.warn("Error while executing {} bean of type {}: {} - {}", entry.getKey(), + entry.getValue().getClass().getName(), e.getClass().getName(), e.getMessage()); + } + }; + } +} diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginFactory.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginFactory.java new file mode 100644 index 000000000..1886345f4 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginFactory.java @@ -0,0 +1,23 @@ +package dev.dsf.bpe.api.plugin; + +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.camunda.bpm.engine.delegate.TaskListener; +import org.camunda.bpm.engine.impl.variable.serializer.TypedValueSerializer; + +import dev.dsf.bpe.api.listener.ListenerFactory; + +public interface ProcessPluginFactory +{ + int getApiVersion(); + + @SuppressWarnings("rawtypes") + Stream getSerializer(); + + ListenerFactory getListenerFactory(); + + Class getDefaultUserTaskListener(); + + ProcessPlugin load(Path jar); +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginFhirConfig.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginFhirConfig.java new file mode 100644 index 000000000..e13e8debe --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/plugin/ProcessPluginFhirConfig.java @@ -0,0 +1,276 @@ +package dev.dsf.bpe.api.plugin; + +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +public final class ProcessPluginFhirConfig +{ + public static final record Identifier(Optional system, Optional value) + { + } + + public static final record Reference(Optional system, Optional value, Optional types) + { + } + + private final Class activityDefinitionClass; + private final Class codeSystemClass; + private final Class libraryClass; + private final Class measureClass; + private final Class namingSystemClass; + private final Class questionnaireClass; + private final Class structureDefinitionClass; + private final Class taskClass; + private final Class valueSetClass; + + private final String organizationIdentifierSid; + private final String taskIdentifierSid; + private final String taskStatusDraftCode; + private final String taskInputParameterMessageNameSystem; + private final String taskInputParameterMessageNameCode; + + private final BiFunction parseResource; + private final Function encodeResource; + private final Function> getResourceName; + + private final Predicate hasMetadataresourceVersion; + private final Predicate hasMetadataResourceUrl; + private final Function> getMetadataResourceVersion; + + private final Function> getActivityDefinitionUrl; + private final Predicate hasNamingSystemName; + private final Function> getTaskInstantiatesCanonical; + private final Function> getTaskIdentifier; + private final Predicate isTaskStatusDraft; + private final Function> getTaskRequester; + private final Function> getTaskRecipient; + private final Predicate hasTaskInput; + private final Predicate hasTaskInputMessageName; + private final Predicate hasTaskOutput; + + public ProcessPluginFhirConfig(Class activityDefinitionClass, Class codeSystemClass, Class libraryClass, + Class measureClass, Class namingSystemClass, Class questionnaireClass, + Class structureDefinitionClass, Class taskClass, Class valueSetClass, + + String organizationIdentifierSid, String taskIdentifierSid, String taskStatusDraftCode, + String taskInputParameterMessageNameSystem, String taskInputParameterMessageNameCode, + + BiFunction parseResource, Function encodeResource, + Function> getResourceName, Predicate hasMetadataResourceUrl, + Predicate hasMetadataResourceVersion, Function> getMetadataResourceVersion, + + Function> getActivityDefinitionUrl, Predicate hasNamingSystemName, + Function> getTaskInstantiatesCanonical, + Function> getTaskIdentifier, Predicate isTaskStatusDraft, + Function> getTaskRequester, Function> getTaskRecipient, + Predicate hasTaskInput, Predicate hasTaskInputMessageName, Predicate hasTaskOutput) + { + this.activityDefinitionClass = activityDefinitionClass; + this.codeSystemClass = codeSystemClass; + this.libraryClass = libraryClass; + this.measureClass = measureClass; + this.namingSystemClass = namingSystemClass; + this.questionnaireClass = questionnaireClass; + this.structureDefinitionClass = structureDefinitionClass; + this.taskClass = taskClass; + this.valueSetClass = valueSetClass; + + this.organizationIdentifierSid = organizationIdentifierSid; + this.taskIdentifierSid = taskIdentifierSid; + this.taskStatusDraftCode = taskStatusDraftCode; + this.taskInputParameterMessageNameSystem = taskInputParameterMessageNameSystem; + this.taskInputParameterMessageNameCode = taskInputParameterMessageNameCode; + + this.parseResource = parseResource; + this.encodeResource = encodeResource; + this.getResourceName = getResourceName; + + this.hasMetadataResourceUrl = hasMetadataResourceUrl; + this.hasMetadataresourceVersion = hasMetadataResourceVersion; + this.getMetadataResourceVersion = getMetadataResourceVersion; + + this.getActivityDefinitionUrl = getActivityDefinitionUrl; + this.hasNamingSystemName = hasNamingSystemName; + this.getTaskInstantiatesCanonical = getTaskInstantiatesCanonical; + this.getTaskIdentifier = getTaskIdentifier; + this.isTaskStatusDraft = isTaskStatusDraft; + this.getTaskRequester = getTaskRequester; + this.getTaskRecipient = getTaskRecipient; + this.hasTaskInput = hasTaskInput; + this.hasTaskInputMessageName = hasTaskInputMessageName; + this.hasTaskOutput = hasTaskOutput; + } + + public String getOrganizationIdentifierSid() + { + return organizationIdentifierSid; + } + + public String getTaskIdentifierSid() + { + return taskIdentifierSid; + } + + public String getTaskStatusDraftCode() + { + return taskStatusDraftCode; + } + + public String getTaskInputParameterMessageNameSystem() + { + return taskInputParameterMessageNameSystem; + } + + public String getTaskInputParameterMessageNameCode() + { + return taskInputParameterMessageNameCode; + } + + public Object parseResource(String filename, String content) + { + return parseResource.apply(filename, content); + } + + public byte[] encodeResource(Object resource) + { + if (isResource(resource)) + return encodeResource.apply(resource); + else + throw new IllegalArgumentException( + "Given resource of type " + resource.getClass().getName() + " not a supported FHIR resource"); + } + + public boolean isActivityDefinition(Object resource) + { + return resource != null && activityDefinitionClass.isInstance(resource); + } + + public boolean isCodeSystem(Object resource) + { + return resource != null && codeSystemClass.isInstance(resource); + } + + public boolean isLibrary(Object resource) + { + return resource != null && libraryClass.isInstance(resource); + } + + public boolean isMeasure(Object resource) + { + return resource != null && measureClass.isInstance(resource); + } + + public boolean isNamingSystem(Object namingSystem) + { + return namingSystem != null && namingSystemClass.isInstance(namingSystem); + } + + public boolean isQuestionnaire(Object task) + { + return task != null && questionnaireClass.isInstance(task); + } + + public boolean isStructureDefinition(Object task) + { + return task != null && structureDefinitionClass.isInstance(task); + } + + public boolean isTask(Object task) + { + return task != null && taskClass.isInstance(task); + } + + public boolean isValueSet(Object task) + { + return task != null && valueSetClass.isInstance(task); + } + + private boolean isMetadataResource(Object metadataResource) + { + return metadataResource != null && (isActivityDefinition(metadataResource) || isCodeSystem(metadataResource) + || isLibrary(metadataResource) || isMeasure(metadataResource) || isQuestionnaire(metadataResource) + || isStructureDefinition(metadataResource) || isValueSet(metadataResource)); + } + + private boolean isResource(Object resource) + { + return resource != null && (isActivityDefinition(resource) || isCodeSystem(resource) || isLibrary(resource) + || isMeasure(resource) || isNamingSystem(resource) || isQuestionnaire(resource) + || isStructureDefinition(resource) || isTask(resource) || isValueSet(resource)); + } + + public Optional getActivityDefinitionUrl(Object activityDefinition) + { + return isActivityDefinition(activityDefinition) + ? getActivityDefinitionUrl.apply(activityDefinitionClass.cast(activityDefinition)) + : Optional.empty(); + } + + public Optional getTaskInstantiatesCanonical(Object task) + { + return isTask(task) ? getTaskInstantiatesCanonical.apply(taskClass.cast(task)) : Optional.empty(); + } + + public boolean hasMetadataResourceUrl(Object metadataResource) + { + return isMetadataResource(metadataResource) && hasMetadataResourceUrl.test(metadataResource); + } + + public boolean hasMetadataresourceVersion(Object metadataResource) + { + return isMetadataResource(metadataResource) && hasMetadataresourceVersion.test(metadataResource); + } + + public Optional getMetadataResourceVersion(Object metadataResource) + { + return isMetadataResource(metadataResource) ? getMetadataResourceVersion.apply(metadataResource) + : Optional.empty(); + } + + public Optional getResourceName(Object resource) + { + return resource != null && isResource(resource) ? getResourceName.apply(resource) : Optional.empty(); + } + + public boolean hasNamingSystemName(Object namingSystem) + { + return isNamingSystem(namingSystem) && hasNamingSystemName.test(namingSystemClass.cast(namingSystem)); + } + + public Optional getTaskIdentifier(Object task) + { + return isTask(task) ? getTaskIdentifier.apply(taskClass.cast(task)) : Optional.empty(); + } + + public boolean isTaskStatusDraft(Object task) + { + return isTask(task) && isTaskStatusDraft.test(taskClass.cast(task)); + } + + public Optional getTaskRequester(Object task) + { + return isTask(task) ? getTaskRequester.apply(taskClass.cast(task)) : Optional.empty(); + } + + public Optional getTaskRecipient(Object task) + { + return isTask(task) ? getTaskRecipient.apply(taskClass.cast(task)) : Optional.empty(); + } + + public boolean hasTaskInput(Object task) + { + return isTask(task) && hasTaskInput.test(taskClass.cast(task)); + } + + public boolean hasTaskInputMessageName(Object task) + { + return isTask(task) && hasTaskInputMessageName.test(taskClass.cast(task)); + } + + public boolean hasTaskOutput(Object task) + { + return isTask(task) && hasTaskOutput.test(taskClass.cast(task)); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/service/BpeMailService.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/service/BpeMailService.java new file mode 100644 index 000000000..f6b740967 --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/service/BpeMailService.java @@ -0,0 +1,155 @@ +package dev.dsf.bpe.api.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 BpeMailService +{ + /** + * 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/src/main/java/dev/dsf/bpe/api/service/BuildInfoProvider.java b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/service/BuildInfoProvider.java new file mode 100644 index 000000000..6cb9bf67e --- /dev/null +++ b/dsf-bpe/dsf-bpe-process-api/src/main/java/dev/dsf/bpe/api/service/BuildInfoProvider.java @@ -0,0 +1,6 @@ +package dev.dsf.bpe.api.service; + +public interface BuildInfoProvider +{ + String getProjectVersion(); +} diff --git a/dsf-bpe/dsf-bpe-server-jetty/api/v1/README.md b/dsf-bpe/dsf-bpe-server-jetty/api/v1/README.md new file mode 100644 index 000000000..1cde66f27 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server-jetty/api/v1/README.md @@ -0,0 +1 @@ +Empty v1 directory for jar-files used in dev setup diff --git a/dsf-bpe/dsf-bpe-server-jetty/api/v2/README.md b/dsf-bpe/dsf-bpe-server-jetty/api/v2/README.md new file mode 100644 index 000000000..851b656a2 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server-jetty/api/v2/README.md @@ -0,0 +1 @@ +Empty v2 directory for jar-files used in dev setup diff --git a/dsf-bpe/dsf-bpe-server-jetty/conf/log4j2.xml b/dsf-bpe/dsf-bpe-server-jetty/conf/log4j2.xml index 0c1747aa9..59f314620 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/conf/log4j2.xml +++ b/dsf-bpe/dsf-bpe-server-jetty/conf/log4j2.xml @@ -17,6 +17,7 @@ + diff --git a/dsf-bpe/dsf-bpe-server-jetty/docker/.dockerignore b/dsf-bpe/dsf-bpe-server-jetty/docker/.dockerignore index 41fcacd84..e128606f6 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/docker/.dockerignore +++ b/dsf-bpe/dsf-bpe-server-jetty/docker/.dockerignore @@ -1,5 +1,7 @@ .dockerignore Dockerfile +api/v1/README.md +api/v2/README.md lib/README.md lib_external/README.md log/README.md diff --git a/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile b/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile index 3a8c7d93e..01f84e0d8 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile +++ b/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile @@ -3,7 +3,7 @@ RUN adduser --system --no-create-home --group --uid 2202 java WORKDIR /opt/bpe COPY --chown=root:java ./ ./ RUN chown root:java ./ && \ - chmod 750 ./ ./conf ./lib ./lib_external ./process ./ui ./dsf_bpe_start.sh ./healthcheck.sh && \ + chmod 750 ./ ./api ./api/v1 ./api/v2 ./conf ./lib ./lib_external ./process ./ui ./dsf_bpe_start.sh ./healthcheck.sh && \ chmod 440 ./conf/log4j2.xml ./dsf_bpe.jar ./lib/*.jar && \ chmod 1775 ./log diff --git a/dsf-bpe/dsf-bpe-server-jetty/docker/api/v1/README.md b/dsf-bpe/dsf-bpe-server-jetty/docker/api/v1/README.md new file mode 100644 index 000000000..815964cb3 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server-jetty/docker/api/v1/README.md @@ -0,0 +1 @@ +Empty v1 directory for jar-files used in docker container diff --git a/dsf-bpe/dsf-bpe-server-jetty/docker/api/v2/README.md b/dsf-bpe/dsf-bpe-server-jetty/docker/api/v2/README.md new file mode 100644 index 000000000..810bcae0c --- /dev/null +++ b/dsf-bpe/dsf-bpe-server-jetty/docker/api/v2/README.md @@ -0,0 +1 @@ +Empty v2 directory for jar-files used in docker container diff --git a/dsf-bpe/dsf-bpe-server-jetty/docker/conf/log4j2.xml b/dsf-bpe/dsf-bpe-server-jetty/docker/conf/log4j2.xml index bc09f21e4..ace0d5f9f 100644 --- a/dsf-bpe/dsf-bpe-server-jetty/docker/conf/log4j2.xml +++ b/dsf-bpe/dsf-bpe-server-jetty/docker/conf/log4j2.xml @@ -18,6 +18,7 @@ + diff --git a/dsf-bpe/dsf-bpe-server-jetty/pom.xml b/dsf-bpe/dsf-bpe-server-jetty/pom.xml index 0b60e3a68..d193662fa 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/pom.xml +++ b/dsf-bpe/dsf-bpe-server-jetty/pom.xml @@ -89,6 +89,436 @@ compile + + copy-api-v1-dependencies-to-docker + install + + copy + + + + + dev.dsf + dsf-bpe-process-api-v1 + + + dev.dsf + dsf-bpe-process-api-v1-impl + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + hapi-fhir-base + ${hapi.fhir.version.v1} + + + com.google.j2objc + j2objc-annotations + 2.8 + + + ca.uhn.hapi.fhir + org.hl7.fhir.utilities + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.r4 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r5 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.r5 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + hapi-fhir-validation + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + hapi-fhir-converter + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.convertors + ${hapi.fhir.version.v1} + + + net.sf.saxon + Saxon-HE + 9.5.1-5 + + + ca.uhn.hapi.fhir + org.hl7.fhir.validation + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.dstu2 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.dstu2016may + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.dstu3 + ${hapi.fhir.version.v1} + + + org.apache.commons + commons-compress + 1.27.1 + + + org.fhir + ucum + 1.0.2 + + + com.github.ben-manes.caffeine + caffeine + 2.7.0 + + + org.checkerframework + checker-qual + 2.6.0 + + + com.google.errorprone + error_prone_annotations + 2.3.3 + + + com.google.code.gson + gson + 2.11.0 + + + ca.uhn.hapi.fhir + hapi-fhir-validation-resources-r4 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + hapi-fhir-validation-resources-r5 + ${hapi.fhir.version.v1} + + + docker/api/v1 + + + + copy-api-v1-dependencies-to-server-jetty + generate-sources + + copy + + + + + dev.dsf + dsf-bpe-process-api-v1 + + + dev.dsf + dsf-bpe-process-api-v1-impl + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + hapi-fhir-base + ${hapi.fhir.version.v1} + + + com.google.j2objc + j2objc-annotations + 2.8 + + + ca.uhn.hapi.fhir + org.hl7.fhir.utilities + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.r4 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r5 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.r5 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + hapi-fhir-validation + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + hapi-fhir-converter + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.convertors + ${hapi.fhir.version.v1} + + + net.sf.saxon + Saxon-HE + 9.5.1-5 + + + ca.uhn.hapi.fhir + org.hl7.fhir.validation + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.dstu2 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.dstu2016may + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + org.hl7.fhir.dstu3 + ${hapi.fhir.version.v1} + + + org.apache.commons + commons-compress + 1.26.2 + + + org.fhir + ucum + 1.0.2 + + + com.github.ben-manes.caffeine + caffeine + 2.7.0 + + + org.checkerframework + checker-qual + 2.6.0 + + + com.google.errorprone + error_prone_annotations + 2.3.3 + + + com.google.code.gson + gson + 2.11.0 + + + ca.uhn.hapi.fhir + hapi-fhir-validation-resources-r4 + ${hapi.fhir.version.v1} + + + ca.uhn.hapi.fhir + hapi-fhir-validation-resources-r5 + ${hapi.fhir.version.v1} + + + api/v1 + + + + copy-api-v2-dependencies-to-docker + install + + copy + + + + + dev.dsf + dsf-bpe-process-api-v2 + + + dev.dsf + dsf-bpe-process-api-v2-impl + + + org.checkerframework + checker-qual + 3.43.0 + + + com.google.errorprone + error_prone_annotations + 2.28.0 + + + ca.uhn.hapi.fhir + hapi-fhir-base + ${hapi.fhir.version.v2} + + + ca.uhn.hapi.fhir + hapi-fhir-caching-api + ${hapi.fhir.version.v2} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${hapi.fhir.version.v2} + + + com.ibm.icu + icu4j + 72.1 + + + com.google.j2objc + j2objc-annotations + 3.0.0 + + + io.opentelemetry + opentelemetry-api + 1.38.0 + + + io.opentelemetry + opentelemetry-context + 1.38.0 + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-annotations + 2.4.0 + + + ca.uhn.hapi.fhir + org.hl7.fhir.r4 + 6.3.11 + + + ca.uhn.hapi.fhir + org.hl7.fhir.utilities + 6.3.11 + + + docker/api/v2 + + + + copy-api-v2-dependencies-to-server-jetty + generate-sources + + copy + + + + + dev.dsf + dsf-bpe-process-api-v2 + + + dev.dsf + dsf-bpe-process-api-v2-impl + + + org.checkerframework + checker-qual + 3.43.0 + + + com.google.errorprone + error_prone_annotations + 2.28.0 + + + ca.uhn.hapi.fhir + hapi-fhir-base + ${hapi.fhir.version.v2} + + + ca.uhn.hapi.fhir + hapi-fhir-caching-api + ${hapi.fhir.version.v2} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${hapi.fhir.version.v2} + + + com.ibm.icu + icu4j + 72.1 + + + com.google.j2objc + j2objc-annotations + 3.0.0 + + + io.opentelemetry + opentelemetry-api + 1.38.0 + + + io.opentelemetry + opentelemetry-context + 1.38.0 + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-annotations + 2.4.0 + + + ca.uhn.hapi.fhir + org.hl7.fhir.r4 + 6.3.11 + + + ca.uhn.hapi.fhir + org.hl7.fhir.utilities + 6.3.11 + + + api/v2 + + copy-server-jar-to-docker install @@ -119,6 +549,8 @@ dsf_bpe.jar lib/*.jar + api/v1/*.jar + api/v2/*.jar false @@ -129,6 +561,14 @@ false + + api + + v1/*.jar + v2/*.jar + + false + diff --git a/dsf-bpe/dsf-bpe-server-jetty/process/README.md b/dsf-bpe/dsf-bpe-server-jetty/process/README.md index 6c7aa1e6d..d63be5711 100644 --- a/dsf-bpe/dsf-bpe-server-jetty/process/README.md +++ b/dsf-bpe/dsf-bpe-server-jetty/process/README.md @@ -1 +1 @@ -Empty process directory for jar-files with process definition +Empty process directory for jar-files used in dev setup diff --git a/dsf-bpe/dsf-bpe-server/pom.xml b/dsf-bpe/dsf-bpe-server/pom.xml index 51e7cc740..6ed75ff12 100755 --- a/dsf-bpe/dsf-bpe-server/pom.xml +++ b/dsf-bpe/dsf-bpe-server/pom.xml @@ -12,16 +12,12 @@ dev.dsf - dsf-bpe-process-api-v1 + dsf-bpe-process-api dev.dsf dsf-fhir-websocket-client - - dev.dsf - dsf-fhir-webservice-client - dev.dsf dsf-tools-build-info-reader @@ -59,12 +55,6 @@ crypto-utils - - - dev.dsf - dsf-fhir-validation - - jakarta.servlet jakarta.servlet-api @@ -94,6 +84,17 @@ org.springframework spring-web + + + org.glassfish.jersey.connectors + jersey-apache-connector + + + commons-logging + commons-logging + + + org.glassfish.jersey.core diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/DelegateProvider.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/DelegateProvider.java index 8130450a7..c4251f68a 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/DelegateProvider.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/DelegateProvider.java @@ -3,7 +3,7 @@ import org.camunda.bpm.engine.delegate.TaskListener; import org.springframework.context.ApplicationContext; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; public interface DelegateProvider extends ProcessPluginConsumer { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/DelegateProviderImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/DelegateProviderImpl.java index 7fac7dc1e..b54f38289 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/DelegateProviderImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/DelegateProviderImpl.java @@ -10,17 +10,18 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; -import dev.dsf.bpe.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessPluginFactory; public class DelegateProviderImpl implements DelegateProvider, ProcessPluginConsumer, InitializingBean { private static final class ProcessByIdAndVersion { final ProcessIdAndVersion processIdAndVersion; - final ProcessPlugin plugin; + final ProcessPlugin plugin; - ProcessByIdAndVersion(ProcessIdAndVersion idAndVersion, ProcessPlugin plugin) + ProcessByIdAndVersion(ProcessIdAndVersion idAndVersion, ProcessPlugin plugin) { this.processIdAndVersion = idAndVersion; this.plugin = plugin; @@ -31,7 +32,7 @@ public ProcessIdAndVersion getProcessIdAndVersion() return processIdAndVersion; } - public ProcessPlugin getPlugin() + public ProcessPlugin getPlugin() { return plugin; } @@ -40,12 +41,19 @@ public ProcessIdAndVersion getProcessIdAndVersion() private final ClassLoader defaultClassLoader; private final ApplicationContext defaultApplicationContext; - private final Map> processPluginsByIdAndVersion = new HashMap<>(); + private final Map processPluginsByProcessIdAndVersion = new HashMap<>(); + private final Map> defaultUserTaskListenerByApiVersion; - public DelegateProviderImpl(ClassLoader mainClassLoader, ApplicationContext mainApplicationContext) + public DelegateProviderImpl(ClassLoader mainClassLoader, ApplicationContext mainApplicationContext, + List pluginFactories) { this.defaultClassLoader = mainClassLoader; this.defaultApplicationContext = mainApplicationContext; + + Objects.requireNonNull(pluginFactories, "pluginFactories"); + + defaultUserTaskListenerByApiVersion = pluginFactories.stream().collect(Collectors + .toMap(f -> String.valueOf(f.getApiVersion()), ProcessPluginFactory::getDefaultUserTaskListener)); } @Override @@ -56,9 +64,9 @@ public void afterPropertiesSet() throws Exception } @Override - public void setProcessPlugins(List> plugins) + public void setProcessPlugins(List plugins) { - processPluginsByIdAndVersion.putAll(plugins.stream() + processPluginsByProcessIdAndVersion.putAll(plugins.stream() .flatMap(plugin -> plugin.getProcessKeysAndVersions().stream() .map(idAndVersion -> new ProcessByIdAndVersion(idAndVersion, plugin))) .collect(Collectors.toMap(ProcessByIdAndVersion::getProcessIdAndVersion, @@ -71,7 +79,7 @@ public ClassLoader getClassLoader(ProcessIdAndVersion processIdAndVersion) if (processIdAndVersion == null) return defaultClassLoader; - var plugin = processPluginsByIdAndVersion.get(processIdAndVersion); + var plugin = processPluginsByProcessIdAndVersion.get(processIdAndVersion); if (plugin == null) return defaultClassLoader; @@ -85,7 +93,7 @@ public ApplicationContext getApplicationContext(ProcessIdAndVersion processIdAnd if (processIdAndVersion == null) return defaultApplicationContext; - var plugin = processPluginsByIdAndVersion.get(processIdAndVersion); + var plugin = processPluginsByProcessIdAndVersion.get(processIdAndVersion); if (plugin == null) return defaultApplicationContext; @@ -96,11 +104,12 @@ public ApplicationContext getApplicationContext(ProcessIdAndVersion processIdAnd @Override public Class getDefaultUserTaskListenerClass(String processPluginApiVersion) { - return switch (processPluginApiVersion) - { - case "1" -> dev.dsf.bpe.v1.activity.DefaultUserTaskListener.class; - default -> throw new IllegalArgumentException( - "Process plugin API version " + processPluginApiVersion + " not supported"); - }; + Class listenerClass = defaultUserTaskListenerByApiVersion.get(processPluginApiVersion); + + if (listenerClass != null) + return listenerClass; + else + throw new IllegalArgumentException( + "Process plugin api version " + processPluginApiVersion + " not supported"); } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/FallbackSerializerFactoryImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/FallbackSerializerFactoryImpl.java index 6f8846de2..4cb47fa2b 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/FallbackSerializerFactoryImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/FallbackSerializerFactoryImpl.java @@ -16,7 +16,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import dev.dsf.bpe.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessPlugin; public class FallbackSerializerFactoryImpl implements FallbackSerializerFactory { @@ -81,11 +81,12 @@ protected boolean canWriteValue(TypedValue value) private final Map serializersByName = new HashMap<>(); @Override - public void setProcessPlugins(List> plugins) + public void setProcessPlugins(List plugins) { @SuppressWarnings({ "unchecked", "rawtypes" }) - List serializers = plugins.stream().map(ProcessPlugin::getTypedValueSerializers) - .flatMap(List::stream).map(TypedValueSerializerWrapper::new).collect(Collectors.toList()); + List serializers = plugins.stream() + .flatMap(ProcessPlugin::getTypedValueSerializers).map(TypedValueSerializerWrapper::new) + .collect(Collectors.toList()); serializersByName.putAll( serializers.stream().collect(Collectors.toMap(TypedValueSerializer::getName, Function.identity()))); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionBpmnParse.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionBpmnParse.java index 0f1785855..483f20c78 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionBpmnParse.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionBpmnParse.java @@ -17,8 +17,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; -import dev.dsf.bpe.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessPlugin; public class MultiVersionBpmnParse extends BpmnParse { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateActivityBehavior.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateActivityBehavior.java index fa4abdc7b..ef24275ed 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateActivityBehavior.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateActivityBehavior.java @@ -13,7 +13,7 @@ import org.camunda.bpm.engine.impl.pvm.delegate.ActivityExecution; import org.camunda.bpm.engine.impl.util.ClassDelegateUtil; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; public class MultiVersionClassDelegateActivityBehavior extends ClassDelegateActivityBehavior { @@ -36,15 +36,14 @@ protected ActivityBehavior getActivityBehaviorInstance(ActivityExecution executi Object delegateInstance = instantiateDelegate(processKeyAndVersion, className, fieldDeclarations); - if (delegateInstance instanceof ActivityBehavior b) - return new CustomActivityBehavior(b); - - else if (delegateInstance instanceof JavaDelegate d) - return new ServiceTaskJavaDelegateActivityBehavior(d); + return switch (delegateInstance) + { + case ActivityBehavior b -> new CustomActivityBehavior(b); + case JavaDelegate d -> new ServiceTaskJavaDelegateActivityBehavior(d); - else - throw LOG.missingDelegateParentClassException(delegateInstance.getClass().getName(), + default -> throw LOG.missingDelegateParentClassException(delegateInstance.getClass().getName(), JavaDelegate.class.getName(), ActivityBehavior.class.getName()); + }; } private Object instantiateDelegate(ProcessIdAndVersion processKeyAndVersion, String className, diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateExecutionListener.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateExecutionListener.java index 83d72f4ea..fb10cabcd 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateExecutionListener.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateExecutionListener.java @@ -13,7 +13,7 @@ import org.camunda.bpm.engine.impl.persistence.entity.ExecutionEntity; import org.camunda.bpm.engine.impl.util.ClassDelegateUtil; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; public class MultiVersionClassDelegateExecutionListener extends ClassDelegateExecutionListener { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateTaskListener.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateTaskListener.java index 47dc7ee2c..b3dc49530 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateTaskListener.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/MultiVersionClassDelegateTaskListener.java @@ -13,7 +13,7 @@ import org.camunda.bpm.engine.impl.task.listener.ClassDelegateTaskListener; import org.camunda.bpm.engine.impl.util.ClassDelegateUtil; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; public class MultiVersionClassDelegateTaskListener extends ClassDelegateTaskListener { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/ProcessPluginConsumer.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/ProcessPluginConsumer.java index f18755457..c101e3aff 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/ProcessPluginConsumer.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/camunda/ProcessPluginConsumer.java @@ -2,9 +2,9 @@ import java.util.List; -import dev.dsf.bpe.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessPlugin; public interface ProcessPluginConsumer { - void setProcessPlugins(List> plugins); + void setProcessPlugins(List plugins); } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/AbstractFhirWebserviceClientJerseyWithRetry.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/AbstractFhirWebserviceClientJerseyWithRetry.java new file mode 100644 index 000000000..58ce9c756 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/AbstractFhirWebserviceClientJerseyWithRetry.java @@ -0,0 +1,113 @@ +package dev.dsf.bpe.client; + +import java.net.UnknownHostException; +import java.util.function.Supplier; + +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.HttpHostConnectException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response.Status; + +public abstract class AbstractFhirWebserviceClientJerseyWithRetry +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractFhirWebserviceClientJerseyWithRetry.class); + + protected final FhirWebserviceClientJersey delegate; + protected final int nTimes; + protected final long delayMillis; + + protected AbstractFhirWebserviceClientJerseyWithRetry(FhirWebserviceClientJersey delegate, int nTimes, + long delayMillis) + { + this.delegate = delegate; + this.nTimes = nTimes; + this.delayMillis = delayMillis; + } + + protected final R retry(int nTimes, long delayMillis, Supplier supplier) + { + RuntimeException caughtException = null; + for (int tryNumber = 0; tryNumber <= nTimes || nTimes == RetryClient.RETRY_FOREVER; tryNumber++) + { + try + { + if (tryNumber == 0) + logger.debug("First try ..."); + else if (nTimes != RetryClient.RETRY_FOREVER) + logger.debug("Retry {} of {}", tryNumber, nTimes); + + return supplier.get(); + } + catch (ProcessingException | WebApplicationException e) + { + if (shouldRetry(e)) + { + if (tryNumber < nTimes || nTimes == RetryClient.RETRY_FOREVER) + { + logger.warn("Caught {} - {}; trying again in {} ms{}", e.getClass(), e.getMessage(), + delayMillis, + nTimes == RetryClient.RETRY_FOREVER ? " (retry " + (tryNumber + 1) + ")" : ""); + + try + { + Thread.sleep(delayMillis); + } + catch (InterruptedException e1) + { + } + } + else + { + logger.warn("Caught {} - {}; not trying again", e.getClass(), e.getMessage()); + } + + if (caughtException != null) + e.addSuppressed(caughtException); + caughtException = e; + } + else + throw e; + } + } + + throw caughtException; + } + + private boolean shouldRetry(RuntimeException e) + { + if (e instanceof WebApplicationException w) + { + return isRetryStatusCode(w); + } + else if (e instanceof ProcessingException) + { + Throwable cause = e; + if (isRetryCause(cause)) + return true; + + while (cause.getCause() != null) + { + cause = cause.getCause(); + if (isRetryCause(cause)) + return true; + } + } + + return false; + } + + private boolean isRetryStatusCode(WebApplicationException e) + { + return Status.Family.SERVER_ERROR.equals(e.getResponse().getStatusInfo().getFamily()); + } + + private boolean isRetryCause(Throwable cause) + { + return cause instanceof ConnectTimeoutException || cause instanceof HttpHostConnectException + || cause instanceof UnknownHostException; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/AbstractJerseyClient.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/AbstractJerseyClient.java new file mode 100644 index 000000000..f554e6337 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/AbstractJerseyClient.java @@ -0,0 +1,108 @@ +package dev.dsf.bpe.client; + +import java.security.KeyStore; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +import javax.net.ssl.SSLContext; + +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.logging.LoggingFeature.Verbosity; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.HttpHeaders; + +public class AbstractJerseyClient +{ + private static final java.util.logging.Logger requestDebugLogger; + static + { + requestDebugLogger = java.util.logging.Logger.getLogger(AbstractJerseyClient.class.getName()); + requestDebugLogger.setLevel(Level.INFO); + } + + private final Client client; + private final String baseUrl; + + public AbstractJerseyClient(String baseUrl, KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword, + ObjectMapper objectMapper, Collection componentsToRegister) + { + this(baseUrl, trustStore, keyStore, keyStorePassword, objectMapper, componentsToRegister, null, null, null, 0, + 0, false, null); + } + + public AbstractJerseyClient(String baseUrl, KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword, + ObjectMapper objectMapper, Collection componentsToRegister, String proxySchemeHostPort, + String proxyUserName, char[] proxyPassword, int connectTimeout, int readTimeout, boolean logRequests, + String userAgentValue) + { + SSLContext sslContext = null; + if (trustStore != null && keyStore == null && keyStorePassword == null) + sslContext = SslConfigurator.newInstance().trustStore(trustStore).createSSLContext(); + else if (trustStore != null && keyStore != null && keyStorePassword != null) + sslContext = SslConfigurator.newInstance().trustStore(trustStore).keyStore(keyStore) + .keyStorePassword(keyStorePassword).createSSLContext(); + + ClientBuilder builder = ClientBuilder.newBuilder(); + + if (sslContext != null) + builder = builder.sslContext(sslContext); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(new ApacheConnectorProvider()); + config.property(ClientProperties.PROXY_URI, proxySchemeHostPort); + config.property(ClientProperties.PROXY_USERNAME, proxyUserName); + config.property(ClientProperties.PROXY_PASSWORD, proxyPassword == null ? null : String.valueOf(proxyPassword)); + builder = builder.withConfig(config); + + if (userAgentValue != null && !userAgentValue.isBlank()) + builder = builder.register((ClientRequestFilter) requestContext -> requestContext.getHeaders() + .add(HttpHeaders.USER_AGENT, userAgentValue)); + + builder = builder.readTimeout(readTimeout, TimeUnit.MILLISECONDS).connectTimeout(connectTimeout, + TimeUnit.MILLISECONDS); + + if (objectMapper != null) + { + JacksonJaxbJsonProvider p = new JacksonJaxbJsonProvider(JacksonJsonProvider.BASIC_ANNOTATIONS); + p.setMapper(objectMapper); + builder.register(p); + } + + if (componentsToRegister != null) + componentsToRegister.forEach(builder::register); + + if (logRequests) + { + builder = builder.register(new LoggingFeature(requestDebugLogger, Level.INFO, Verbosity.PAYLOAD_ANY, + LoggingFeature.DEFAULT_MAX_ENTITY_SIZE)); + } + + client = builder.build(); + + this.baseUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"; + // making sure the root url works, this might be a workaround for a jersey client bug + } + + protected WebTarget getResource() + { + return client.target(baseUrl); + } + + public String getBaseUrl() + { + return baseUrl; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/BasicFhirWebserviceCientWithRetryImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/BasicFhirWebserviceCientWithRetryImpl.java new file mode 100644 index 000000000..f5962a959 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/BasicFhirWebserviceCientWithRetryImpl.java @@ -0,0 +1,34 @@ +package dev.dsf.bpe.client; + +import java.util.List; +import java.util.Map; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Resource; + +class BasicFhirWebserviceCientWithRetryImpl extends AbstractFhirWebserviceClientJerseyWithRetry + implements BasicFhirWebserviceClient +{ + BasicFhirWebserviceCientWithRetryImpl(FhirWebserviceClientJersey delegate, int nTimes, long delayMillis) + { + super(delegate, nTimes, delayMillis); + } + + @Override + public R update(R resource) + { + return retry(nTimes, delayMillis, () -> delegate.update(resource)); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return retry(nTimes, delayMillis, () -> delegate.postBundle(bundle)); + } + + @Override + public Bundle searchWithStrictHandling(Class resourceType, Map> parameters) + { + return retry(nTimes, delayMillis, () -> delegate.searchWithStrictHandling(resourceType, parameters)); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/BasicFhirWebserviceClient.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/BasicFhirWebserviceClient.java new file mode 100644 index 000000000..256ae63b1 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/BasicFhirWebserviceClient.java @@ -0,0 +1,12 @@ +package dev.dsf.bpe.client; + +import java.util.List; +import java.util.Map; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Resource; + +public interface BasicFhirWebserviceClient extends PreferReturnResource +{ + Bundle searchWithStrictHandling(Class resourceType, Map> parameters); +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirAdapter.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirAdapter.java new file mode 100644 index 000000000..235be3e61 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirAdapter.java @@ -0,0 +1,114 @@ +package dev.dsf.bpe.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Set; +import java.util.function.Supplier; + +import org.hl7.fhir.r4.model.BaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.api.Constants; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; + +@Provider +@Consumes({ Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML, + Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON }) +@Produces({ Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML, + Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON }) +public class FhirAdapter implements MessageBodyReader, MessageBodyWriter +{ + private static final Logger logger = LoggerFactory.getLogger(FhirAdapter.class); + + private final FhirContext fhirContext; + + public FhirAdapter(FhirContext fhirContext) + { + this.fhirContext = fhirContext; + } + + private IParser getParser(MediaType mediaType, Supplier parserFactor) + { + /* Parsers are not guaranteed to be thread safe */ + IParser p = parserFactor.get(); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + + if (mediaType != null) + { + if ("true".equals(mediaType.getParameters().getOrDefault("pretty", "false"))) + p.setPrettyPrint(true); + + switch (mediaType.getParameters().getOrDefault("summary", "false")) + { + case "true" -> p.setSummaryMode(true); + case "text" -> p.setEncodeElements(Set.of("*.text", "*.id", "*.meta", "*.(mandatory)")); + case "data" -> p.setSuppressNarratives(true); + } + } + + return p; + } + + private IParser getParser(MediaType mediaType) + { + return switch (mediaType.getType() + "/" + mediaType.getSubtype()) + { + case Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML -> + getParser(mediaType, fhirContext::newXmlParser); + case Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON -> + getParser(mediaType, fhirContext::newJsonParser); + default -> throw new IllegalStateException("MediaType " + mediaType.toString() + " not supported"); + }; + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + return type != null && BaseResource.class.isAssignableFrom(type); + } + + @Override + public void writeTo(BaseResource t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException + { + getParser(mediaType).encodeResourceToWriter(t, new OutputStreamWriter(entityStream)); + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + return type != null && BaseResource.class.isAssignableFrom(type); + } + + @Override + public BaseResource readFrom(Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException + { + BaseResource resource = getParser(mediaType).parseResource(type, new InputStreamReader(entityStream)); + + if (resource instanceof Bundle) + logger.trace( + "Read Bundle may have references with contained resources, resulting in errors during validation or serialization, see ReferenceCleaner"); + + return resource; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirWebserviceClient.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirWebserviceClient.java new file mode 100644 index 000000000..a912f883c --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirWebserviceClient.java @@ -0,0 +1,6 @@ +package dev.dsf.bpe.client; + +public interface FhirWebserviceClient extends BasicFhirWebserviceClient, RetryClient +{ + PreferReturnMinimalWithRetry withMinimalReturn(); +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirWebserviceClientJersey.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirWebserviceClientJersey.java new file mode 100644 index 000000000..8f9ebe55e --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirWebserviceClientJersey.java @@ -0,0 +1,312 @@ +package dev.dsf.bpe.client; + +import java.io.InputStream; +import java.security.KeyStore; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; +import org.hl7.fhir.r4.model.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.annotation.ResourceDef; +import ca.uhn.fhir.rest.api.Constants; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation.Builder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.EntityTag; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; + +public class FhirWebserviceClientJersey extends AbstractJerseyClient implements FhirWebserviceClient +{ + private static final Logger logger = LoggerFactory.getLogger(FhirWebserviceClientJersey.class); + + private final PreferReturnMinimalWithRetry preferReturnMinimal; + + public FhirWebserviceClientJersey(String baseUrl, KeyStore trustStore, KeyStore keyStore, char[] keyStorePassword, + ObjectMapper objectMapper, String proxySchemeHostPort, String proxyUserName, char[] proxyPassword, + int connectTimeout, int readTimeout, boolean logRequests, String userAgentValue, FhirContext fhirContext) + { + super(baseUrl, trustStore, keyStore, keyStorePassword, objectMapper, + Collections.singleton(new FhirAdapter(fhirContext)), proxySchemeHostPort, proxyUserName, proxyPassword, + connectTimeout, readTimeout, logRequests, userAgentValue); + + preferReturnMinimal = new PreferReturnMinimalWithRetryImpl(this); + } + + private WebApplicationException handleError(Response response) + { + try + { + OperationOutcome outcome = response.readEntity(OperationOutcome.class); + String message = toString(outcome); + + logger.warn("Request failed, OperationOutcome: {}", message); + return new WebApplicationException(message, response.getStatus()); + } + catch (ProcessingException e) + { + response.close(); + + logger.warn("Request failed: {} - {}", e.getClass().getName(), e.getMessage()); + return new WebApplicationException(e, response.getStatus()); + } + } + + private String toString(OperationOutcome outcome) + { + return outcome == null ? "" : outcome.getIssue().stream().map(this::toString).collect(Collectors.joining("\n")); + } + + private String toString(OperationOutcomeIssueComponent issue) + { + return issue == null ? "" : issue.getSeverity() + " " + issue.getCode() + " " + issue.getDiagnostics(); + } + + private void logStatusAndHeaders(Response response) + { + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + logger.debug("HTTP header Location: {}", response.getLocation()); + logger.debug("HTTP header ETag: {}", response.getHeaderString(HttpHeaders.ETAG)); + logger.debug("HTTP header Last-Modified: {}", response.getHeaderString(HttpHeaders.LAST_MODIFIED)); + } + + private PreferReturn toPreferReturn(PreferReturnType returnType, Class resourceType, + Response response) + { + return switch (returnType) + { + case REPRESENTATION -> PreferReturn.resource(response.readEntity(resourceType)); + case MINIMAL -> PreferReturn.minimal(response.getLocation()); + case OPERATION_OUTCOME -> PreferReturn.outcome(response.readEntity(OperationOutcome.class)); + default -> + throw new RuntimeException(PreferReturn.class.getName() + " value " + returnType + " not supported"); + }; + } + + @Override + public PreferReturnMinimalWithRetry withMinimalReturn() + { + return preferReturnMinimal; + } + + PreferReturn create(PreferReturnType returnType, Resource resource) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + + Response response = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()).request() + .header(Constants.HEADER_PREFER, returnType.getHeaderValue()).accept(Constants.CT_FHIR_JSON_NEW) + .post(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn createConditionaly(PreferReturnType returnType, Resource resource, String ifNoneExistCriteria) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(ifNoneExistCriteria, "ifNoneExistCriteria"); + + Response response = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()).request() + .header(Constants.HEADER_PREFER, returnType.getHeaderValue()) + .header(Constants.HEADER_IF_NONE_EXIST, ifNoneExistCriteria).accept(Constants.CT_FHIR_JSON_NEW) + .post(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn createBinary(PreferReturnType returnType, InputStream in, MediaType mediaType, + String securityContextReference) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(in, "in"); + Objects.requireNonNull(mediaType, "mediaType"); + // securityContextReference may be null + + Builder request = getResource().path("Binary").request().header(Constants.HEADER_PREFER, + returnType.getHeaderValue()); + if (securityContextReference != null && !securityContextReference.isBlank()) + request = request.header(Constants.HEADER_X_SECURITY_CONTEXT, securityContextReference); + Response response = request.accept(Constants.CT_FHIR_JSON_NEW).post(Entity.entity(in, mediaType)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, Binary.class, response); + else + throw handleError(response); + } + + PreferReturn update(PreferReturnType returnType, Resource resource) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + + Builder builder = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()) + .path(resource.getIdElement().getIdPart()).request() + .header(Constants.HEADER_PREFER, returnType.getHeaderValue()).accept(Constants.CT_FHIR_JSON_NEW); + + if (resource.getMeta().hasVersionId()) + builder.header(Constants.HEADER_IF_MATCH, new EntityTag(resource.getMeta().getVersionId(), true)); + + Response response = builder.put(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn updateConditionaly(PreferReturnType returnType, Resource resource, Map> criteria) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(criteria, "criteria"); + if (criteria.isEmpty()) + throw new IllegalArgumentException("criteria map empty"); + + WebTarget target = getResource().path(resource.getClass().getAnnotation(ResourceDef.class).name()); + + for (Entry> entry : criteria.entrySet()) + target = target.queryParam(entry.getKey(), entry.getValue().toArray()); + + Builder builder = target.request().accept(Constants.CT_FHIR_JSON_NEW).header(Constants.HEADER_PREFER, + returnType.getHeaderValue()); + + if (resource.getMeta().hasVersionId()) + builder.header(Constants.HEADER_IF_MATCH, new EntityTag(resource.getMeta().getVersionId(), true)); + + Response response = builder.put(Entity.entity(resource, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus() || Status.OK.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, resource.getClass(), response); + else + throw handleError(response); + } + + PreferReturn updateBinary(PreferReturnType returnType, String id, InputStream in, MediaType mediaType, + String securityContextReference) + { + Objects.requireNonNull(returnType, "returnType"); + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(in, "in"); + Objects.requireNonNull(mediaType, "mediaType"); + // securityContextReference may be null + + Builder request = getResource().path("Binary").path(id).request().header(Constants.HEADER_PREFER, + returnType.getHeaderValue()); + if (securityContextReference != null && !securityContextReference.isBlank()) + request = request.header(Constants.HEADER_X_SECURITY_CONTEXT, securityContextReference); + Response response = request.accept(Constants.CT_FHIR_JSON_NEW).put(Entity.entity(in, mediaType)); + + logStatusAndHeaders(response); + + if (Status.CREATED.getStatusCode() == response.getStatus()) + return toPreferReturn(returnType, Binary.class, response); + else + throw handleError(response); + } + + Bundle postBundle(PreferReturnType returnType, Bundle bundle) + { + Objects.requireNonNull(bundle, "bundle"); + + Response response = getResource().request().header(Constants.HEADER_PREFER, returnType.getHeaderValue()) + .accept(Constants.CT_FHIR_JSON_NEW).post(Entity.entity(bundle, Constants.CT_FHIR_JSON_NEW)); + + logStatusAndHeaders(response); + + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(Bundle.class); + else + throw handleError(response); + } + + @Override + @SuppressWarnings("unchecked") + public R update(R resource) + { + return (R) update(PreferReturnType.REPRESENTATION, resource).getResource(); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return postBundle(PreferReturnType.REPRESENTATION, bundle); + } + + @Override + public Bundle searchWithStrictHandling(Class resourceType, Map> parameters) + { + Objects.requireNonNull(resourceType, "resourceType"); + + WebTarget target = getResource().path(resourceType.getAnnotation(ResourceDef.class).name()); + if (parameters != null) + { + for (Entry> entry : parameters.entrySet()) + target = target.queryParam(entry.getKey(), entry.getValue().toArray()); + } + + Response response = target.request().header(Constants.HEADER_PREFER, PreferHandlingType.STRICT.getHeaderValue()) + .accept(Constants.CT_FHIR_JSON_NEW).get(); + + logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), + response.getStatusInfo().getReasonPhrase()); + if (Status.OK.getStatusCode() == response.getStatus()) + return response.readEntity(Bundle.class); + else + throw handleError(response); + } + + @Override + public BasicFhirWebserviceClient withRetry(int nTimes, long delayMillis) + { + if (nTimes < 0) + throw new IllegalArgumentException("nTimes < 0"); + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new BasicFhirWebserviceCientWithRetryImpl(this, nTimes, delayMillis); + } + + @Override + public BasicFhirWebserviceClient withRetryForever(long delayMillis) + { + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new BasicFhirWebserviceCientWithRetryImpl(this, RETRY_FOREVER, delayMillis); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirClientProvider.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/LocalFhirClientProvider.java similarity index 61% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirClientProvider.java rename to dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/LocalFhirClientProvider.java index 31a47ef12..a9a00882a 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/FhirClientProvider.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/LocalFhirClientProvider.java @@ -1,14 +1,11 @@ package dev.dsf.bpe.client; -import dev.dsf.fhir.client.FhirWebserviceClient; import dev.dsf.fhir.client.WebsocketClient; -public interface FhirClientProvider +public interface LocalFhirClientProvider { FhirWebserviceClient getLocalWebserviceClient(); - FhirWebserviceClient getWebserviceClient(String webserviceUrl); - WebsocketClient getLocalWebsocketClient(Runnable reconnector, String subscriptionId); void disconnectAll(); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/LocalFhirClientProviderImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/LocalFhirClientProviderImpl.java new file mode 100644 index 000000000..98e272124 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/LocalFhirClientProviderImpl.java @@ -0,0 +1,137 @@ +package dev.dsf.bpe.client; + +import java.net.URI; +import java.security.KeyStore; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +import ca.uhn.fhir.context.FhirContext; +import dev.dsf.common.config.ProxyConfig; +import dev.dsf.fhir.client.WebsocketClient; +import dev.dsf.fhir.client.WebsocketClientTyrus; +import dev.dsf.tools.build.BuildInfoReader; + +public class LocalFhirClientProviderImpl implements LocalFhirClientProvider, InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(LocalFhirClientProviderImpl.class); + private static final String USER_AGENT_VALUE = "DSF/"; + + private final Map websocketClientsBySubscriptionId = new HashMap<>(); + + private final String localWebserviceBaseUrl; + + private final String localWebsocketUrl; + private final KeyStore localWebsocketTrustStore; + private final KeyStore localWebsocketKeyStore; + private final char[] localWebsocketKeyStorePassword; + + private final ProxyConfig proxyConfig; + private final BuildInfoReader buildInfoReader; + + private final FhirWebserviceClient localWebserviceClient; + + public LocalFhirClientProviderImpl(FhirContext fhirContext, String localWebserviceBaseUrl, + int localWebserviceReadTimeout, int localWebserviceConnectTimeout, boolean localWebserviceLogRequests, + KeyStore webserviceTrustStore, KeyStore webserviceKeyStore, char[] webserviceKeyStorePassword, + String localWebsocketUrl, KeyStore localWebsocketTrustStore, KeyStore localWebsocketKeyStore, + char[] localWebsocketKeyStorePassword, ProxyConfig proxyConfig, BuildInfoReader buildInfoReader) + { + Objects.requireNonNull(fhirContext, "fhirContext"); + Objects.requireNonNull(localWebserviceBaseUrl, "localWebserviceBaseUrl"); + + if (localWebserviceReadTimeout < 0) + throw new IllegalArgumentException("localReadTimeout < 0"); + if (localWebserviceConnectTimeout < 0) + throw new IllegalArgumentException("localConnectTimeout < 0"); + Objects.requireNonNull(webserviceTrustStore, "webserviceTrustStore"); + Objects.requireNonNull(webserviceKeyStore, "webserviceKeyStore"); + Objects.requireNonNull(webserviceKeyStorePassword, "webserviceKeyStorePassword"); + Objects.requireNonNull(proxyConfig, "proxyConfig"); + Objects.requireNonNull(buildInfoReader, "buildInfoReader"); + + this.localWebserviceBaseUrl = localWebserviceBaseUrl; + + this.localWebsocketUrl = localWebsocketUrl; + this.localWebsocketTrustStore = localWebsocketTrustStore; + this.localWebsocketKeyStore = localWebsocketKeyStore; + this.localWebsocketKeyStorePassword = localWebsocketKeyStorePassword; + + this.proxyConfig = proxyConfig; + this.buildInfoReader = buildInfoReader; + + String proxyUrl = proxyConfig.isEnabled(localWebserviceBaseUrl) ? proxyConfig.getUrl() : null; + String proxyUsername = proxyConfig.isEnabled(localWebserviceBaseUrl) ? proxyConfig.getUsername() : null; + char[] proxyPassword = proxyConfig.isEnabled(localWebserviceBaseUrl) ? proxyConfig.getPassword() : null; + + localWebserviceClient = new FhirWebserviceClientJersey(localWebserviceBaseUrl, webserviceTrustStore, + webserviceKeyStore, webserviceKeyStorePassword, null, proxyUrl, proxyUsername, proxyPassword, + localWebserviceConnectTimeout, localWebserviceReadTimeout, localWebserviceLogRequests, + USER_AGENT_VALUE + buildInfoReader.getProjectVersion(), fhirContext); + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(localWebsocketUrl, "localWebsocketUrl"); + Objects.requireNonNull(localWebsocketTrustStore, "localWebsocketTrustStore"); + Objects.requireNonNull(localWebsocketKeyStore, "localWebsocketKeyStore"); + Objects.requireNonNull(localWebsocketKeyStorePassword, "localWebsocketKeyStorePassword"); + } + + public String getLocalBaseUrl() + { + return localWebserviceBaseUrl; + } + + @Override + public FhirWebserviceClient getLocalWebserviceClient() + { + return localWebserviceClient; + } + + @Override + public WebsocketClient getLocalWebsocketClient(Runnable reconnector, String subscriptionId) + { + if (!websocketClientsBySubscriptionId.containsKey(subscriptionId)) + { + WebsocketClientTyrus client = createWebsocketClient(reconnector, subscriptionId); + websocketClientsBySubscriptionId.put(subscriptionId, client); + return client; + } + + return websocketClientsBySubscriptionId.get(subscriptionId); + } + + protected WebsocketClientTyrus createWebsocketClient(Runnable reconnector, String subscriptionId) + { + return new WebsocketClientTyrus(reconnector, URI.create(localWebsocketUrl), localWebsocketTrustStore, + localWebsocketKeyStore, localWebsocketKeyStorePassword, + proxyConfig.isEnabled(localWebsocketUrl) ? proxyConfig.getUrl() : null, + proxyConfig.isEnabled(localWebsocketUrl) ? proxyConfig.getUsername() : null, + proxyConfig.isEnabled(localWebsocketUrl) ? proxyConfig.getPassword() : null, + USER_AGENT_VALUE + buildInfoReader.getProjectVersion(), subscriptionId); + } + + @Override + public void disconnectAll() + { + for (WebsocketClient c : websocketClientsBySubscriptionId.values()) + { + try + { + c.disconnect(); + } + catch (Exception e) + { + logger.debug("Error while disconnecting websocket client", e); + logger.warn("Error while disconnecting websocket client: {} - {}", e.getClass().getName(), + e.getMessage()); + } + } + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferHandlingType.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferHandlingType.java new file mode 100644 index 000000000..fb08b80c1 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferHandlingType.java @@ -0,0 +1,31 @@ +package dev.dsf.bpe.client; + +public enum PreferHandlingType +{ + STRICT("handling=strict"), LENIENT("handling=lenient"); + + private final String headerValue; + + PreferHandlingType(String headerValue) + { + this.headerValue = headerValue; + } + + public static PreferHandlingType fromString(String prefer) + { + if (prefer == null) + return LENIENT; + + return switch (prefer) + { + case "handling=strict" -> STRICT; + case "handling=lenient" -> LENIENT; + default -> LENIENT; + }; + } + + public String getHeaderValue() + { + return headerValue; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturn.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturn.java new file mode 100644 index 000000000..ed229019c --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturn.java @@ -0,0 +1,51 @@ +package dev.dsf.bpe.client; + +import java.net.URI; + +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; + +public class PreferReturn +{ + private final IdType id; + private final Resource resource; + private final OperationOutcome operationOutcome; + + private PreferReturn(IdType id, Resource resource, OperationOutcome operationOutcome) + { + this.id = id; + this.resource = resource; + this.operationOutcome = operationOutcome; + } + + public static PreferReturn minimal(URI location) + { + return new PreferReturn(new IdType(location.toString()), null, null); + } + + public static PreferReturn resource(Resource resource) + { + return new PreferReturn(null, resource, null); + } + + public static PreferReturn outcome(OperationOutcome operationOutcome) + { + return new PreferReturn(null, null, operationOutcome); + } + + public IdType getId() + { + return id; + } + + public Resource getResource() + { + return resource; + } + + public OperationOutcome getOperationOutcome() + { + return operationOutcome; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimal.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimal.java new file mode 100644 index 000000000..c897bb5f5 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimal.java @@ -0,0 +1,8 @@ +package dev.dsf.bpe.client; + +import org.hl7.fhir.r4.model.Bundle; + +public interface PreferReturnMinimal +{ + Bundle postBundle(Bundle bundle); +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimalRetryImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimalRetryImpl.java new file mode 100644 index 000000000..aca13529f --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimalRetryImpl.java @@ -0,0 +1,17 @@ +package dev.dsf.bpe.client; + +import org.hl7.fhir.r4.model.Bundle; + +class PreferReturnMinimalRetryImpl extends AbstractFhirWebserviceClientJerseyWithRetry implements PreferReturnMinimal +{ + PreferReturnMinimalRetryImpl(FhirWebserviceClientJersey delegate, int nTimes, long delayMillis) + { + super(delegate, nTimes, delayMillis); + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return retry(nTimes, delayMillis, () -> delegate.postBundle(PreferReturnType.MINIMAL, bundle)); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimalWithRetry.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimalWithRetry.java new file mode 100644 index 000000000..5d39f6456 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimalWithRetry.java @@ -0,0 +1,5 @@ +package dev.dsf.bpe.client; + +public interface PreferReturnMinimalWithRetry extends PreferReturnMinimal, RetryClient +{ +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimalWithRetryImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimalWithRetryImpl.java new file mode 100644 index 000000000..f4f94bb4f --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnMinimalWithRetryImpl.java @@ -0,0 +1,39 @@ +package dev.dsf.bpe.client; + +import org.hl7.fhir.r4.model.Bundle; + +class PreferReturnMinimalWithRetryImpl implements PreferReturnMinimalWithRetry +{ + private final FhirWebserviceClientJersey delegate; + + PreferReturnMinimalWithRetryImpl(FhirWebserviceClientJersey delegate) + { + this.delegate = delegate; + } + + @Override + public Bundle postBundle(Bundle bundle) + { + return delegate.postBundle(PreferReturnType.MINIMAL, bundle); + } + + @Override + public PreferReturnMinimal withRetry(int nTimes, long delayMillis) + { + if (nTimes < 0) + throw new IllegalArgumentException("nTimes < 0"); + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new PreferReturnMinimalRetryImpl(delegate, nTimes, delayMillis); + } + + @Override + public PreferReturnMinimal withRetryForever(long delayMillis) + { + if (delayMillis < 0) + throw new IllegalArgumentException("delayMillis < 0"); + + return new PreferReturnMinimalRetryImpl(delegate, RETRY_FOREVER, delayMillis); + } +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnResource.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnResource.java new file mode 100644 index 000000000..0f18aa83b --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnResource.java @@ -0,0 +1,11 @@ +package dev.dsf.bpe.client; + +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Resource; + +public interface PreferReturnResource +{ + R update(R resource); + + Bundle postBundle(Bundle bundle); +} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnType.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnType.java new file mode 100644 index 000000000..cd29c27ef --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/PreferReturnType.java @@ -0,0 +1,32 @@ +package dev.dsf.bpe.client; + +public enum PreferReturnType +{ + MINIMAL("return=minimal"), REPRESENTATION("return=representation"), OPERATION_OUTCOME("return=OperationOutcome"); + + private final String headerValue; + + PreferReturnType(String headerValue) + { + this.headerValue = headerValue; + } + + public static PreferReturnType fromString(String prefer) + { + if (prefer == null) + return REPRESENTATION; + + return switch (prefer) + { + case "return=minimal" -> MINIMAL; + case "return=OperationOutcome" -> OPERATION_OUTCOME; + case "return=representation" -> REPRESENTATION; + default -> REPRESENTATION; + }; + } + + public String getHeaderValue() + { + return headerValue; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/RetryClient.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/RetryClient.java new file mode 100644 index 000000000..6b5a28864 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/client/RetryClient.java @@ -0,0 +1,68 @@ +package dev.dsf.bpe.client; + +public interface RetryClient +{ + int RETRY_ONCE = 1; + int RETRY_FOREVER = -1; + long FIVE_SECONDS = 5_000L; + + /** + * retries once after a delay of {@value RetryClient#FIVE_SECONDS} ms + * + * @return T + */ + default T withRetry() + { + return withRetry(RETRY_ONCE, FIVE_SECONDS); + } + + /** + * retries nTimes and waits {@value RetryClient#FIVE_SECONDS} ms between tries + * + * @param nTimes + * {@code >= 0} + * @return T + * + * @throws IllegalArgumentException + * if param nTimes is {@code <0} + */ + default T withRetry(int nTimes) + { + return withRetry(nTimes, FIVE_SECONDS); + } + + /** + * retries once after a delay of delayMillis ms + * + * @param delayMillis + * {@code >= 0} + * @return T + * @throws IllegalArgumentException + * if param delayMillis is {@code <0} + */ + default T withRetry(long delayMillis) + { + return withRetry(RETRY_ONCE, delayMillis); + } + + /** + * @param nTimes + * {@code >= 0} + * @param delayMillis + * {@code >= 0} + * @return T + * + * @throws IllegalArgumentException + * if param nTimes or delayMillis is {@code <0} + */ + T withRetry(int nTimes, long delayMillis); + + /** + * @param delayMillis + * {@code >= 0} + * @return T + * @throws IllegalArgumentException + * if param delayMillis is {@code <0} + */ + T withRetryForever(long delayMillis); +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessPluginResourcesDao.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessPluginResourcesDao.java index 1de858ea9..196f009cd 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessPluginResourcesDao.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessPluginResourcesDao.java @@ -6,7 +6,7 @@ import java.util.Map; import java.util.UUID; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; import dev.dsf.bpe.plugin.ProcessesResource; import dev.dsf.bpe.plugin.ResourceInfo; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessPluginResourcesDaoJdbc.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessPluginResourcesDaoJdbc.java index 1cfc42525..64d81fb81 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessPluginResourcesDaoJdbc.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessPluginResourcesDaoJdbc.java @@ -18,7 +18,7 @@ import org.postgresql.util.PGobject; import ca.uhn.fhir.parser.DataFormatException; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; import dev.dsf.bpe.plugin.ProcessesResource; import dev.dsf.bpe.plugin.ResourceInfo; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessStateDao.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessStateDao.java index 0ade0a792..e6fcf7875 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessStateDao.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessStateDao.java @@ -3,7 +3,7 @@ import java.sql.SQLException; import java.util.Map; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; import dev.dsf.bpe.plugin.ProcessState; public interface ProcessStateDao diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessStateDaoJdbc.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessStateDaoJdbc.java index e0bfb7048..d8fffbe16 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessStateDaoJdbc.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/dao/ProcessStateDaoJdbc.java @@ -11,7 +11,7 @@ import javax.sql.DataSource; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; import dev.dsf.bpe.plugin.ProcessState; public class ProcessStateDaoJdbc extends AbstractDaoJdbc implements ProcessStateDao diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/listener/DefaultBpmnParseListener.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/listener/DefaultBpmnParseListener.java index 9bc8963c1..457c02681 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/listener/DefaultBpmnParseListener.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/listener/DefaultBpmnParseListener.java @@ -1,82 +1,108 @@ package dev.dsf.bpe.listener; -import java.util.Objects; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.camunda.bpm.engine.delegate.ExecutionListener; import org.camunda.bpm.engine.impl.bpmn.parser.AbstractBpmnParseListener; import org.camunda.bpm.engine.impl.bpmn.parser.BpmnParse; import org.camunda.bpm.engine.impl.pvm.process.ActivityImpl; +import org.camunda.bpm.engine.impl.pvm.process.ProcessDefinitionImpl; import org.camunda.bpm.engine.impl.pvm.process.ScopeImpl; import org.camunda.bpm.engine.impl.util.xml.Element; +import org.camunda.bpm.engine.repository.ProcessDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.InitializingBean; -public class DefaultBpmnParseListener extends AbstractBpmnParseListener implements InitializingBean +import dev.dsf.bpe.api.listener.ListenerFactory; + +public class DefaultBpmnParseListener extends AbstractBpmnParseListener { private static final Logger logger = LoggerFactory.getLogger(DefaultBpmnParseListener.class); - private final StartListener startListener; - private final EndListener endListener; - private final ContinueListener continueListener; + private final Map listenerFactoriesByApiVersion = new HashMap<>(); - public DefaultBpmnParseListener(StartListener startListener, EndListener endListener, - ContinueListener continueListener) + public DefaultBpmnParseListener(Stream listenerFactories) { - this.startListener = startListener; - this.endListener = endListener; - this.continueListener = continueListener; + if (listenerFactories != null) + this.listenerFactoriesByApiVersion.putAll(listenerFactories + .collect(Collectors.toMap(f -> String.valueOf(f.getApiVersion()), Function.identity()))); } - @Override - public void afterPropertiesSet() throws Exception + private Optional getListenerFactory(ActivityImpl element) { - Objects.requireNonNull(startListener, "startListener"); - Objects.requireNonNull(endListener, "endListener"); - Objects.requireNonNull(continueListener, "continueListener"); + ProcessDefinitionImpl processDefinition = element.getProcessDefinition(); + + if (processDefinition instanceof ProcessDefinition withTenant) + { + String apiVersion = withTenant.getTenantId(); + + if (apiVersion == null) + return Optional.empty(); + else + return Optional.ofNullable(listenerFactoriesByApiVersion.get(apiVersion)); + } + else + return Optional.empty(); } @Override public void parseStartEvent(Element startEventElement, ScopeImpl scope, ActivityImpl startEventActivity) { - Element messageEventDefinition = startEventElement.element(BpmnParse.MESSAGE_EVENT_DEFINITION); - if (messageEventDefinition != null) - startEventActivity.addListener(ExecutionListener.EVENTNAME_START, startListener); - else - logger.debug("Not adding Listener to StartEvent {}", startEventActivity.getId()); + getListenerFactory(startEventActivity).ifPresent(listenerFactory -> + { + Element messageEventDefinition = startEventElement.element(BpmnParse.MESSAGE_EVENT_DEFINITION); + if (messageEventDefinition != null) + startEventActivity.addListener(ExecutionListener.EVENTNAME_START, listenerFactory.getStartListener()); + else + logger.debug("Not adding Listener to StartEvent {}", startEventActivity.getId()); + }); } @Override public void parseEndEvent(Element endEventElement, ScopeImpl scope, ActivityImpl endEventActivity) { - /* - * Adding at index 0 to the end phase of the EndEvent, so processes can execute listeners after the Task - * resource has been updated. Listeners added to the end phase of the EndEvent via BPMN are execute after this - * listener - */ - endEventActivity.addListener(ExecutionListener.EVENTNAME_END, endListener, 0); + getListenerFactory(endEventActivity).ifPresent(listenerFactory -> + { + /* + * Adding at index 0 to the end phase of the EndEvent, so processes can execute listeners after the Task + * resource has been updated. Listeners added to the end phase of the EndEvent via BPMN are execute after + * this listener + */ + endEventActivity.addListener(ExecutionListener.EVENTNAME_END, listenerFactory.getEndListener(), 0); + }); } @Override public void parseIntermediateMessageCatchEventDefinition(Element messageEventDefinition, ActivityImpl nestedActivity) { - /* - * Adding at index 0 to the end phase of the IntermediateMessageCatchEvent, so processes can execute listeners - * after variables has been updated. Listeners added to the end phase of the IntermediateMessageCatchEvent via - * BPMN are execute after this listener - */ - nestedActivity.addListener(ExecutionListener.EVENTNAME_END, continueListener, 0); + getListenerFactory(nestedActivity).ifPresent(listenerFactory -> + { + /* + * Adding at index 0 to the end phase of the IntermediateMessageCatchEvent, so processes can execute + * listeners after variables has been updated. Listeners added to the end phase of the + * IntermediateMessageCatchEvent via BPMN are execute after this listener + */ + nestedActivity.addListener(ExecutionListener.EVENTNAME_END, listenerFactory.getContinueListener(), 0); + }); } @Override public void parseReceiveTask(Element receiveTaskElement, ScopeImpl scope, ActivityImpl activity) { - /* - * Adding at index 0 to the end phase of the IntermediateMessageCatchEvent, so processes can execute listeners - * after variables has been updated. Listeners added to the end phase of the IntermediateMessageCatchEvent via - * BPMN are execute after this listener - */ - activity.addListener(ExecutionListener.EVENTNAME_END, continueListener, 0); + getListenerFactory(activity).ifPresent(listenerFactory -> + { + /* + * Adding at index 0 to the end phase of the IntermediateMessageCatchEvent, so processes can execute + * listeners after variables has been updated. Listeners added to the end phase of the + * IntermediateMessageCatchEvent via BPMN are execute after this listener + */ + activity.addListener(ExecutionListener.EVENTNAME_END, listenerFactory.getContinueListener(), 0); + }); } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/mail/BpeMailService.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/mail/BpeMailService.java new file mode 100644 index 000000000..c53c3044e --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/mail/BpeMailService.java @@ -0,0 +1,155 @@ +package dev.dsf.bpe.mail; + +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 BpeMailService +{ + /** + * 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-server/src/main/java/dev/dsf/bpe/mail/LoggingMailService.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/mail/LoggingMailService.java index b39d8c4fc..72502a3ea 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/mail/LoggingMailService.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/mail/LoggingMailService.java @@ -14,9 +14,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import dev.dsf.bpe.v1.service.MailService; - -public class LoggingMailService implements MailService +public class LoggingMailService implements BpeMailService { private static final Logger logger = LoggerFactory.getLogger(LoggingMailService.class); private static final Logger mailLogger = LoggerFactory.getLogger("mail-logger"); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/mail/SmtpMailService.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/mail/SmtpMailService.java index 505774660..75e11dbf2 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/mail/SmtpMailService.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/mail/SmtpMailService.java @@ -68,9 +68,8 @@ import org.springframework.beans.factory.InitializingBean; import de.rwh.utils.crypto.context.SSLContextFactory; -import dev.dsf.bpe.v1.service.MailService; -public class SmtpMailService implements MailService, InitializingBean +public class SmtpMailService implements BpeMailService, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(SmtpMailService.class); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnProcessStateChangeService.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnProcessStateChangeService.java index 0790ccf05..b2e399113 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnProcessStateChangeService.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnProcessStateChangeService.java @@ -2,6 +2,8 @@ import java.util.List; +import dev.dsf.bpe.api.plugin.BpmnFileAndModel; + public interface BpmnProcessStateChangeService { /** diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnProcessStateChangeServiceImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnProcessStateChangeServiceImpl.java index 848bc562c..fb317c36d 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnProcessStateChangeServiceImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/BpmnProcessStateChangeServiceImpl.java @@ -18,6 +18,8 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; +import dev.dsf.bpe.api.plugin.BpmnFileAndModel; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; import dev.dsf.bpe.dao.ProcessStateDao; public class BpmnProcessStateChangeServiceImpl implements BpmnProcessStateChangeService, InitializingBean @@ -199,7 +201,7 @@ private void deploy(BpmnFileAndModel fileAndModel) DeploymentBuilder builder = repositoryService.createDeployment().name(processKeyAndVersion.toString()) .source(fileAndModel.getFile()).addModelInstance(fileAndModel.getFile(), fileAndModel.getModel()) - .enableDuplicateFiltering(true); + .enableDuplicateFiltering(true).tenantId(String.valueOf(fileAndModel.getProcessPluginApiVersion())); Deployment deployment = builder.deploy(); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/FhirResourceHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/FhirResourceHandler.java index fb0bc3c7e..b3a64e786 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/FhirResourceHandler.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/FhirResourceHandler.java @@ -3,10 +3,10 @@ import java.util.List; import java.util.Map; -import org.hl7.fhir.r4.model.Resource; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; public interface FhirResourceHandler { - void applyStateChangesAndStoreNewResourcesInDb(Map> resources, + void applyStateChangesAndStoreNewResourcesInDb(Map> resources, List changes); } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/FhirResourceHandlerImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/FhirResourceHandlerImpl.java index ac257afdd..9dcff0ab2 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/FhirResourceHandlerImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/FhirResourceHandlerImpl.java @@ -18,17 +18,18 @@ import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Bundle.BundleType; import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.ResourceType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.client.BasicFhirWebserviceClient; +import dev.dsf.bpe.client.FhirWebserviceClient; +import dev.dsf.bpe.client.PreferReturnMinimal; import dev.dsf.bpe.dao.ProcessPluginResourcesDao; -import dev.dsf.fhir.client.BasicFhirWebserviceClient; -import dev.dsf.fhir.client.FhirWebserviceClient; -import dev.dsf.fhir.client.PreferReturnMinimal; public class FhirResourceHandlerImpl implements FhirResourceHandler, InitializingBean { @@ -80,7 +81,7 @@ private BasicFhirWebserviceClient retryClient() } @Override - public void applyStateChangesAndStoreNewResourcesInDb(Map> pluginResources, + public void applyStateChangesAndStoreNewResourcesInDb(Map> pluginResources, List changes) { Objects.requireNonNull(pluginResources, "pluginResources"); @@ -152,7 +153,7 @@ public void applyStateChangesAndStoreNewResourcesInDb(Map addIdsAndReturnDeleted(List resourceValues } private Stream getCurrentOrOldResources( - Map> pluginResourcesByProcess, + Map> pluginResourcesByProcess, Map> dbResourcesByProcess, ProcessIdAndVersion process) { - List pluginResources = pluginResourcesByProcess.get(process); + List pluginResources = pluginResourcesByProcess.get(process); if (pluginResources != null) { - Stream resources = getResources(process, pluginResourcesByProcess); - return resources.map(fhirResource -> + Stream resources = getResources(process, pluginResourcesByProcess); + return resources.map(r -> { - ProcessesResource resource = ProcessesResource.from(fhirResource).add(process); + ProcessesResource resource = ProcessesResource.from(fhirContext, r).add(process); Optional resourceId = getResourceId(dbResourcesByProcess, process, resource.getResourceInfo()); resourceId.ifPresent(id -> resource.getResourceInfo().setResourceId(id)); @@ -364,10 +373,10 @@ private Stream getCurrentOrOldResources( } } - private Stream getResources(ProcessIdAndVersion process, - Map> pluginResources) + private Stream getResources(ProcessIdAndVersion process, + Map> pluginResources) { - List resources = pluginResources.get(process); + List resources = pluginResources.get(process); if (resources.isEmpty()) { logger.warn("No FHIR resources found for process {}", process.toString()); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginApiClassLoader.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginApiClassLoader.java new file mode 100644 index 000000000..bc41776ae --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginApiClassLoader.java @@ -0,0 +1,186 @@ +package dev.dsf.bpe.plugin; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ProcessPluginApiClassLoader extends URLClassLoader +{ + static + { + ClassLoader.registerAsParallelCapable(); + } + + private static final Logger logger = LoggerFactory.getLogger(ProcessPluginApiClassLoader.class); + + private final Set allowedBpeClasses = new HashSet<>(); + private final Set apiResourcesWithPriority = new HashSet<>(); + private final Set allowedBpeResources = new HashSet<>(); + + public ProcessPluginApiClassLoader(String name, URL[] urls, ClassLoader bpeLoader, Set allowedBpeClasses, + Set apiResourcesWithPriority, Set allowedBpeResources) + { + super(name, urls, bpeLoader); + + if (allowedBpeClasses != null) + this.allowedBpeClasses.addAll(allowedBpeClasses); + + if (apiResourcesWithPriority != null) + this.apiResourcesWithPriority.addAll(apiResourcesWithPriority); + + if (allowedBpeResources != null) + this.allowedBpeResources.addAll(allowedBpeResources); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException + { + return loadClass(name, false); + } + + @Override + protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException + { + synchronized (getClassLoadingLock(className)) + { + // check already loaded + Class apiClass = findLoadedClass(className); + if (apiClass != null) + return apiClass; + + // check api class path + apiClass = loadClassAsResource(className); + if (apiClass != null) + return apiClass; + + // check bpe + Class bpeClass = getParent().loadClass(className); + if (isBpeClassAllowed(bpeClass)) + return bpeClass; + + logger.warn("Class " + className + " not found or hidden"); + throw new ClassNotFoundException(className); + } + } + + private Class loadClassAsResource(final String name) throws ClassNotFoundException + { + URL apiClassUrl = findResource(toClassReference(name)); + if (apiClassUrl != null) + { + Class apiClass = findClass(name); + resolveClass(apiClass); + + return apiClass; + } + else + return null; + } + + private String toClassReference(String className) + { + return className == null ? null : className.replace('.', '/').concat(".class"); + } + + @Override + public URL getResource(String name) + { + URL resource = null; + + URL apiResourceUrl = findResource(name); + if (apiResourceUrl != null && hasApiResourcePriority(name, apiResourceUrl)) + resource = apiResourceUrl; + else + { + URL bpeResourceUrl = getParent().getResource(name); + if (bpeResourceUrl != null && isBpeResourceAllowed(name, bpeResourceUrl)) + resource = bpeResourceUrl; + else if (apiResourceUrl != null) + resource = apiResourceUrl; + } + + if (resource == null && name.startsWith("/")) + resource = getResource(name.substring(1)); + + return resource; + } + + @Override + public Enumeration getResources(String name) throws IOException + { + List fromBpe = new ArrayList<>(), fromApi = new ArrayList<>(); + + Enumeration urls = getParent().getResources(name); + while (urls != null && urls.hasMoreElements()) + { + URL bpeResourceUrl = urls.nextElement(); + if (isBpeResourceAllowed(name, bpeResourceUrl)) + fromBpe.add(bpeResourceUrl); + } + + urls = findResources(name); + while (urls != null && urls.hasMoreElements()) + { + URL apiResourceUrl = urls.nextElement(); + if (hasApiResourcePriority(name, apiResourceUrl) || fromBpe.isEmpty()) + fromApi.add(apiResourceUrl); + } + + fromApi.addAll(fromBpe); + + return Collections.enumeration(fromApi); + } + + /** + * @param clazz + * @return false if bpe class should be hidden from api or process plugin + */ + private boolean isBpeClassAllowed(Class clazz) + { + final String className = clazz.getName(); + + if (className.startsWith("java.") || className.startsWith("javax.mail.") || className.startsWith("javax.xml.") + || allowedBpeClasses.contains(className)) + return true; + + logger.debug("{} TODO: Should bpe class {} be allowed?", getName(), className); + return false; + } + + /** + * @param name + * @param apiResourceUrl + * @return true if resource from from api or process plugins has priority over resource from bpe + */ + private boolean hasApiResourcePriority(String name, URL apiResourceUrl) + { + if ("jar".equals(apiResourceUrl.getProtocol()) && apiResourcesWithPriority.contains(name)) + return true; + + logger.debug("{} TODO: Should api resource {} / {} have priority?", getName(), name, apiResourceUrl); + return false; + } + + /** + * @param name + * @param bpeResourcetUrl + * @return false if resource from bpe should be hidden from api or process plugins + */ + private boolean isBpeResourceAllowed(String name, URL bpeResourcetUrl) + { + if ("jar".equals(bpeResourcetUrl.getProtocol()) && allowedBpeResources.contains(name)) + return true; + + logger.debug("{} TODO: Should bpe resource {} / {} be allowed?", getName(), name, bpeResourcetUrl); + return false; + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginApiClassLoaderFactory.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginApiClassLoaderFactory.java new file mode 100644 index 000000000..0dabb92e8 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginApiClassLoaderFactory.java @@ -0,0 +1,135 @@ +package dev.dsf.bpe.plugin; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ProcessPluginApiClassLoaderFactory +{ + private static final Logger logger = LoggerFactory.getLogger(ProcessPluginApiClassLoaderFactory.class); + + private static final String ALLOWED_BPE_CLASSES_LIST = "allowed-bpe-classes.list"; + private static final String API_RESOURCES_WITH_PRIORITY_LIST = "api-resources-with-priority.list"; + private static final String ALLOWED_BPE_RESOURCES = "allowed-bpe-resources.list"; + + private URL[] getApiClassPath(String apiVersion) + { + Path apiClassPathFolder = Paths.get("api/v" + apiVersion); + + try + { + return Files.list(apiClassPathFolder).filter(p -> p.getFileName().toString().endsWith(".jar")) + .map(this::toUrl).toArray(URL[]::new); + } + catch (IOException e) + { + logger.warn("Unable to iterate files in api class path folder {}", apiClassPathFolder); + throw new RuntimeException( + "Unable to iterate files in api class path folder " + apiClassPathFolder.toString(), e); + } + } + + private URL toUrl(Path p) + { + try + { + return p.toUri().toURL(); + } + catch (MalformedURLException e) + { + throw new RuntimeException(e); + } + } + + private Set readList(String apiVersion, String file) + { + Path externalFile = getExternalFileIfReadable(apiVersion, file); + return externalFile == null ? readInternal(apiVersion, file) : readExternal(externalFile); + } + + private Path getExternalFileIfReadable(String apiVersion, String file) + { + Path externalFile = Paths.get("api/v" + apiVersion + "/" + file); + + if (!Files.exists(externalFile)) + { + logger.debug("External file {} does not exist, using file from jar", + externalFile.toAbsolutePath().toString()); + return null; + } + + if (!Files.isReadable(externalFile)) + { + logger.debug("External file {} is not readable, using file from jar", + externalFile.toAbsolutePath().toString()); + return null; + } + + return externalFile; + } + + private Set readExternal(Path file) + { + try + { + logger.debug("Reading {} ...", file.toAbsolutePath().toString()); + return new HashSet<>(Files.readAllLines(file)); + } + catch (IOException e) + { + logger.warn("Unable to read external file {}", file.toAbsolutePath().toString()); + throw new RuntimeException("Unable to read external file " + file.toAbsolutePath().toString(), e); + } + } + + private Set readInternal(String apiVersion, String file) + { + final String path = "api/v" + apiVersion + "/" + file; + + try (InputStream in = ProcessPluginApiClassLoaderFactory.class.getClassLoader().getResourceAsStream(path); + InputStreamReader inReader = new InputStreamReader(in, StandardCharsets.UTF_8); + BufferedReader reader = new BufferedReader(inReader)) + { + List result = new ArrayList<>(); + for (;;) + { + String line = reader.readLine(); + if (line == null) + break; + result.add(line); + } + return new HashSet<>(result); + } + catch (IOException e) + { + logger.warn("Unable to read internal file {}", path); + throw new RuntimeException("Unable to read internal file " + path, e); + } + } + + public ProcessPluginApiClassLoader createApiClassLoader(String apiVersion) + { + URL[] apiClassPath = getApiClassPath(apiVersion); + + Set allowedBpeClasses = readList(apiVersion, ALLOWED_BPE_CLASSES_LIST); + Set apiResourcesWithPriority = readList(apiVersion, API_RESOURCES_WITH_PRIORITY_LIST); + Set allowedBpeResources = readList(apiVersion, ALLOWED_BPE_RESOURCES); + + return new ProcessPluginApiClassLoader("Plugin API v" + apiVersion, apiClassPath, + ClassLoader.getSystemClassLoader(), allowedBpeClasses, apiResourcesWithPriority, allowedBpeResources); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginApiFactory.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginApiFactory.java new file mode 100644 index 000000000..39b66db99 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginApiFactory.java @@ -0,0 +1,105 @@ +package dev.dsf.bpe.plugin; + +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.ServiceLoader.Provider; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import dev.dsf.bpe.api.config.ClientConfig; +import dev.dsf.bpe.api.config.ProxyConfig; +import dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder; +import dev.dsf.bpe.api.plugin.ProcessPluginFactory; +import dev.dsf.bpe.api.service.BpeMailService; +import dev.dsf.bpe.api.service.BuildInfoProvider; + +public class ProcessPluginApiFactory implements InitializingBean +{ + private static final Logger logger = LoggerFactory.getLogger(ProcessPluginApiFactory.class); + + private final ConfigurableEnvironment environment; + private final ClientConfig clientConfig; + private final ProxyConfig proxyConfig; + private final BuildInfoProvider buildInfoProvider; + private final BpeMailService bpeMailService; + private final ProcessPluginApiClassLoaderFactory classLoaderFactory; + + public ProcessPluginApiFactory(ConfigurableEnvironment environment, ClientConfig clientConfig, + ProxyConfig proxyConfig, BuildInfoProvider buildInfoProvider, BpeMailService bpeMailService, + ProcessPluginApiClassLoaderFactory classLoaderFactory) + { + this.environment = environment; + this.clientConfig = clientConfig; + this.proxyConfig = proxyConfig; + this.buildInfoProvider = buildInfoProvider; + this.bpeMailService = bpeMailService; + this.classLoaderFactory = classLoaderFactory; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(environment, "environment"); + Objects.requireNonNull(clientConfig, "clientConfig"); + Objects.requireNonNull(proxyConfig, "proxyConfig"); + Objects.requireNonNull(buildInfoProvider, "buildInfoProvider"); + Objects.requireNonNull(bpeMailService, "bpeMailService"); + Objects.requireNonNull(classLoaderFactory, "classLoaderFactory"); + } + + public List initialize() + { + return Stream.of("1", "2").map(this::init).toList(); + } + + private ProcessPluginFactory init(String apiVersion) + { + ClassLoader apiClassLoader = classLoaderFactory.createApiClassLoader(apiVersion); + ProcessPluginApiBuilder apiBuilder = loadProcessPluginApiBuilder(apiClassLoader); + ApplicationContext apiApplicationContext = createApiApplicationContext(apiVersion, apiClassLoader, + apiBuilder.getSpringServiceConfigClass()); + + return apiBuilder.build(apiClassLoader, apiApplicationContext, environment); + } + + private ProcessPluginApiBuilder loadProcessPluginApiBuilder(ClassLoader apiClassLoader) + { + return ServiceLoader.load(ProcessPluginApiBuilder.class, apiClassLoader).stream().map(Provider::get).findFirst() + .get(); + } + + private ApplicationContext createApiApplicationContext(String apiVersion, ClassLoader apiClassLoader, + Class springServiceConfigClass) + { + try + { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerSingleton("clientConfig", clientConfig); + factory.registerSingleton("proxyConfig", proxyConfig); + factory.registerSingleton("buildInfoReader", buildInfoProvider); + factory.registerSingleton("bpeMailService", bpeMailService); + + var context = new AnnotationConfigApplicationContext(factory); + context.setClassLoader(apiClassLoader); + context.setEnvironment(environment); + context.register(springServiceConfigClass); + context.refresh(); + + return context; + } + catch (BeansException | IllegalStateException e) + { + logger.error("Unable to create api v{} application context", apiVersion, e); + throw e; + } + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginFactory.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginFactory.java deleted file mode 100644 index 63ca24c3a..000000000 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginFactory.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.dsf.bpe.plugin; - -import java.nio.file.Path; - -import org.springframework.core.env.ConfigurableEnvironment; - -import ca.uhn.fhir.context.FhirContext; - -public interface ProcessPluginFactory -{ - int getApiVersion(); - - Class getProcessPluginDefinitionType(); - - ProcessPlugin createProcessPlugin(D processPluginDefinition, boolean draft, Path jarFile, - ClassLoader classLoader, FhirContext fhirContext, ConfigurableEnvironment environment); -} \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginLoader.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginLoader.java index 1535b0078..5dcbef6d6 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginLoader.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginLoader.java @@ -2,7 +2,9 @@ import java.util.List; +import dev.dsf.bpe.api.plugin.ProcessPlugin; + public interface ProcessPluginLoader { - List> loadPlugins(); + List loadPlugins(); } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginLoaderImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginLoaderImpl.java index d7928897f..532b921b2 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginLoaderImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginLoaderImpl.java @@ -1,9 +1,6 @@ package dev.dsf.bpe.plugin; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; @@ -12,42 +9,32 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.ServiceLoader; -import java.util.ServiceLoader.Provider; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.env.ConfigurableEnvironment; -import ca.uhn.fhir.context.FhirContext; +import dev.dsf.bpe.api.plugin.ProcessPlugin; +import dev.dsf.bpe.api.plugin.ProcessPluginFactory; public class ProcessPluginLoaderImpl implements ProcessPluginLoader, InitializingBean { - public static final String SNAPSHOT_FILE_SUFFIX = "-SNAPSHOT.jar"; - public static final String MILESTONE_FILE_PATTERN = ".*-M[0-9]+.jar"; - public static final String RELEASE_CANDIDATE_FILE_PATTERN = ".*-RC[0-9]+.jar"; - private static final Logger logger = LoggerFactory.getLogger(ProcessPluginLoaderImpl.class); private final Path pluginDirectory; - private final List> processPluginFactories = new ArrayList<>(); - private final FhirContext fhirContext; - private final ConfigurableEnvironment environment; + private final List processPluginFactories = new ArrayList<>(); - public ProcessPluginLoaderImpl(Collection> processPluginFactories, - Path pluginDirectory, FhirContext fhirContext, ConfigurableEnvironment environment) + public ProcessPluginLoaderImpl(Collection processPluginFactories, + Path pluginDirectory) { this.pluginDirectory = pluginDirectory; - this.fhirContext = fhirContext; - this.environment = environment; if (processPluginFactories != null) { this.processPluginFactories.addAll(processPluginFactories); this.processPluginFactories.sort( - Comparator.> comparingInt(ProcessPluginFactory::getApiVersion).reversed()); + Comparator. comparingInt(ProcessPluginFactory::getApiVersion).reversed()); } } @@ -55,16 +42,14 @@ public ProcessPluginLoaderImpl(Collection> pro public void afterPropertiesSet() throws Exception { Objects.requireNonNull(pluginDirectory, "pluginDirectory"); - Objects.requireNonNull(fhirContext, "fhirContext"); - Objects.requireNonNull(environment, "environment"); } @Override - public List> loadPlugins() + public List loadPlugins() { try (DirectoryStream directoryStream = Files.newDirectoryStream(pluginDirectory)) { - List> plugins = new ArrayList<>(); + List plugins = new ArrayList<>(); directoryStream.forEach(p -> { @@ -74,7 +59,7 @@ else if (!p.getFileName().toString().endsWith(".jar")) logger.warn("Ignoring {}: {}", p.toAbsolutePath().toString(), "Not a .jar file"); else { - ProcessPlugin plugin = load(p); + ProcessPlugin plugin = load(p); if (plugin != null) plugins.add(plugin); } @@ -91,11 +76,11 @@ else if (!p.getFileName().toString().endsWith(".jar")) } } - private ProcessPlugin load(Path jar) + private ProcessPlugin load(Path jar) { - for (ProcessPluginFactory factory : processPluginFactories) + for (ProcessPluginFactory factory : processPluginFactories) { - var plugin = load(jar, factory); + ProcessPlugin plugin = factory.load(jar); if (plugin != null) return plugin; @@ -108,49 +93,4 @@ else if (!p.getFileName().toString().endsWith(".jar")) .collect(Collectors.joining(", ", "[", "]"))); return null; } - - private ProcessPlugin load(Path jar, ProcessPluginFactory factory) - { - try - { - URLClassLoader classLoader = new URLClassLoader(jar.getFileName().toString(), new URL[] { toUrl(jar) }, - ClassLoader.getSystemClassLoader()); - - List> definitions = ServiceLoader.load(factory.getProcessPluginDefinitionType(), classLoader) - .stream().collect(Collectors.toList()); - - if (definitions.size() != 1) - return null; - - String filename = jar.getFileName().toString(); - boolean isSnapshot = filename.endsWith(SNAPSHOT_FILE_SUFFIX); - boolean isMilestone = filename.matches(MILESTONE_FILE_PATTERN); - boolean isReleaseCandidate = filename.matches(RELEASE_CANDIDATE_FILE_PATTERN); - - boolean draft = isSnapshot || isMilestone || isReleaseCandidate; - - return factory.createProcessPlugin(definitions.get(0).get(), draft, jar, classLoader, fhirContext, - environment); - } - catch (Exception e) - { - logger.debug("Ignoring {}: Unable to load process plugin", jar.toString(), e); - logger.warn("Ignoring {}: Unable to load process plugin: {} - {}", jar.toString(), e.getClass().getName(), - e.getMessage()); - - return null; - } - } - - private URL toUrl(Path p) - { - try - { - return p.toUri().toURL(); - } - catch (MalformedURLException e) - { - throw new RuntimeException(e); - } - } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginManager.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginManager.java index dec746cf9..24e0532ed 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginManager.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginManager.java @@ -1,6 +1,13 @@ package dev.dsf.bpe.plugin; +import java.util.Optional; + +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessPlugin; + public interface ProcessPluginManager { void loadAndDeployPlugins(); + + Optional getProcessPlugin(ProcessIdAndVersion processIdAndVersion); } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginManagerImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginManagerImpl.java index bd9996cef..b3002d14e 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginManagerImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessPluginManagerImpl.java @@ -11,7 +11,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -21,21 +20,27 @@ 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.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; +import dev.dsf.bpe.api.plugin.BpmnFileAndModel; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessPlugin; import dev.dsf.bpe.camunda.ProcessPluginConsumer; -import dev.dsf.bpe.v1.ProcessPluginDeploymentStateListener; -import dev.dsf.bpe.v1.constants.NamingSystems.OrganizationIdentifier; -import dev.dsf.fhir.client.BasicFhirWebserviceClient; -import dev.dsf.fhir.client.FhirWebserviceClient; +import dev.dsf.bpe.client.BasicFhirWebserviceClient; +import dev.dsf.bpe.client.FhirWebserviceClient; public class ProcessPluginManagerImpl implements ProcessPluginManager, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(ProcessPluginManagerImpl.class); + public static final String ORGANIZATION_IDENTIFIER_SID = "http://dsf.dev/sid/organization-identifier"; + + private record ProcessIdAndVersionAndProcessPlugin(ProcessIdAndVersion idAndVersion, ProcessPlugin plugin) + { + } + private final List processPluginConsumers = new ArrayList<>(); private final ProcessPluginLoader processPluginLoader; @@ -47,6 +52,8 @@ public class ProcessPluginManagerImpl implements ProcessPluginManager, Initializ private final int fhirServerRequestMaxRetries; private final long fhirServerRetryDelayMillis; + private Map pluginsByProcessIdAndVersion; + public ProcessPluginManagerImpl(List processPluginConsumers, ProcessPluginLoader processPluginLoader, BpmnProcessStateChangeService bpmnProcessStateChangeService, FhirResourceHandler fhirResourceHandler, String localEndpointAddress, @@ -84,7 +91,7 @@ public void loadAndDeployPlugins() if (localOrganizationIdentifierValue.isEmpty()) logger.warn("Local organization identifier unknown, check DSF FHIR server allow list"); - List> plugins = removeDuplicates(processPluginLoader.loadPlugins().stream() + List plugins = removeDuplicates(processPluginLoader.loadPlugins().stream() .filter(p -> p.initializeAndValidateResources(localOrganizationIdentifierValue.orElse(null)))); if (plugins.isEmpty()) @@ -98,11 +105,16 @@ public void loadAndDeployPlugins() .deploySuspendOrActivateProcesses(models); // deploy FHIR resources - Map> resources = plugins.stream().map(ProcessPlugin::getFhirResources) + Map> resources = plugins.stream().map(ProcessPlugin::getFhirResources) .flatMap(m -> m.entrySet().stream()).collect(Collectors.toMap(Entry::getKey, Entry::getValue)); fhirResourceHandler.applyStateChangesAndStoreNewResourcesInDb(resources, outcomes); onProcessesDeployed(outcomes, plugins); + + this.pluginsByProcessIdAndVersion = plugins.stream().flatMap( + p -> p.getProcessKeysAndVersions().stream().map(iAV -> new ProcessIdAndVersionAndProcessPlugin(iAV, p))) + .collect(Collectors.toMap(ProcessIdAndVersionAndProcessPlugin::idAndVersion, + ProcessIdAndVersionAndProcessPlugin::plugin)); } private BasicFhirWebserviceClient retryClient() @@ -136,8 +148,12 @@ else if (getActiveOrganizationFromIncludes(resultBundle).count() != 1) return Optional.empty(); } - return getActiveOrganizationFromIncludes(resultBundle).findFirst().flatMap(OrganizationIdentifier::findFirst) - .map(Identifier::getValue); + return getActiveOrganizationFromIncludes(resultBundle).findFirst() + .flatMap(o -> o.getIdentifier().stream() + .filter(i -> i.hasSystemElement() && i.getSystemElement().hasValue() + && ORGANIZATION_IDENTIFIER_SID.equals(i.getSystem())) + .findFirst()) + .filter(i -> i.hasValueElement() && i.getValueElement().hasValue()).map(Identifier::getValue); } private Stream getActiveOrganizationFromIncludes(Bundle resultBundle) @@ -148,9 +164,9 @@ private Stream getActiveOrganizationFromIncludes(Bundle resultBund .filter(r -> r instanceof Organization).map(r -> (Organization) r).filter(Organization::getActive); } - private List> removeDuplicates(Stream> plugins) + private List removeDuplicates(Stream plugins) { - Map>> pluginsByProcessIdAndVersion = new HashMap<>(); + Map> pluginsByProcessIdAndVersion = new HashMap<>(); plugins.forEach(plugin -> { List processes = plugin.getProcessKeysAndVersions(); @@ -160,7 +176,7 @@ private Stream getActiveOrganizationFromIncludes(Bundle resultBund pluginsByProcessIdAndVersion.get(process).add(plugin); else { - List> list = new ArrayList<>(); + List list = new ArrayList<>(); list.add(plugin); pluginsByProcessIdAndVersion.put(process, list); } @@ -179,40 +195,21 @@ private Stream getActiveOrganizationFromIncludes(Bundle resultBund .flatMap(e -> e.getValue().stream()).distinct().toList(); } - private void onProcessesDeployed(List changes, List> plugins) + private void onProcessesDeployed(List changes, List plugins) { Set activeProcesses = changes.stream() .filter(c -> EnumSet.of(ProcessState.ACTIVE, ProcessState.DRAFT).contains(c.getNewProcessState())) .map(ProcessStateChangeOutcome::getProcessKeyAndVersion).collect(Collectors.toSet()); - plugins.forEach(plugin -> - { - List activePluginProcesses = plugin.getProcessKeysAndVersions().stream() - .filter(activeProcesses::contains).map(ProcessIdAndVersion::getId).toList(); - - plugin.getApplicationContext().getBeansOfType(ProcessPluginDeploymentStateListener.class).entrySet() - .forEach(onProcessesDeployed(plugin, activePluginProcesses)); - }); + plugins.stream() + .forEach(plugin -> plugin.getProcessPluginDeploymentListener().onProcessesDeployed(activeProcesses)); } - private Consumer> onProcessesDeployed( - ProcessPlugin plugin, List activePluginProcesses) + public Optional getProcessPlugin(ProcessIdAndVersion processIdAndVersion) { - return entry -> - { - try - { - entry.getValue().onProcessesDeployed(activePluginProcesses); - } - catch (Exception e) - { - logger.debug("Error while executing {} bean {} for process plugin {}", - ProcessPluginDeploymentStateListener.class.getName(), entry.getKey(), - plugin.getJarFile().toString(), e); - logger.warn("Error while executing {} bean {} for process plugin {}: {} - {}", - ProcessPluginDeploymentStateListener.class.getName(), entry.getKey(), - plugin.getJarFile().toString(), e.getClass().getName(), e.getMessage()); - } - }; + if (pluginsByProcessIdAndVersion == null) + return Optional.empty(); + else + return Optional.ofNullable(pluginsByProcessIdAndVersion.get(processIdAndVersion)); } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessStateChangeOutcome.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessStateChangeOutcome.java index a2779d61c..815567660 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessStateChangeOutcome.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessStateChangeOutcome.java @@ -2,6 +2,8 @@ import java.util.Objects; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; + public class ProcessStateChangeOutcome { private final ProcessIdAndVersion processKeyAndVersion; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessesResource.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessesResource.java index 015c7bd89..2978abb18 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessesResource.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ProcessesResource.java @@ -1,5 +1,8 @@ package dev.dsf.bpe.plugin; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -25,35 +28,46 @@ import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.ValueSet; -import dev.dsf.bpe.v1.constants.NamingSystems.TaskIdentifier; +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.DataFormatException; +import dev.dsf.bpe.api.Constants; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; public final class ProcessesResource { + public static ProcessesResource from(FhirContext fhirContext, byte[] encodedResource) + { + try (InputStream in = new ByteArrayInputStream(encodedResource)) + { + Resource resource = (Resource) fhirContext.newJsonParser().parseResource(in); + return from(resource); + } + catch (ConfigurationException | DataFormatException | IOException e) + { + throw new RuntimeException(e); + } + } + public static ProcessesResource from(Resource resource) { Objects.requireNonNull(resource, "resource"); - if (resource instanceof ActivityDefinition a) - return fromMetadataResource(a); - else if (resource instanceof CodeSystem c) - return fromMetadataResource(c); - else if (resource instanceof Library l) - return fromMetadataResource(l); - else if (resource instanceof Measure m) - return fromMetadataResource(m); - else if (resource instanceof NamingSystem n) - return fromNamingSystem(n); - else if (resource instanceof Questionnaire q) - return fromMetadataResource(q); - else if (resource instanceof StructureDefinition s) - return fromMetadataResource(s); - else if (resource instanceof Task t) - return fromTask(t); - else if (resource instanceof ValueSet v) - return fromMetadataResource(v); - else - throw new IllegalArgumentException( + return switch (resource) + { + case ActivityDefinition a -> fromMetadataResource(a); + case CodeSystem c -> fromMetadataResource(c); + case Library l -> fromMetadataResource(l); + case Measure m -> fromMetadataResource(m); + case NamingSystem n -> fromNamingSystem(n); + case Questionnaire q -> fromMetadataResource(q); + case StructureDefinition s -> fromMetadataResource(s); + case Task t -> fromTask(t); + case ValueSet v -> fromMetadataResource(v); + + default -> throw new IllegalArgumentException( "MetadataResource of type " + resource.getClass().getName() + " not supported"); + }; } public static ProcessesResource fromMetadataResource(MetadataResource resource) @@ -77,7 +91,11 @@ public static ProcessesResource fromTask(Task resource) private static String getIdentifier(Task resource) { - return TaskIdentifier.findFirst(resource).map(Identifier::getValue).get(); + return resource.getIdentifier().stream() + .filter(i -> i.hasSystemElement() && i.getSystemElement().hasValue() + && Constants.TASK_IDENTIFIER_SID.equals(i.getSystemElement().getValue())) + .findFirst().filter(i -> i.hasValueElement() && i.getValueElement().hasValue()) + .map(Identifier::getValue).get(); } public static ProcessesResource from(ResourceInfo resourceInfo) diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ResourceInfo.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ResourceInfo.java index 4c84f3b3b..4117df1e8 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ResourceInfo.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/ResourceInfo.java @@ -6,7 +6,7 @@ import org.hl7.fhir.r4.model.ResourceType; -import dev.dsf.bpe.v1.constants.NamingSystems.TaskIdentifier; +import dev.dsf.bpe.api.Constants; public class ResourceInfo implements Comparable { @@ -136,7 +136,7 @@ public String toConditionalUrl() if (ResourceType.NamingSystem.equals(getResourceType())) return "name=" + getName(); if (ResourceType.Task.equals(getResourceType())) - return "identifier=" + TaskIdentifier.SID + "|" + getIdentifier() + "&status=draft"; + return "identifier=" + Constants.TASK_IDENTIFIER_SID + "|" + getIdentifier() + "&status=draft"; else return "url=" + getUrl() + "&version=" + getVersion(); } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/service/LocalOrganizationProviderImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/service/LocalOrganizationProviderImpl.java index a2a537221..f617b408d 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/service/LocalOrganizationProviderImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/service/LocalOrganizationProviderImpl.java @@ -2,17 +2,28 @@ import java.time.LocalDateTime; import java.time.temporal.TemporalAmount; +import java.util.Collections; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.SearchEntryMode; +import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.Organization; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; -import dev.dsf.bpe.v1.service.OrganizationProvider; +import dev.dsf.bpe.client.LocalFhirClientProvider; public class LocalOrganizationProviderImpl implements LocalOrganizationProvider, InitializingBean { + private static final Logger logger = LoggerFactory.getLogger(LocalOrganizationProviderImpl.class); + private record OrganizationEntry(Optional organization, LocalDateTime readTime) { } @@ -20,19 +31,23 @@ private record OrganizationEntry(Optional organization, LocalDateT private final AtomicReference organization = new AtomicReference<>(); private final TemporalAmount cacheTimeout; - private final OrganizationProvider delegate; + private final LocalFhirClientProvider clientProvider; + private final String localEndpointAddress; - public LocalOrganizationProviderImpl(TemporalAmount cacheTimeout, OrganizationProvider delegate) + public LocalOrganizationProviderImpl(TemporalAmount cacheTimeout, LocalFhirClientProvider clientProvider, + String localEndpointAddress) { this.cacheTimeout = cacheTimeout; - this.delegate = delegate; + this.clientProvider = clientProvider; + this.localEndpointAddress = localEndpointAddress; } @Override public void afterPropertiesSet() throws Exception { Objects.requireNonNull(cacheTimeout, "cacheTimeout"); - Objects.requireNonNull(delegate, "delegate"); + Objects.requireNonNull(clientProvider, "clientProvider"); + Objects.requireNonNull(localEndpointAddress, "localEndpointAddress"); } @Override @@ -42,7 +57,7 @@ public Optional getLocalOrganization() if (entry == null || entry.organization().isEmpty() || LocalDateTime.now().isAfter(entry.readTime().plus(cacheTimeout))) { - Optional o = delegate.getLocalOrganization(); + Optional o = doGetLocalOrganization(); if (organization.compareAndSet(entry, new OrganizationEntry(o, LocalDateTime.now()))) return o; else @@ -51,4 +66,38 @@ public Optional getLocalOrganization() else return entry.organization(); } + + private Optional doGetLocalOrganization() + { + Bundle resultBundle = clientProvider.getLocalWebserviceClient().searchWithStrictHandling(Endpoint.class, + Map.of("status", Collections.singletonList("active"), "address", + Collections.singletonList(localEndpointAddress), "_include", + Collections.singletonList("Endpoint:organization"))); + + if (resultBundle == null || resultBundle.getEntry() == null || resultBundle.getEntry().size() != 2 + || resultBundle.getEntry().get(0).getResource() == null + || !(resultBundle.getEntry().get(0).getResource() instanceof Endpoint) + || resultBundle.getEntry().get(1).getResource() == null + || !(resultBundle.getEntry().get(1).getResource() instanceof Organization)) + { + logger.warn("No active (or more than one) Endpoint found for address '{}'", localEndpointAddress); + return Optional.empty(); + } + else if (getActiveOrganizationFromIncludes(resultBundle).count() != 1) + { + logger.warn("No active (or more than one) Organization found by active Endpoint with address '{}'", + localEndpointAddress); + return Optional.empty(); + } + + return getActiveOrganizationFromIncludes(resultBundle).findFirst(); + } + + private Stream getActiveOrganizationFromIncludes(Bundle resultBundle) + { + return resultBundle.getEntry().stream().filter(BundleEntryComponent::hasSearch) + .filter(e -> SearchEntryMode.INCLUDE.equals(e.getSearch().getMode())) + .filter(BundleEntryComponent::hasResource).map(BundleEntryComponent::getResource) + .filter(r -> r instanceof Organization).map(r -> (Organization) r).filter(Organization::getActive); + } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AbstractConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AbstractConfig.java new file mode 100644 index 000000000..830daa4b8 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AbstractConfig.java @@ -0,0 +1,56 @@ +package dev.dsf.bpe.spring.config; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.pkcs.PKCSException; + +import de.rwh.utils.crypto.CertificateHelper; +import de.rwh.utils.crypto.io.CertificateReader; +import de.rwh.utils.crypto.io.PemIo; + +public class AbstractConfig +{ + private static final BouncyCastleProvider provider = new BouncyCastleProvider(); + + protected final KeyStore createTrustStore(String trustStoreFile) + throws IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException + { + Path trustStorePath = Paths.get(trustStoreFile); + + if (!Files.isReadable(trustStorePath)) + throw new IOException("Trust store file '" + trustStorePath.toString() + "' not readable"); + + return CertificateReader.allFromCer(trustStorePath); + } + + protected final KeyStore createKeyStore(String certificateFile, String privateKeyFile, char[] privateKeyPassword, + char[] keyStorePassword) + throws IOException, PKCSException, CertificateException, KeyStoreException, NoSuchAlgorithmException + { + Path certificatePath = Paths.get(certificateFile); + Path privateKeyPath = Paths.get(privateKeyFile); + + if (!Files.isReadable(certificatePath)) + throw new IOException("Certificate file '" + certificatePath.toString() + "' not readable"); + if (!Files.isReadable(privateKeyPath)) + throw new IOException("Private key file '" + privateKeyPath.toString() + "' not readable"); + + X509Certificate certificate = PemIo.readX509CertificateFromPem(certificatePath); + PrivateKey privateKey = PemIo.readPrivateKeyFromPem(provider, privateKeyPath, privateKeyPassword); + + String subjectCommonName = CertificateHelper.getSubjectCommonName(certificate); + return CertificateHelper.toJksKeyStore(privateKey, new Certificate[] { certificate }, subjectCommonName, + keyStorePassword); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AuthenticationConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AuthenticationConfig.java index 34630a48d..a3e70e66a 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AuthenticationConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AuthenticationConfig.java @@ -22,16 +22,16 @@ public class AuthenticationConfig private static final Logger logger = LoggerFactory.getLogger(AuthenticationConfig.class); @Autowired - private PropertiesConfig propertiesConfig; + private FhirClientConfig fhirClientConfig; @Autowired - private PluginConfig pluginConfig; + private PropertiesConfig propertiesConfig; @Bean public LocalOrganizationProvider localOrganizationProvider() { - return new LocalOrganizationProviderImpl(Duration.ofSeconds(30), - pluginConfig.processPluginApiV1().getOrganizationProvider()); + return new LocalOrganizationProviderImpl(Duration.ofSeconds(30), fhirClientConfig.clientProvider(), + propertiesConfig.getFhirServerBaseUrl()); } @Bean diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java index e2c95a525..0eea08eb1 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java @@ -18,17 +18,14 @@ import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; import org.springframework.transaction.PlatformTransactionManager; +import dev.dsf.bpe.api.plugin.ProcessPluginFactory; import dev.dsf.bpe.camunda.DelegateProvider; import dev.dsf.bpe.camunda.DelegateProviderImpl; import dev.dsf.bpe.camunda.FallbackSerializerFactory; import dev.dsf.bpe.camunda.FallbackSerializerFactoryImpl; import dev.dsf.bpe.camunda.MultiVersionSpringProcessEngineConfiguration; -import dev.dsf.bpe.listener.ContinueListener; import dev.dsf.bpe.listener.DebugLoggingBpmnParseListener; import dev.dsf.bpe.listener.DefaultBpmnParseListener; -import dev.dsf.bpe.listener.EndListener; -import dev.dsf.bpe.listener.StartListener; -import dev.dsf.bpe.variables.VariablesImpl; @Configuration public class CamundaConfig @@ -36,14 +33,11 @@ public class CamundaConfig @Autowired private PropertiesConfig propertiesConfig; - @Autowired - private FhirClientConfig fhirClientConfig; - @Autowired private ApplicationContext applicationContext; @Autowired - private SerializerConfig serializerConfig; + private List processPluginFactories; @Bean public PlatformTransactionManager transactionManager() @@ -76,29 +70,11 @@ private String toString(char[] password) return password == null ? null : String.valueOf(password); } - @Bean - public StartListener startListener() - { - return new StartListener(propertiesConfig.getFhirServerBaseUrl(), VariablesImpl::new); - } - - @Bean - public EndListener endListener() - { - return new EndListener(propertiesConfig.getFhirServerBaseUrl(), VariablesImpl::new, - fhirClientConfig.clientProvider().getLocalWebserviceClient()); - } - - @Bean - public ContinueListener continueListener() - { - return new ContinueListener(propertiesConfig.getFhirServerBaseUrl(), VariablesImpl::new); - } - @Bean public DefaultBpmnParseListener defaultBpmnParseListener() { - return new DefaultBpmnParseListener(startListener(), endListener(), continueListener()); + return new DefaultBpmnParseListener( + processPluginFactories.stream().map(ProcessPluginFactory::getListenerFactory)); } @Bean @@ -120,8 +96,7 @@ public SpringProcessEngineConfiguration processEngineConfiguration() c.setJobExecutorActivate(false); c.setCustomPreBPMNParseListeners(List.of(defaultBpmnParseListener(), debugLoggingBpmnParseListener())); c.setCustomPreVariableSerializers( - List.of(serializerConfig.targetSerializer(), serializerConfig.targetsSerializer(), - serializerConfig.fhirResourceSerializer(), serializerConfig.fhirResourcesListSerializer())); + processPluginFactories.stream().flatMap(ProcessPluginFactory::getSerializer).toList()); c.setFallbackSerializerFactory(fallbackSerializerFactory()); // see also MultiVersionSpringProcessEngineConfiguration @@ -148,7 +123,7 @@ public FallbackSerializerFactory fallbackSerializerFactory() @Bean public DelegateProvider delegateProvider() { - return new DelegateProviderImpl(ClassLoader.getSystemClassLoader(), applicationContext); + return new DelegateProviderImpl(ClassLoader.getSystemClassLoader(), applicationContext, processPluginFactories); } @Bean diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirClientConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirClientConfig.java index 7ae3fbdd9..daa002c95 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirClientConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirClientConfig.java @@ -1,19 +1,12 @@ package dev.dsf.bpe.spring.config; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.cert.Certificate; import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; import java.util.UUID; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.pkcs.PKCSException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,21 +15,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import de.rwh.utils.crypto.CertificateHelper; -import de.rwh.utils.crypto.io.CertificateReader; -import de.rwh.utils.crypto.io.PemIo; -import dev.dsf.bpe.client.FhirClientProvider; -import dev.dsf.bpe.client.FhirClientProviderImpl; -import dev.dsf.fhir.service.ReferenceCleaner; -import dev.dsf.fhir.service.ReferenceCleanerImpl; -import dev.dsf.fhir.service.ReferenceExtractor; -import dev.dsf.fhir.service.ReferenceExtractorImpl; +import dev.dsf.bpe.client.LocalFhirClientProvider; +import dev.dsf.bpe.client.LocalFhirClientProviderImpl; @Configuration -public class FhirClientConfig implements InitializingBean +public class FhirClientConfig extends AbstractConfig implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(FhirClientConfig.class); - private static final BouncyCastleProvider provider = new BouncyCastleProvider(); @Autowired private PropertiesConfig propertiesConfig; @@ -79,19 +64,7 @@ public void afterPropertiesSet() throws Exception } @Bean - public ReferenceCleaner referenceCleaner() - { - return new ReferenceCleanerImpl(referenceExtractor()); - } - - @Bean - public ReferenceExtractor referenceExtractor() - { - return new ReferenceExtractorImpl(); - } - - @Bean - public FhirClientProvider clientProvider() + public LocalFhirClientProvider clientProvider() { char[] keyStorePassword = UUID.randomUUID().toString().toCharArray(); @@ -102,15 +75,12 @@ public FhirClientProvider clientProvider() propertiesConfig.getClientCertificatePrivateKeyFilePassword(), keyStorePassword); KeyStore webserviceTrustStore = createTrustStore(propertiesConfig.getClientCertificateTrustStoreFile()); - return new FhirClientProviderImpl(fhirConfig.fhirContext(), referenceCleaner(), - propertiesConfig.getFhirServerBaseUrl(), propertiesConfig.getWebserviceClientLocalReadTimeout(), + return new LocalFhirClientProviderImpl(fhirConfig.fhirContext(), propertiesConfig.getFhirServerBaseUrl(), + propertiesConfig.getWebserviceClientLocalReadTimeout(), propertiesConfig.getWebserviceClientLocalConnectTimeout(), propertiesConfig.getWebserviceClientLocalVerbose(), webserviceTrustStore, webserviceKeyStore, - keyStorePassword, propertiesConfig.getWebserviceClientRemoteReadTimeout(), - propertiesConfig.getWebserviceClientRemoteConnectTimeout(), - propertiesConfig.getWebserviceClientRemoteVerbose(), getWebsocketUrl(), webserviceTrustStore, - webserviceKeyStore, keyStorePassword, propertiesConfig.proxyConfig(), - buildInfoReaderConfig.buildInfoReader()); + keyStorePassword, getWebsocketUrl(), webserviceTrustStore, webserviceKeyStore, keyStorePassword, + propertiesConfig.proxyConfig(), buildInfoReaderConfig.buildInfoReader()); } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException | PKCSException e) { @@ -129,35 +99,4 @@ else if (baseUrl.startsWith("http://")) else throw new RuntimeException("server base url (" + baseUrl + ") does not start with https:// or http://"); } - - private KeyStore createTrustStore(String trustStoreFile) - throws IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException - { - Path trustStorePath = Paths.get(trustStoreFile); - - if (!Files.isReadable(trustStorePath)) - throw new IOException("Trust store file '" + trustStorePath.toString() + "' not readable"); - - return CertificateReader.allFromCer(trustStorePath); - } - - private KeyStore createKeyStore(String certificateFile, String privateKeyFile, char[] privateKeyPassword, - char[] keyStorePassword) - throws IOException, PKCSException, CertificateException, KeyStoreException, NoSuchAlgorithmException - { - Path certificatePath = Paths.get(certificateFile); - Path privateKeyPath = Paths.get(privateKeyFile); - - if (!Files.isReadable(certificatePath)) - throw new IOException("Certificate file '" + certificatePath.toString() + "' not readable"); - if (!Files.isReadable(privateKeyPath)) - throw new IOException("Private key file '" + privateKeyPath.toString() + "' not readable"); - - X509Certificate certificate = PemIo.readX509CertificateFromPem(certificatePath); - PrivateKey privateKey = PemIo.readPrivateKeyFromPem(provider, privateKeyPath, privateKeyPassword); - - String subjectCommonName = CertificateHelper.getSubjectCommonName(certificate); - return CertificateHelper.toJksKeyStore(privateKey, new Certificate[] { certificate }, subjectCommonName, - keyStorePassword); - } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirConfig.java index 6c4586a6e..f86fa60c5 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirConfig.java @@ -5,8 +5,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.fasterxml.jackson.core.StreamReadConstraints; - import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.HapiLocalizer; @@ -16,10 +14,6 @@ public class FhirConfig @Bean public FhirContext fhirContext() { - // TODO remove workaround after upgrading to HAPI 6.8+, see https://github.com/hapifhir/hapi-fhir/issues/5205 - StreamReadConstraints.overrideDefaultStreamReadConstraints( - StreamReadConstraints.builder().maxStringLength(Integer.MAX_VALUE).build()); - FhirContext context = FhirContext.forR4(); HapiLocalizer localizer = new HapiLocalizer() { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/MailConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/MailConfig.java index 8a3c32bb6..18e7b45f1 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/MailConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/MailConfig.java @@ -37,9 +37,9 @@ import de.rwh.utils.crypto.CertificateHelper; import de.rwh.utils.crypto.io.CertificateReader; import de.rwh.utils.crypto.io.PemIo; +import dev.dsf.bpe.mail.BpeMailService; import dev.dsf.bpe.mail.LoggingMailService; import dev.dsf.bpe.mail.SmtpMailService; -import dev.dsf.bpe.v1.service.MailService; import dev.dsf.tools.build.BuildInfoReader; @Configuration @@ -56,7 +56,7 @@ public class MailConfig implements InitializingBean BuildInfoReaderConfig buildInfoReaderConfig; @Bean - public MailService mailService() + public BpeMailService mailService() { if (isConfigured()) { @@ -78,7 +78,7 @@ private boolean isConfigured() return propertiesConfig.getMailServerHostname() != null && propertiesConfig.getMailServerPort() > 0; } - private MailService newSmptMailService() + private BpeMailService newSmptMailService() throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, PKCSException { String fromAddress = propertiesConfig.getMailFromAddress(); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginConfig.java index 30c3dc46f..d682565e7 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginConfig.java @@ -7,51 +7,21 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import com.fasterxml.jackson.databind.ObjectMapper; - -import ca.uhn.fhir.context.FhirContext; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessPluginFactory; import dev.dsf.bpe.plugin.BpmnProcessStateChangeService; import dev.dsf.bpe.plugin.BpmnProcessStateChangeServiceImpl; import dev.dsf.bpe.plugin.FhirResourceHandler; import dev.dsf.bpe.plugin.FhirResourceHandlerImpl; -import dev.dsf.bpe.plugin.ProcessIdAndVersion; -import dev.dsf.bpe.plugin.ProcessPluginFactory; import dev.dsf.bpe.plugin.ProcessPluginLoader; import dev.dsf.bpe.plugin.ProcessPluginLoaderImpl; import dev.dsf.bpe.plugin.ProcessPluginManager; import dev.dsf.bpe.plugin.ProcessPluginManagerImpl; -import dev.dsf.bpe.v1.ProcessPluginApi; -import dev.dsf.bpe.v1.ProcessPluginApiImpl; -import dev.dsf.bpe.v1.ProcessPluginDefinition; -import dev.dsf.bpe.v1.config.ProxyConfig; -import dev.dsf.bpe.v1.config.ProxyConfigDelegate; -import dev.dsf.bpe.v1.plugin.ProcessPluginFactoryImpl; -import dev.dsf.bpe.v1.service.EndpointProvider; -import dev.dsf.bpe.v1.service.EndpointProviderImpl; -import dev.dsf.bpe.v1.service.FhirWebserviceClientProvider; -import dev.dsf.bpe.v1.service.FhirWebserviceClientProviderImpl; -import dev.dsf.bpe.v1.service.MailService; -import dev.dsf.bpe.v1.service.MailServiceImpl; -import dev.dsf.bpe.v1.service.OrganizationProvider; -import dev.dsf.bpe.v1.service.OrganizationProviderImpl; -import dev.dsf.bpe.v1.service.QuestionnaireResponseHelper; -import dev.dsf.bpe.v1.service.QuestionnaireResponseHelperImpl; -import dev.dsf.bpe.v1.service.TaskHelper; -import dev.dsf.bpe.v1.service.TaskHelperImpl; -import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelper; -import dev.dsf.fhir.authorization.process.ProcessAuthorizationHelperImpl; -import dev.dsf.fhir.authorization.read.ReadAccessHelper; -import dev.dsf.fhir.authorization.read.ReadAccessHelperImpl; @Configuration public class PluginConfig { - @Autowired - private Environment environment; - @Autowired private PropertiesConfig propertiesConfig; @@ -64,46 +34,11 @@ public class PluginConfig @Autowired private DaoConfig daoConfig; - @Autowired - private MailConfig mailConfig; - - @Autowired - private SerializerConfig serializerConfig; - @Autowired private CamundaConfig camundaConfig; - @Bean - public ProcessPluginApi processPluginApiV1() - { - ProxyConfig proxyConfig = new ProxyConfigDelegate(propertiesConfig.proxyConfig()); - - FhirWebserviceClientProvider clientProvider = new FhirWebserviceClientProviderImpl( - fhirClientConfig.clientProvider()); - EndpointProvider endpointProvider = new EndpointProviderImpl(clientProvider, - propertiesConfig.getFhirServerBaseUrl()); - FhirContext fhirContext = fhirConfig.fhirContext(); - MailService mailService = new MailServiceImpl(mailConfig.mailService()); - ObjectMapper objectMapper = serializerConfig.objectMapper(); - OrganizationProvider organizationProvider = new OrganizationProviderImpl(clientProvider, - propertiesConfig.getFhirServerBaseUrl()); - - ProcessAuthorizationHelper processAuthorizationHelper = new ProcessAuthorizationHelperImpl(); - QuestionnaireResponseHelper questionnaireResponseHelper = new QuestionnaireResponseHelperImpl( - propertiesConfig.getFhirServerBaseUrl()); - ReadAccessHelper readAccessHelper = new ReadAccessHelperImpl(); - TaskHelper taskHelper = new TaskHelperImpl(propertiesConfig.getFhirServerBaseUrl()); - - return new ProcessPluginApiImpl(proxyConfig, endpointProvider, fhirContext, clientProvider, mailService, - objectMapper, organizationProvider, processAuthorizationHelper, questionnaireResponseHelper, - readAccessHelper, taskHelper); - } - - @Bean - public ProcessPluginFactory processPluginFactoryV1() - { - return new ProcessPluginFactoryImpl(processPluginApiV1()); - } + @Autowired + private List processPluginFactories; @Bean public ProcessPluginLoader processPluginLoader() @@ -114,8 +49,7 @@ public ProcessPluginLoader processPluginLoader() throw new RuntimeException( "Process plug in directory '" + processPluginDirectoryPath.toString() + "' not readable"); - return new ProcessPluginLoaderImpl(List.of(processPluginFactoryV1()), processPluginDirectoryPath, - fhirConfig.fhirContext(), (ConfigurableEnvironment) environment); + return new ProcessPluginLoaderImpl(processPluginFactories, processPluginDirectoryPath); } @Bean diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginFactoryConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginFactoryConfig.java new file mode 100644 index 000000000..2fdba90c2 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginFactoryConfig.java @@ -0,0 +1,199 @@ +package dev.dsf.bpe.spring.config; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.function.Consumer; + +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; + +import org.bouncycastle.pkcs.PKCSException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; + +import dev.dsf.bpe.api.config.ClientConfig; +import dev.dsf.bpe.api.config.ProxyConfig; +import dev.dsf.bpe.api.plugin.ProcessPluginFactory; +import dev.dsf.bpe.api.service.BpeMailService; +import dev.dsf.bpe.api.service.BuildInfoProvider; +import dev.dsf.bpe.plugin.ProcessPluginApiClassLoaderFactory; +import dev.dsf.bpe.plugin.ProcessPluginApiFactory; + +@Configuration +public class PluginFactoryConfig extends AbstractConfig +{ + @Autowired + private Environment environment; + + @Autowired + private PropertiesConfig propertiesConfig; + + @Autowired + private BuildInfoReaderConfig buildInfoReaderConfig; + + @Autowired + private MailConfig mailConfig; + + @Bean + public ProcessPluginApiClassLoaderFactory pluginApiClassLoaderFactory() + { + return new ProcessPluginApiClassLoaderFactory(); + } + + @Bean + public ProcessPluginApiFactory processPluginApiFactory() + { + ProxyConfig proxyConfig = new ProxyConfig() + { + @Override + public boolean isNoProxyUrl(String targetUrl) + { + return propertiesConfig.proxyConfig().isNoProxyUrl(targetUrl); + } + + @Override + public boolean isEnabled(String targetUrl) + { + return propertiesConfig.proxyConfig().isEnabled(targetUrl); + } + + @Override + public boolean isEnabled() + { + return propertiesConfig.proxyConfig().isEnabled(); + } + + @Override + public String getUsername() + { + return propertiesConfig.proxyConfig().getUsername(); + } + + @Override + public String getUrl() + { + return propertiesConfig.proxyConfig().getUrl(); + } + + @Override + public char[] getPassword() + { + return propertiesConfig.proxyConfig().getPassword(); + } + + @Override + public List getNoProxyUrls() + { + return propertiesConfig.proxyConfig().getNoProxyUrls(); + } + }; + + ClientConfig clientConfig = new ClientConfig() + { + @Override + public KeyStore getWebserviceKeyStore(char[] keyStorePassword) + { + try + { + return createKeyStore(propertiesConfig.getClientCertificateFile(), + propertiesConfig.getClientCertificatePrivateKeyFile(), + propertiesConfig.getClientCertificatePrivateKeyFilePassword(), keyStorePassword); + } + catch (CertificateException | KeyStoreException | NoSuchAlgorithmException | IOException + | PKCSException e) + { + throw new RuntimeException(e); + } + } + + @Override + public KeyStore getWebserviceTrustStore() + { + try + { + return createTrustStore(propertiesConfig.getClientCertificateTrustStoreFile()); + } + catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException e) + { + throw new RuntimeException(e); + } + } + + @Override + public boolean getWebserviceClientRemoteVerbose() + { + return propertiesConfig.getWebserviceClientRemoteVerbose(); + } + + @Override + public int getWebserviceClientRemoteReadTimeout() + { + return propertiesConfig.getWebserviceClientRemoteReadTimeout(); + } + + @Override + public int getWebserviceClientRemoteConnectTimeout() + { + return propertiesConfig.getWebserviceClientRemoteConnectTimeout(); + } + + @Override + public boolean getWebserviceClientLocalVerbose() + { + return propertiesConfig.getWebserviceClientLocalVerbose(); + } + + @Override + public int getWebserviceClientLocalReadTimeout() + { + return propertiesConfig.getWebserviceClientLocalReadTimeout(); + } + + @Override + public int getWebserviceClientLocalConnectTimeout() + { + return propertiesConfig.getWebserviceClientLocalConnectTimeout(); + } + + @Override + public String getFhirServerBaseUrl() + { + return propertiesConfig.getFhirServerBaseUrl(); + } + }; + + BuildInfoProvider buildInfoProvider = new BuildInfoProvider() + { + @Override + public String getProjectVersion() + { + return buildInfoReaderConfig.buildInfoReader().getProjectVersion(); + } + }; + + BpeMailService bpeMailService = new BpeMailService() + { + @Override + public void send(String subject, MimeBodyPart body, Consumer messageModifier) + { + mailConfig.mailService().send(subject, body, messageModifier); + } + }; + + return new ProcessPluginApiFactory((ConfigurableEnvironment) environment, clientConfig, proxyConfig, + buildInfoProvider, bpeMailService, pluginApiClassLoaderFactory()); + } + + @Bean + public List processPluginFactories() + { + return processPluginApiFactory().initialize(); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/SerializerConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/SerializerConfig.java deleted file mode 100644 index ea7ecb4d8..000000000 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/SerializerConfig.java +++ /dev/null @@ -1,50 +0,0 @@ -package dev.dsf.bpe.spring.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import dev.dsf.bpe.variables.FhirResourceSerializer; -import dev.dsf.bpe.variables.FhirResourcesListSerializer; -import dev.dsf.bpe.variables.ObjectMapperFactory; -import dev.dsf.bpe.variables.TargetSerializer; -import dev.dsf.bpe.variables.TargetsSerializer; - -@Configuration -public class SerializerConfig -{ - @Autowired - private FhirConfig fhirConfig; - - @Bean - public ObjectMapper objectMapper() - { - return ObjectMapperFactory.createObjectMapper(fhirConfig.fhirContext()); - } - - @Bean - public FhirResourceSerializer fhirResourceSerializer() - { - return new FhirResourceSerializer(fhirConfig.fhirContext()); - } - - @Bean - public FhirResourcesListSerializer fhirResourcesListSerializer() - { - return new FhirResourcesListSerializer(objectMapper()); - } - - @Bean - public TargetSerializer targetSerializer() - { - return new TargetSerializer(objectMapper()); - } - - @Bean - public TargetsSerializer targetsSerializer() - { - return new TargetsSerializer(objectMapper()); - } -} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/WebsocketConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/WebsocketConfig.java index 9894ca95e..cf65710da 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/WebsocketConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/WebsocketConfig.java @@ -7,8 +7,8 @@ import org.springframework.context.annotation.Configuration; import dev.dsf.bpe.subscription.ConcurrentSubscriptionHandlerFactory; -import dev.dsf.bpe.subscription.FhirConnector; -import dev.dsf.bpe.subscription.FhirConnectorImpl; +import dev.dsf.bpe.subscription.LocalFhirConnector; +import dev.dsf.bpe.subscription.LocalFhirConnectorImpl; import dev.dsf.bpe.subscription.QuestionnaireResponseHandler; import dev.dsf.bpe.subscription.QuestionnaireResponseSubscriptionHandlerFactory; import dev.dsf.bpe.subscription.ResourceHandler; @@ -34,11 +34,15 @@ public class WebsocketConfig @Autowired private FhirClientConfig fhirClientConfig; + @Autowired + private PluginConfig pluginConfig; + @Bean public ResourceHandler taskHandler() { - return new TaskHandler(camundaConfig.processEngine().getRuntimeService(), - camundaConfig.processEngine().getRepositoryService(), + return new TaskHandler(camundaConfig.processEngine().getRepositoryService(), + pluginConfig.processPluginManager(), fhirConfig.fhirContext(), + camundaConfig.processEngine().getRuntimeService(), fhirClientConfig.clientProvider().getLocalWebserviceClient()); } @@ -50,17 +54,20 @@ public SubscriptionHandlerFactory taskSubscriptionHandlerFactory() } @Bean - public FhirConnector fhirConnectorTask() + public LocalFhirConnector fhirConnectorTask() { - return new FhirConnectorImpl<>(Task.class, fhirClientConfig.clientProvider(), taskSubscriptionHandlerFactory(), - fhirConfig.fhirContext(), propertiesConfig.getTaskSubscriptionSearchParameter(), - propertiesConfig.getWebsocketRetrySleepMillis(), propertiesConfig.getWebsocketMaxRetries()); + return new LocalFhirConnectorImpl<>(Task.class, fhirClientConfig.clientProvider(), + taskSubscriptionHandlerFactory(), fhirConfig.fhirContext(), + propertiesConfig.getTaskSubscriptionSearchParameter(), propertiesConfig.getWebsocketRetrySleepMillis(), + propertiesConfig.getWebsocketMaxRetries()); } @Bean public ResourceHandler questionnaireResponseHandler() { - return new QuestionnaireResponseHandler(camundaConfig.processEngine().getTaskService()); + return new QuestionnaireResponseHandler(camundaConfig.processEngine().getRepositoryService(), + pluginConfig.processPluginManager(), fhirConfig.fhirContext(), + camundaConfig.processEngine().getTaskService()); } @Bean @@ -72,9 +79,9 @@ public SubscriptionHandlerFactory questionnaireResponseSu } @Bean - public FhirConnector fhirConnectorQuestionnaireResponse() + public LocalFhirConnector fhirConnectorQuestionnaireResponse() { - return new FhirConnectorImpl<>(QuestionnaireResponse.class, fhirClientConfig.clientProvider(), + return new LocalFhirConnectorImpl<>(QuestionnaireResponse.class, fhirClientConfig.clientProvider(), questionnaireResponseSubscriptionHandlerFactory(), fhirConfig.fhirContext(), propertiesConfig.getQuestionnaireResponseSubscriptionSearchParameter(), propertiesConfig.getWebsocketRetrySleepMillis(), propertiesConfig.getWebsocketMaxRetries()); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/AbstractResourceHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/AbstractResourceHandler.java new file mode 100644 index 000000000..9447b0ada --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/AbstractResourceHandler.java @@ -0,0 +1,51 @@ +package dev.dsf.bpe.subscription; + +import java.util.Objects; +import java.util.Optional; + +import org.camunda.bpm.engine.RepositoryService; +import org.camunda.bpm.engine.repository.ProcessDefinition; +import org.springframework.beans.factory.InitializingBean; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessPlugin; +import dev.dsf.bpe.plugin.ProcessPluginManager; + +public abstract class AbstractResourceHandler implements InitializingBean +{ + protected final RepositoryService repositoryService; + + private final ProcessPluginManager processPluginManager; + private final FhirContext fhirContext; + + public AbstractResourceHandler(RepositoryService repositoryService, ProcessPluginManager processPluginManager, + FhirContext fhirContext) + { + this.repositoryService = repositoryService; + this.processPluginManager = processPluginManager; + this.fhirContext = fhirContext; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(repositoryService, "repositoryService"); + Objects.requireNonNull(processPluginManager, "processPluginManager"); + Objects.requireNonNull(fhirContext, "fhirContext"); + } + + protected final IParser newJsonParser() + { + IParser p = fhirContext.newJsonParser(); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + return p; + } + + protected final Optional getProcessPlugin(ProcessDefinition processDefinition) + { + return processPluginManager.getProcessPlugin(ProcessIdAndVersion.fromDefinition(processDefinition)); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/ConcurrentSubscriptionHandlerFactory.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/ConcurrentSubscriptionHandlerFactory.java index f5b652b4c..a24b79853 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/ConcurrentSubscriptionHandlerFactory.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/ConcurrentSubscriptionHandlerFactory.java @@ -11,7 +11,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; -import dev.dsf.fhir.client.FhirWebserviceClient; +import dev.dsf.bpe.client.FhirWebserviceClient; public class ConcurrentSubscriptionHandlerFactory implements SubscriptionHandlerFactory, InitializingBean diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/ExistingResourceLoaderImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/ExistingResourceLoaderImpl.java index dee0ce922..8acc74678 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/ExistingResourceLoaderImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/ExistingResourceLoaderImpl.java @@ -17,8 +17,8 @@ import org.slf4j.LoggerFactory; import ca.uhn.fhir.model.api.annotation.ResourceDef; +import dev.dsf.bpe.client.FhirWebserviceClient; import dev.dsf.bpe.dao.LastEventTimeDao; -import dev.dsf.fhir.client.FhirWebserviceClient; import jakarta.ws.rs.core.UriBuilder; public class ExistingResourceLoaderImpl implements ExistingResourceLoader diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/FhirConnector.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/LocalFhirConnector.java similarity index 60% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/FhirConnector.java rename to dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/LocalFhirConnector.java index 1ee32c4a9..7be882bbe 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/FhirConnector.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/LocalFhirConnector.java @@ -1,6 +1,6 @@ package dev.dsf.bpe.subscription; -public interface FhirConnector +public interface LocalFhirConnector { void connect(); } \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/FhirConnectorImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/LocalFhirConnectorImpl.java similarity index 96% rename from dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/FhirConnectorImpl.java rename to dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/LocalFhirConnectorImpl.java index d53a85f7d..d01b5f095 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/FhirConnectorImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/LocalFhirConnectorImpl.java @@ -22,24 +22,24 @@ import ca.uhn.fhir.model.api.annotation.ResourceDef; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.Constants; -import dev.dsf.bpe.client.FhirClientProvider; -import dev.dsf.fhir.client.FhirWebserviceClient; +import dev.dsf.bpe.client.FhirWebserviceClient; +import dev.dsf.bpe.client.LocalFhirClientProvider; import dev.dsf.fhir.client.WebsocketClient; -public class FhirConnectorImpl implements FhirConnector, InitializingBean +public class LocalFhirConnectorImpl implements LocalFhirConnector, InitializingBean { - private static final Logger logger = LoggerFactory.getLogger(FhirConnectorImpl.class); + private static final Logger logger = LoggerFactory.getLogger(LocalFhirConnectorImpl.class); private final Class resourceType; private final String resourceName; - private final FhirClientProvider clientProvider; + private final LocalFhirClientProvider clientProvider; private final FhirContext fhirContext; private final SubscriptionHandlerFactory subscriptionHandlerFactory; private final long retrySleepMillis; private final int maxRetries; private final Map> subscriptionSearchParameter; - public FhirConnectorImpl(Class resourceType, FhirClientProvider clientProvider, + public LocalFhirConnectorImpl(Class resourceType, LocalFhirClientProvider clientProvider, SubscriptionHandlerFactory subscriptionHandlerFactory, FhirContext fhirContext, String subscriptionSearchParameter, long retrySleepMillis, int maxRetries) { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/QuestionnaireResponseHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/QuestionnaireResponseHandler.java index 927d287b6..aa965006d 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/QuestionnaireResponseHandler.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/QuestionnaireResponseHandler.java @@ -6,33 +6,41 @@ import java.util.Optional; import java.util.stream.Collectors; +import org.camunda.bpm.engine.RepositoryService; import org.camunda.bpm.engine.TaskService; +import org.camunda.bpm.engine.repository.ProcessDefinition; +import org.camunda.bpm.engine.variable.value.PrimitiveValue; import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.StringType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; -import dev.dsf.bpe.v1.constants.CodeSystems.BpmnUserTask; -import dev.dsf.bpe.variables.FhirResourceValues; +import ca.uhn.fhir.context.FhirContext; +import dev.dsf.bpe.api.Constants; +import dev.dsf.bpe.api.plugin.ProcessPlugin; +import dev.dsf.bpe.plugin.ProcessPluginManager; -public class QuestionnaireResponseHandler implements ResourceHandler, InitializingBean +public class QuestionnaireResponseHandler extends AbstractResourceHandler + implements ResourceHandler, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(QuestionnaireResponseHandler.class); - public static final String QUESTIONNAIRE_RESPONSE_VARIABLE = QuestionnaireResponseHandler.class.getName() - + ".questionnaireResponse"; - private final TaskService userTaskService; - public QuestionnaireResponseHandler(TaskService userTaskService) + public QuestionnaireResponseHandler(RepositoryService repositoryService, ProcessPluginManager processPluginManager, + FhirContext fhirContext, TaskService userTaskService) { + super(repositoryService, processPluginManager, fhirContext); + this.userTaskService = userTaskService; } @Override public void afterPropertiesSet() throws Exception { + super.afterPropertiesSet(); + Objects.requireNonNull(userTaskService, "userTaskService"); } @@ -47,26 +55,36 @@ public void onResource(QuestionnaireResponse questionnaireResponse) String questionnaire = questionnaireResponse.getQuestionnaire(); String user = questionnaireResponse.getAuthor().getIdentifier().getValue(); String userType = questionnaireResponse.getAuthor().getType(); - String businessKey = getStringValueFromItems(items, BpmnUserTask.Codes.BUSINESS_KEY, + String businessKey = getStringValueFromItems(items, Constants.ITEM_LINK_ID_BUSINESS_KEY, questionnaireResponseId).orElse("?"); - Optional userTaskIdOpt = getStringValueFromItems(items, BpmnUserTask.Codes.USER_TASK_ID, + Optional userTaskIdOpt = getStringValueFromItems(items, Constants.ITEM_LINK_ID_USER_TASK_ID, questionnaireResponseId); userTaskIdOpt.ifPresentOrElse(userTaskId -> { + String processDefinitionId = userTaskService.createTaskQuery().taskId(userTaskId).singleResult() + .getProcessDefinitionId(); + ProcessDefinition processDefinition = repositoryService.getProcessDefinition(processDefinitionId); + + Optional processPlugin = getProcessPlugin(processDefinition); + + PrimitiveValue fhirQuestionnaireResponseVariable = processPlugin.get() + .createFhirQuestionnaireResponseVariable( + newJsonParser().encodeResourceToString(questionnaireResponse)); + Map variables = Map.of(Constants.QUESTIONNAIRE_RESPONSE_VARIABLE, + fhirQuestionnaireResponseVariable); + logger.info( "QuestionnaireResponse '{}' for Questionnaire '{}' completed [userTaskId: {}, businessKey: {}, user: {}]", questionnaireResponseId, questionnaire, userTaskId, businessKey, user + "|" + userType); - Map variables = Map.of(QUESTIONNAIRE_RESPONSE_VARIABLE, - FhirResourceValues.create(questionnaireResponse)); userTaskService.complete(userTaskId, variables); }, () -> { logger.warn( "QuestionnaireResponse '{}' for Questionnaire '{}' has no answer with item.linkId '{}' [businessKey: {}, user: {}], ignoring QuestionnaireResponse", - questionnaireResponseId, questionnaire, BpmnUserTask.Codes.USER_TASK_ID, businessKey, + questionnaireResponseId, questionnaire, Constants.ITEM_LINK_ID_USER_TASK_ID, businessKey, user + "|" + userType); }); } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/QuestionnaireResponseSubscriptionHandlerFactory.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/QuestionnaireResponseSubscriptionHandlerFactory.java index cd2d6e1f2..80f3905e4 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/QuestionnaireResponseSubscriptionHandlerFactory.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/QuestionnaireResponseSubscriptionHandlerFactory.java @@ -5,8 +5,8 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.springframework.beans.factory.InitializingBean; +import dev.dsf.bpe.client.FhirWebserviceClient; import dev.dsf.bpe.dao.LastEventTimeDao; -import dev.dsf.fhir.client.FhirWebserviceClient; public class QuestionnaireResponseSubscriptionHandlerFactory implements SubscriptionHandlerFactory, InitializingBean diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/SubscriptionHandlerFactory.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/SubscriptionHandlerFactory.java index d4bae8325..6127db11a 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/SubscriptionHandlerFactory.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/SubscriptionHandlerFactory.java @@ -2,7 +2,7 @@ import org.hl7.fhir.r4.model.Resource; -import dev.dsf.fhir.client.FhirWebserviceClient; +import dev.dsf.bpe.client.FhirWebserviceClient; public interface SubscriptionHandlerFactory { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskHandler.java index cc6ef2d96..bd6707cde 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskHandler.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskHandler.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -20,6 +21,7 @@ import org.camunda.bpm.engine.runtime.MessageCorrelationBuilder; import org.camunda.bpm.engine.runtime.ProcessInstance; import org.camunda.bpm.engine.runtime.ProcessInstanceQuery; +import org.camunda.bpm.engine.variable.value.PrimitiveValue; import org.camunda.bpm.model.bpmn.BpmnModelInstance; import org.camunda.bpm.model.bpmn.instance.MessageEventDefinition; import org.camunda.bpm.model.bpmn.instance.StartEvent; @@ -32,15 +34,21 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; -import dev.dsf.bpe.v1.constants.BpmnExecutionVariables; -import dev.dsf.bpe.v1.constants.CodeSystems.BpmnMessage; -import dev.dsf.bpe.variables.FhirResourceValues; -import dev.dsf.fhir.client.FhirWebserviceClient; +import ca.uhn.fhir.context.FhirContext; +import dev.dsf.bpe.api.Constants; +import dev.dsf.bpe.api.plugin.ProcessPlugin; +import dev.dsf.bpe.client.FhirWebserviceClient; +import dev.dsf.bpe.plugin.ProcessPluginManager; -public class TaskHandler implements ResourceHandler, InitializingBean +public class TaskHandler extends AbstractResourceHandler implements ResourceHandler, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(TaskHandler.class); + private static final String INSTANTIATES_CANONICAL_PATTERN_STRING = "(?http[s]{0,1}://(?(?:(?:[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])\\.)+(?:[a-zA-Z0-9]{1,63}))" + + "/bpe/Process/(?[a-zA-Z0-9-]+))\\|(?\\d+\\.\\d+)$"; + private static final Pattern INSTANTIATES_CANONICAL_PATTERN = Pattern + .compile(INSTANTIATES_CANONICAL_PATTERN_STRING); + private static final class ProcessNotFoundException extends ProcessEngineException { private static final long serialVersionUID = 1L; @@ -88,30 +96,24 @@ String getShortMessage() } } - public static final String TASK_VARIABLE = TaskHandler.class.getName() + ".task"; - - private static final String INSTANTIATES_CANONICAL_PATTERN_STRING = "(?http[s]{0,1}://(?(?:(?:[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])\\.)+(?:[a-zA-Z0-9]{1,63}))" - + "/bpe/Process/(?[a-zA-Z0-9-]+))\\|(?\\d+\\.\\d+)$"; - private static final Pattern INSTANTIATES_CANONICAL_PATTERN = Pattern - .compile(INSTANTIATES_CANONICAL_PATTERN_STRING); - private final RuntimeService runtimeService; - private final RepositoryService repositoryService; private final FhirWebserviceClient webserviceClient; - public TaskHandler(RuntimeService runtimeService, RepositoryService repositoryService, - FhirWebserviceClient webserviceClient) + public TaskHandler(RepositoryService repositoryService, ProcessPluginManager processPluginManager, + FhirContext fhirContext, RuntimeService runtimeService, FhirWebserviceClient webserviceClient) { + super(repositoryService, processPluginManager, fhirContext); + this.runtimeService = runtimeService; - this.repositoryService = repositoryService; this.webserviceClient = webserviceClient; } @Override public void afterPropertiesSet() throws Exception { + super.afterPropertiesSet(); + Objects.requireNonNull(runtimeService, "runtimeService"); - Objects.requireNonNull(repositoryService, "repositoryService"); Objects.requireNonNull(webserviceClient, "webserviceClient"); } @@ -130,26 +132,39 @@ public void onResource(Task task) String processDefinitionKey = matcher.group("processName"); String processVersion = matcher.group("processVersion"); - String messageName = getFirstInputParameter(task, BpmnMessage.messageName()); - String businessKey = getFirstInputParameter(task, BpmnMessage.businessKey()); - String correlationKey = getFirstInputParameter(task, BpmnMessage.correlationKey()); + ProcessDefinition processDefinition = getProcessDefinition(processDomain, processDefinitionKey, processVersion); + + if (processDefinition == null) + throw new ProcessNotFoundException(processDomain, processDefinitionKey, processVersion, null); + + Optional processPlugin = getProcessPlugin(processDefinition); + + if (processPlugin.isEmpty()) + throw new ProcessNotFoundException(processDomain, processDefinitionKey, processVersion, null); + + String messageName = getFirstBpmnMessageInputParameter(task, Constants.BPMN_MESSAGE_MESSAGE_NAME); + String businessKey = getFirstBpmnMessageInputParameter(task, Constants.BPMN_MESSAGE_BUSINESS_KEY); + String correlationKey = getFirstBpmnMessageInputParameter(task, Constants.BPMN_MESSAGE_CORRELATION_KEY); if (businessKey == null) { businessKey = UUID.randomUUID().toString(); logger.debug("Adding business-key {} to Task with id {}", businessKey, task.getId()); - task.addInput().setType(new CodeableConcept(BpmnMessage.businessKey())) + task.addInput().setType(new CodeableConcept().addCoding( + new Coding().setSystem(Constants.BPMN_MESSAGE_URL).setCode(Constants.BPMN_MESSAGE_BUSINESS_KEY))) .setValue(new StringType(businessKey)); } task.setStatus(Task.TaskStatus.INPROGRESS); task = webserviceClient.update(task); - Map variables = Map.of(TASK_VARIABLE, FhirResourceValues.create(task)); + PrimitiveValue fhirTaskVariable = processPlugin.get() + .createFhirTaskVariable(newJsonParser().encodeResourceToString(task)); + Map variables = Map.of(Constants.TASK_VARIABLE, fhirTaskVariable); try { onMessage(businessKey, correlationKey, processDomain, processDefinitionKey, processVersion, messageName, - variables); + processDefinition.getId(), variables); } catch (MismatchingMessageCorrelationException e) { @@ -184,7 +199,10 @@ private void updateTaskFailed(Task task, Exception e) private void updateTaskFailed(Task task, String message) { - task.addOutput().setType(new CodeableConcept(BpmnMessage.error())).setValue(new StringType(message)); + task.addOutput() + .setType(new CodeableConcept().addCoding( + new Coding().setSystem(Constants.BPMN_MESSAGE_URL).setCode(Constants.BPMN_MESSAGE_ERROR))) + .setValue(new StringType(message)); task.setStatus(Task.TaskStatus.FAILED); try @@ -199,15 +217,15 @@ private void updateTaskFailed(Task task, String message) } } - private String getFirstInputParameter(Task task, Coding code) + private String getFirstBpmnMessageInputParameter(Task task, String code) { if (task == null || code == null) return null; return task.getInput().stream().filter(ParameterComponent::hasType) .filter(c -> c.getType().getCoding().stream() - .anyMatch(co -> co != null && Objects.equals(code.getSystem(), co.getSystem()) - && Objects.equals(code.getCode(), co.getCode()))) + .anyMatch(co -> co != null && Objects.equals(Constants.BPMN_MESSAGE_URL, co.getSystem()) + && Objects.equals(code, co.getCode()))) .filter(ParameterComponent::hasValue).map(ParameterComponent::getValue) .filter(v -> v instanceof StringType).map(v -> (StringType) v).map(StringType::getValue).findFirst() .orElse(null); @@ -226,11 +244,14 @@ private String getFirstInputParameter(Task task, Coding code) * not null * @param messageName * not null + * @param processDefinitionId + * not null * @param variables * may be null */ protected void onMessage(String businessKey, String correlationKey, String processDomain, - String processDefinitionKey, String processVersion, String messageName, Map variables) + String processDefinitionKey, String processVersion, String messageName, String processDefinitionId, + Map variables) { // businessKey may be null // correlationKey may be null @@ -238,25 +259,21 @@ protected void onMessage(String businessKey, String correlationKey, String proce Objects.requireNonNull(processDefinitionKey, "processDefinitionKey"); Objects.requireNonNull(processVersion, "processVersion"); Objects.requireNonNull(messageName, "messageName"); + Objects.requireNonNull(processDefinitionId, "processDefinitionId"); if (variables == null) variables = Collections.emptyMap(); - ProcessDefinition processDefinition = getProcessDefinition(processDomain, processDefinitionKey, processVersion); - - if (processDefinition == null) - throw new ProcessNotFoundException(processDomain, processDefinitionKey, processVersion, null); - if (businessKey == null) { - runtimeService.startProcessInstanceByMessageAndProcessDefinitionId(messageName, processDefinition.getId(), + runtimeService.startProcessInstanceByMessageAndProcessDefinitionId(messageName, processDefinitionId, UUID.randomUUID().toString(), variables); } else { - List instances = getProcessInstanceQuery(processDefinition, businessKey).list(); + List instances = getProcessInstanceQuery(processDefinitionId, businessKey).list(); List instancesWithAlternativeBusinessKey = getAlternativeProcessInstanceQuery( - processDefinition, businessKey).list(); + processDefinitionId, businessKey).list(); if (instances.size() + instancesWithAlternativeBusinessKey.size() > 1) logger.warn("instance-ids {}", @@ -265,7 +282,7 @@ protected void onMessage(String businessKey, String correlationKey, String proce if (instances.size() + instancesWithAlternativeBusinessKey.size() <= 0) { - BpmnModelInstance model = repositoryService.getBpmnModelInstance(processDefinition.getId()); + BpmnModelInstance model = repositoryService.getBpmnModelInstance(processDefinitionId); Collection startEvents = model == null ? Collections.emptySet() : model.getModelElementsByType(StartEvent.class); Stream startEventMesssageNames = startEvents.stream().flatMap(e -> @@ -276,7 +293,7 @@ protected void onMessage(String businessKey, String correlationKey, String proce if (startEventMesssageNames.anyMatch(m -> m.equals(messageName))) { - runtimeService.createMessageCorrelation(messageName).processDefinitionId(processDefinition.getId()) + runtimeService.createMessageCorrelation(messageName).processDefinitionId(processDefinitionId) .processInstanceBusinessKey(businessKey).setVariables(variables).correlateStartMessage(); } else @@ -292,12 +309,10 @@ protected void onMessage(String businessKey, String correlationKey, String proce .processInstanceBusinessKey(businessKey); else correlation = runtimeService.createMessageCorrelation(messageName).setVariables(variables) - .processInstanceVariableEquals(BpmnExecutionVariables.ALTERNATIVE_BUSINESS_KEY, - businessKey); + .processInstanceVariableEquals(Constants.ALTERNATIVE_BUSINESS_KEY, businessKey); if (correlationKey != null) - correlation = correlation.localVariableEquals(BpmnExecutionVariables.CORRELATION_KEY, - correlationKey); + correlation = correlation.localVariableEquals(Constants.CORRELATION_KEY, correlationKey); // throws MismatchingMessageCorrelationException - if none or more than one execution or process // definition is matched by the correlation @@ -318,16 +333,15 @@ private ProcessDefinition getProcessDefinition(String processDomain, String proc .processDefinitionKey(processDomain + "_" + processDefinitionKey).latestVersion().singleResult(); } - private ProcessInstanceQuery getProcessInstanceQuery(ProcessDefinition processDefinition, String businessKey) + private ProcessInstanceQuery getProcessInstanceQuery(String processDefinitionId, String businessKey) { - return runtimeService.createProcessInstanceQuery().processDefinitionId(processDefinition.getId()) + return runtimeService.createProcessInstanceQuery().processDefinitionId(processDefinitionId) .processInstanceBusinessKey(businessKey); } - private ProcessInstanceQuery getAlternativeProcessInstanceQuery(ProcessDefinition processDefinition, - String businessKey) + private ProcessInstanceQuery getAlternativeProcessInstanceQuery(String processDefinitionId, String businessKey) { - return runtimeService.createProcessInstanceQuery().processDefinitionId(processDefinition.getId()) - .variableValueEquals(BpmnExecutionVariables.ALTERNATIVE_BUSINESS_KEY, businessKey); + return runtimeService.createProcessInstanceQuery().processDefinitionId(processDefinitionId) + .variableValueEquals(Constants.ALTERNATIVE_BUSINESS_KEY, businessKey); } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskSubscriptionHandlerFactory.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskSubscriptionHandlerFactory.java index f5af45e2f..5130473f3 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskSubscriptionHandlerFactory.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskSubscriptionHandlerFactory.java @@ -5,8 +5,8 @@ import org.hl7.fhir.r4.model.Task; import org.springframework.beans.factory.InitializingBean; +import dev.dsf.bpe.client.FhirWebserviceClient; import dev.dsf.bpe.dao.LastEventTimeDao; -import dev.dsf.fhir.client.FhirWebserviceClient; public class TaskSubscriptionHandlerFactory implements SubscriptionHandlerFactory, InitializingBean { diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginFactoryImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginFactoryImpl.java deleted file mode 100644 index b476f5d28..000000000 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginFactoryImpl.java +++ /dev/null @@ -1,50 +0,0 @@ -package dev.dsf.bpe.v1.plugin; - -import java.nio.file.Path; -import java.util.Objects; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.env.ConfigurableEnvironment; - -import ca.uhn.fhir.context.FhirContext; -import dev.dsf.bpe.plugin.ProcessPlugin; -import dev.dsf.bpe.plugin.ProcessPluginFactory; -import dev.dsf.bpe.v1.ProcessPluginApi; -import dev.dsf.bpe.v1.ProcessPluginDefinition; - -public class ProcessPluginFactoryImpl implements ProcessPluginFactory, InitializingBean -{ - private final ProcessPluginApi processPluginApi; - - public ProcessPluginFactoryImpl(ProcessPluginApi processPluginApi) - { - this.processPluginApi = processPluginApi; - } - - @Override - public void afterPropertiesSet() throws Exception - { - Objects.requireNonNull(processPluginApi, "processPluginApi"); - } - - @Override - public int getApiVersion() - { - return 1; - } - - @Override - public Class getProcessPluginDefinitionType() - { - return ProcessPluginDefinition.class; - } - - @Override - public ProcessPlugin createProcessPlugin( - ProcessPluginDefinition processPluginDefinition, boolean draft, Path jarFile, ClassLoader classLoader, - FhirContext fhirContext, ConfigurableEnvironment environment) - { - return new ProcessPluginImpl(processPluginDefinition, processPluginApi, draft, jarFile, classLoader, - fhirContext, environment); - } -} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginImpl.java deleted file mode 100644 index 1dac279b5..000000000 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/plugin/ProcessPluginImpl.java +++ /dev/null @@ -1,85 +0,0 @@ -package dev.dsf.bpe.v1.plugin; - -import java.nio.file.Path; -import java.time.LocalDate; -import java.util.List; -import java.util.Map; - -import org.springframework.core.env.ConfigurableEnvironment; - -import ca.uhn.fhir.context.FhirContext; -import dev.dsf.bpe.plugin.AbstractProcessPlugin; -import dev.dsf.bpe.plugin.ProcessPlugin; -import dev.dsf.bpe.v1.ProcessPluginApi; -import dev.dsf.bpe.v1.ProcessPluginDefinition; - -public class ProcessPluginImpl extends AbstractProcessPlugin - implements ProcessPlugin -{ - public ProcessPluginImpl(ProcessPluginDefinition processPluginDefinition, ProcessPluginApi processPluginApi, - boolean draft, Path jarFile, ClassLoader classLoader, FhirContext fhirContext, - ConfigurableEnvironment environment) - { - super(processPluginDefinition, processPluginApi, draft, jarFile, classLoader, fhirContext, environment); - } - - @Override - protected List> getDefinitionSpringConfigurations() - { - return getProcessPluginDefinition().getSpringConfigurations(); - } - - @Override - protected String getDefinitionName() - { - return getProcessPluginDefinition().getName(); - } - - @Override - protected String getDefinitionVersion() - { - return getProcessPluginDefinition().getVersion(); - } - - @Override - protected String getDefinitionResourceVersion() - { - return getProcessPluginDefinition().getResourceVersion(); - } - - @Override - protected LocalDate getDefinitionReleaseDate() - { - return getProcessPluginDefinition().getReleaseDate(); - } - - @Override - protected LocalDate getDefinitionResourceReleaseDate() - { - return getProcessPluginDefinition().getResourceReleaseDate(); - } - - @Override - protected Map> getDefinitionFhirResourcesByProcessId() - { - return getProcessPluginDefinition().getFhirResourcesByProcessId(); - } - - @Override - protected List getDefinitionProcessModels() - { - return getProcessPluginDefinition().getProcessModels(); - } - - @Override - protected Class getDefaultSpringConfiguration() - { - return DefaultSpringConfiguration.class; - } - - @Override - protected String getProcessPluginApiVersion() - { - return "1"; - } -} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/FhirWebserviceClientProviderImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/FhirWebserviceClientProviderImpl.java deleted file mode 100644 index f61165cdb..000000000 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/v1/service/FhirWebserviceClientProviderImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.dsf.bpe.v1.service; - -import java.util.Objects; - -import org.springframework.beans.factory.InitializingBean; - -import dev.dsf.bpe.client.FhirClientProvider; -import dev.dsf.fhir.client.FhirWebserviceClient; - -public class FhirWebserviceClientProviderImpl implements FhirWebserviceClientProvider, InitializingBean -{ - private final FhirClientProvider delegate; - - public FhirWebserviceClientProviderImpl(FhirClientProvider delegate) - { - this.delegate = delegate; - } - - @Override - public void afterPropertiesSet() throws Exception - { - Objects.requireNonNull(delegate, "delegate"); - } - - @Override - public FhirWebserviceClient getLocalWebserviceClient() - { - return delegate.getLocalWebserviceClient(); - } - - @Override - public FhirWebserviceClient getWebserviceClient(String webserviceUrl) - { - return delegate.getWebserviceClient(webserviceUrl); - } -} diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/api/v1/allowed-bpe-classes.list b/dsf-bpe/dsf-bpe-server/src/main/resources/api/v1/allowed-bpe-classes.list new file mode 100644 index 000000000..73d2e4218 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/api/v1/allowed-bpe-classes.list @@ -0,0 +1,119 @@ +com.fasterxml.jackson.annotation.JsonAlias +com.fasterxml.jackson.annotation.JsonCreator +com.fasterxml.jackson.annotation.JsonIgnore +com.fasterxml.jackson.annotation.JsonInclude$Include +com.fasterxml.jackson.annotation.JsonProperty +com.fasterxml.jackson.core.JsonFactory +com.fasterxml.jackson.core.JsonGenerator +com.fasterxml.jackson.core.JsonGenerator$Feature +com.fasterxml.jackson.core.JsonParser$Feature +com.fasterxml.jackson.core.JsonProcessingException +com.fasterxml.jackson.core.PrettyPrinter +com.fasterxml.jackson.core.StreamReadConstraints +com.fasterxml.jackson.core.StreamReadConstraints$Builder +com.fasterxml.jackson.core.util.DefaultPrettyPrinter +com.fasterxml.jackson.core.util.DefaultPrettyPrinter$Indenter +com.fasterxml.jackson.databind.DeserializationFeature +com.fasterxml.jackson.databind.json.JsonMapper +com.fasterxml.jackson.databind.json.JsonMapper$Builder +com.fasterxml.jackson.databind.JsonDeserializer +com.fasterxml.jackson.databind.JsonNode +com.fasterxml.jackson.databind.JsonSerializer +com.fasterxml.jackson.databind.MapperFeature +com.fasterxml.jackson.databind.Module +com.fasterxml.jackson.databind.module.SimpleModule +com.fasterxml.jackson.databind.node.ArrayNode +com.fasterxml.jackson.databind.node.DecimalNode +com.fasterxml.jackson.databind.node.JsonNodeFactory +com.fasterxml.jackson.databind.node.ObjectNode +com.fasterxml.jackson.databind.ObjectMapper +dev.dsf.bpe.api.config.ClientConfig +dev.dsf.bpe.api.config.ProxyConfig +dev.dsf.bpe.api.listener.ListenerFactory +dev.dsf.bpe.api.listener.ListenerFactoryImpl +dev.dsf.bpe.api.plugin.AbstractProcessPlugin +dev.dsf.bpe.api.plugin.AbstractProcessPluginFactory +dev.dsf.bpe.api.plugin.ProcessPlugin +dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder +dev.dsf.bpe.api.plugin.ProcessPluginDeploymentListener +dev.dsf.bpe.api.plugin.ProcessPluginFactory +dev.dsf.bpe.api.plugin.ProcessPluginFhirConfig +dev.dsf.bpe.api.plugin.ProcessPluginFhirConfig$Identifier +dev.dsf.bpe.api.plugin.ProcessPluginFhirConfig$Reference +dev.dsf.bpe.api.service.BpeMailService +dev.dsf.bpe.api.service.BuildInfoProvider +jakarta.ws.rs.client.Client +jakarta.ws.rs.client.ClientBuilder +jakarta.ws.rs.client.ClientRequestContext +jakarta.ws.rs.client.ClientRequestFilter +jakarta.ws.rs.client.Entity +jakarta.ws.rs.client.Invocation$Builder +jakarta.ws.rs.client.WebTarget +jakarta.ws.rs.Consumes +jakarta.ws.rs.core.Configurable +jakarta.ws.rs.core.Configuration +jakarta.ws.rs.core.EntityTag +jakarta.ws.rs.core.MediaType +jakarta.ws.rs.core.MultivaluedMap +jakarta.ws.rs.core.Response +jakarta.ws.rs.core.Response$Status +jakarta.ws.rs.core.Response$StatusType +jakarta.ws.rs.ext.MessageBodyReader +jakarta.ws.rs.ext.MessageBodyWriter +jakarta.ws.rs.ext.Provider +jakarta.ws.rs.ProcessingException +jakarta.ws.rs.Produces +jakarta.ws.rs.WebApplicationException +javax.annotation.Nullable +org.apache.commons.codec.binary.Base64 +org.apache.commons.lang3.StringUtils +org.apache.commons.lang3.time.FastDateFormat +org.apache.commons.lang3.tuple.Pair +org.apache.commons.lang3.Validate +org.apache.commons.text.WordUtils +org.camunda.bpm.engine.delegate.BaseDelegateExecution +org.camunda.bpm.engine.delegate.BpmnError +org.camunda.bpm.engine.delegate.DelegateExecution +org.camunda.bpm.engine.delegate.DelegateTask +org.camunda.bpm.engine.delegate.ExecutionListener +org.camunda.bpm.engine.delegate.JavaDelegate +org.camunda.bpm.engine.delegate.TaskListener +org.camunda.bpm.engine.impl.el.FixedValue +org.camunda.bpm.engine.impl.variable.serializer.PrimitiveValueSerializer +org.camunda.bpm.engine.impl.variable.serializer.TypedValueSerializer +org.camunda.bpm.engine.impl.variable.serializer.ValueFields +org.camunda.bpm.engine.variable.Variables +org.camunda.bpm.engine.variable.impl.type.PrimitiveValueTypeImpl +org.camunda.bpm.engine.variable.impl.value.PrimitiveTypeValueImpl +org.camunda.bpm.engine.variable.impl.value.UntypedValueImpl +org.camunda.bpm.engine.variable.type.PrimitiveValueType +org.camunda.bpm.engine.variable.value.PrimitiveValue +org.camunda.bpm.engine.variable.value.TypedValue +org.camunda.bpm.model.bpmn.instance.IntermediateThrowEvent +org.camunda.bpm.model.bpmn.instance.UserTask +org.glassfish.jersey.apache.connector.ApacheConnectorProvider +org.glassfish.jersey.client.ClientConfig +org.glassfish.jersey.client.spi.ConnectorProvider +org.glassfish.jersey.SslConfigurator +org.slf4j.Logger +org.slf4j.LoggerFactory +org.springframework.beans.BeansException +org.springframework.beans.factory.annotation.Autowired +org.springframework.beans.factory.annotation.Value +org.springframework.beans.factory.BeanFactory +org.springframework.beans.factory.BeanFactoryAware +org.springframework.beans.factory.InitializingBean +org.springframework.cglib.core.ReflectUtils +org.springframework.cglib.core.Signature +org.springframework.cglib.proxy.Callback +org.springframework.cglib.proxy.MethodInterceptor +org.springframework.cglib.proxy.MethodProxy +org.springframework.cglib.proxy.NoOp +org.springframework.cglib.reflect.FastClass +org.springframework.context.annotation.Bean +org.springframework.context.annotation.Configuration +org.springframework.context.annotation.ConfigurationClassEnhancer$EnhancedConfiguration +org.springframework.context.annotation.Scope +org.springframework.context.ApplicationContext +org.springframework.web.util.UriComponents +org.springframework.web.util.UriComponentsBuilder \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/api/v1/allowed-bpe-resources.list b/dsf-bpe/dsf-bpe-server/src/main/resources/api/v1/allowed-bpe-resources.list new file mode 100644 index 000000000..e69de29bb diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/api/v1/api-resources-with-priority.list b/dsf-bpe/dsf-bpe-server/src/main/resources/api/v1/api-resources-with-priority.list new file mode 100644 index 000000000..08987fe78 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/api/v1/api-resources-with-priority.list @@ -0,0 +1,6 @@ +ca/uhn/fhir/hapi-fhir-base-build.properties +ca/uhn/fhir/i18n/hapi-messages.properties +dev/dsf/bpe/v1/plugin/ApiServicesSpringConfiguration.class +dev/dsf/bpe/v1/spring/ApiServiceConfig.class +META-INF/services/dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder +org/hl7/fhir/r4/model/fhirversion.properties \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/api/v2/allowed-bpe-classes.list b/dsf-bpe/dsf-bpe-server/src/main/resources/api/v2/allowed-bpe-classes.list new file mode 100644 index 000000000..8bc43a914 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/api/v2/allowed-bpe-classes.list @@ -0,0 +1,114 @@ +com.fasterxml.jackson.annotation.JsonCreator +com.fasterxml.jackson.annotation.JsonIgnore +com.fasterxml.jackson.annotation.JsonInclude$Include +com.fasterxml.jackson.annotation.JsonProperty +com.fasterxml.jackson.core.json.JsonReadFeature +com.fasterxml.jackson.core.JsonFactory +com.fasterxml.jackson.core.JsonGenerator +com.fasterxml.jackson.core.JsonGenerator$Feature +com.fasterxml.jackson.core.JsonParser$Feature +com.fasterxml.jackson.core.JsonProcessingException +com.fasterxml.jackson.core.PrettyPrinter +com.fasterxml.jackson.core.StreamReadConstraints +com.fasterxml.jackson.core.StreamReadConstraints$Builder +com.fasterxml.jackson.core.util.DefaultPrettyPrinter$Indenter +com.fasterxml.jackson.databind.DeserializationFeature +com.fasterxml.jackson.databind.json.JsonMapper +com.fasterxml.jackson.databind.json.JsonMapper$Builder +com.fasterxml.jackson.databind.JsonDeserializer +com.fasterxml.jackson.databind.JsonNode +com.fasterxml.jackson.databind.JsonSerializer +com.fasterxml.jackson.databind.MapperFeature +com.fasterxml.jackson.databind.Module +com.fasterxml.jackson.databind.module.SimpleModule +com.fasterxml.jackson.databind.node.ArrayNode +com.fasterxml.jackson.databind.node.DecimalNode +com.fasterxml.jackson.databind.node.JsonNodeFactory +com.fasterxml.jackson.databind.node.JsonNodeType +com.fasterxml.jackson.databind.node.ObjectNode +com.fasterxml.jackson.databind.ObjectMapper +com.google.common.collect.Sets +dev.dsf.bpe.api.config.ClientConfig +dev.dsf.bpe.api.config.ProxyConfig +dev.dsf.bpe.api.listener.ListenerFactory +dev.dsf.bpe.api.listener.ListenerFactoryImpl +dev.dsf.bpe.api.plugin.AbstractProcessPlugin +dev.dsf.bpe.api.plugin.AbstractProcessPluginFactory +dev.dsf.bpe.api.plugin.ProcessPlugin +dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder +dev.dsf.bpe.api.plugin.ProcessPluginDeploymentListener +dev.dsf.bpe.api.plugin.ProcessPluginFactory +dev.dsf.bpe.api.plugin.ProcessPluginFhirConfig +dev.dsf.bpe.api.plugin.ProcessPluginFhirConfig$Identifier +dev.dsf.bpe.api.plugin.ProcessPluginFhirConfig$Reference +dev.dsf.bpe.api.service.BpeMailService +dev.dsf.bpe.api.service.BuildInfoProvider +jakarta.annotation.Nonnull +jakarta.annotation.Nullable +jakarta.ws.rs.client.Client +jakarta.ws.rs.client.ClientBuilder +jakarta.ws.rs.client.ClientRequestContext +jakarta.ws.rs.client.ClientRequestFilter +jakarta.ws.rs.client.Entity +jakarta.ws.rs.client.Invocation$Builder +jakarta.ws.rs.client.WebTarget +jakarta.ws.rs.Consumes +jakarta.ws.rs.core.Configurable +jakarta.ws.rs.core.Configuration +jakarta.ws.rs.core.EntityTag +jakarta.ws.rs.core.MediaType +jakarta.ws.rs.core.MultivaluedMap +jakarta.ws.rs.core.Response +jakarta.ws.rs.core.Response$Status +jakarta.ws.rs.core.Response$StatusType +jakarta.ws.rs.ext.MessageBodyReader +jakarta.ws.rs.ext.MessageBodyWriter +jakarta.ws.rs.ext.Provider +jakarta.ws.rs.ProcessingException +jakarta.ws.rs.Produces +jakarta.ws.rs.WebApplicationException +org.apache.commons.io.output.StringBuilderWriter +org.apache.commons.lang3.StringUtils +org.apache.commons.lang3.tuple.Pair +org.apache.commons.lang3.Validate +org.apache.commons.text.WordUtils +org.camunda.bpm.engine.delegate.BaseDelegateExecution +org.camunda.bpm.engine.delegate.BpmnError +org.camunda.bpm.engine.delegate.DelegateExecution +org.camunda.bpm.engine.delegate.DelegateTask +org.camunda.bpm.engine.delegate.ExecutionListener +org.camunda.bpm.engine.delegate.JavaDelegate +org.camunda.bpm.engine.delegate.TaskListener +org.camunda.bpm.engine.impl.variable.serializer.PrimitiveValueSerializer +org.camunda.bpm.engine.impl.variable.serializer.TypedValueSerializer +org.camunda.bpm.engine.impl.variable.serializer.ValueFields +org.camunda.bpm.engine.variable.impl.type.PrimitiveValueTypeImpl +org.camunda.bpm.engine.variable.impl.value.PrimitiveTypeValueImpl +org.camunda.bpm.engine.variable.impl.value.UntypedValueImpl +org.camunda.bpm.engine.variable.type.PrimitiveValueType +org.camunda.bpm.engine.variable.value.PrimitiveValue +org.camunda.bpm.engine.variable.value.TypedValue +org.camunda.bpm.model.bpmn.instance.UserTask +org.glassfish.jersey.apache.connector.ApacheConnectorProvider +org.glassfish.jersey.client.ClientConfig +org.glassfish.jersey.client.spi.ConnectorProvider +org.glassfish.jersey.SslConfigurator +org.slf4j.Logger +org.slf4j.LoggerFactory +org.springframework.beans.BeansException +org.springframework.beans.factory.annotation.Autowired +org.springframework.beans.factory.BeanFactory +org.springframework.beans.factory.BeanFactoryAware +org.springframework.beans.factory.InitializingBean +org.springframework.cglib.core.ReflectUtils +org.springframework.cglib.core.Signature +org.springframework.cglib.proxy.Callback +org.springframework.cglib.proxy.MethodInterceptor +org.springframework.cglib.proxy.MethodProxy +org.springframework.cglib.proxy.NoOp +org.springframework.cglib.reflect.FastClass +org.springframework.context.annotation.Bean +org.springframework.context.annotation.Configuration +org.springframework.context.annotation.ConfigurationClassEnhancer$EnhancedConfiguration +org.springframework.context.annotation.Scope +org.springframework.context.ApplicationContext \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/api/v2/allowed-bpe-resources.list b/dsf-bpe/dsf-bpe-server/src/main/resources/api/v2/allowed-bpe-resources.list new file mode 100644 index 000000000..e69de29bb diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/api/v2/api-resources-with-priority.list b/dsf-bpe/dsf-bpe-server/src/main/resources/api/v2/api-resources-with-priority.list new file mode 100644 index 000000000..b696b1d38 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/api/v2/api-resources-with-priority.list @@ -0,0 +1,6 @@ +ca/uhn/fhir/hapi-fhir-base-build.properties +ca/uhn/fhir/i18n/hapi-messages.properties +dev/dsf/bpe/v2/plugin/ApiServicesSpringConfiguration.class +dev/dsf/bpe/v2/spring/ApiServiceConfig.class +META-INF/services/dev.dsf.bpe.api.plugin.ProcessPluginApiBuilder +org/hl7/fhir/r4/hapi/model/fhirversion.properties \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/subscription/TaskHandlerTest.java b/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/subscription/TaskHandlerTest.java index 6a81bf5f2..499023161 100644 --- a/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/subscription/TaskHandlerTest.java +++ b/dsf-bpe/dsf-bpe-server/src/test/java/dev/dsf/bpe/subscription/TaskHandlerTest.java @@ -5,6 +5,7 @@ import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import org.camunda.bpm.engine.RepositoryService; @@ -14,6 +15,7 @@ import org.camunda.bpm.engine.runtime.MessageCorrelationBuilder; import org.camunda.bpm.engine.runtime.ProcessInstance; import org.camunda.bpm.engine.runtime.ProcessInstanceQuery; +import org.camunda.bpm.engine.variable.Variables; import org.hl7.fhir.r4.model.ResourceType; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; @@ -24,11 +26,15 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; -import dev.dsf.bpe.v1.constants.BpmnExecutionVariables; -import dev.dsf.bpe.v1.constants.CodeSystems.BpmnMessage; -import dev.dsf.fhir.client.FhirWebserviceClient; +import ca.uhn.fhir.context.FhirContext; +import dev.dsf.bpe.api.Constants; +import dev.dsf.bpe.api.plugin.ProcessIdAndVersion; +import dev.dsf.bpe.api.plugin.ProcessPlugin; +import dev.dsf.bpe.client.FhirWebserviceClient; +import dev.dsf.bpe.plugin.ProcessPluginManager; @RunWith(MockitoJUnitRunner.class) public class TaskHandlerTest @@ -57,33 +63,52 @@ public class TaskHandlerTest @Mock private MessageCorrelationBuilder messageCorrelationBuilder; + @Mock + private ProcessPluginManager processPluginManager; + + @Mock + private ProcessPlugin processPlugin; + + @Spy + private FhirContext fhirContext = FhirContext.forR4(); + @InjectMocks private TaskHandler taskHandler; @Captor ArgumentCaptor taskAfterUpdate; + @Captor + ArgumentCaptor taskJson; + @Test public void testCreateBusinessKey() { // Mock preparations - Mockito.when(webserviceClient.update(Mockito.any(Task.class))).thenAnswer(i -> i.getArguments()[0]); - Mockito.when(repositoryService.createProcessDefinitionQuery()).thenReturn(processDefinitionQuery); Mockito.when(processDefinitionQuery.active()).thenReturn(processDefinitionQuery); - Mockito.when(processDefinitionQuery.processDefinitionKey(Mockito.anyString())) + Mockito.when(processDefinitionQuery.processDefinitionKey(Mockito.eq("dsfdev_foo"))) .thenReturn(processDefinitionQuery); - Mockito.when(processDefinitionQuery.versionTag(Mockito.anyString())).thenReturn(processDefinitionQuery); + Mockito.when(processDefinitionQuery.versionTag(Mockito.eq("0.1"))).thenReturn(processDefinitionQuery); Mockito.when(processDefinitionQuery.list()).thenReturn(List.of(processDefinition)); + + Mockito.when(processDefinition.getKey()).thenReturn("dsfdev_foo"); + Mockito.when(processDefinition.getVersionTag()).thenReturn("0.1"); + Mockito.when(processPluginManager.getProcessPlugin(Mockito.eq(new ProcessIdAndVersion("dsfdev_foo", "0.1")))) + .thenReturn(Optional.of(processPlugin)); + Mockito.when(processPlugin.createFhirTaskVariable(Mockito.anyString())) + .thenAnswer(i -> Variables.stringValue(i.getArgument(0))); + + Mockito.when(webserviceClient.update(Mockito.any(Task.class))).thenAnswer(i -> i.getArgument(0)); + Mockito.when(processDefinition.getId()).thenReturn(UUID.randomUUID().toString()); Mockito.when(runtimeService.createProcessInstanceQuery()).thenReturn(processInstanceQuery); Mockito.when(processInstanceQuery.processDefinitionId(Mockito.anyString())).thenReturn(processInstanceQuery); Mockito.when(processInstanceQuery.processInstanceBusinessKey(Mockito.anyString())) .thenReturn(processInstanceQuery); - Mockito.when(processInstanceQuery - .variableValueEquals(Mockito.eq(BpmnExecutionVariables.ALTERNATIVE_BUSINESS_KEY), Mockito.anyString())) - .thenReturn(processInstanceQuery); + Mockito.when(processInstanceQuery.variableValueEquals(Mockito.eq(Constants.ALTERNATIVE_BUSINESS_KEY), + Mockito.anyString())).thenReturn(processInstanceQuery); Mockito.when(processInstanceQuery.list()).thenReturn(List.of(processInstance)).thenReturn(List.of()); Mockito.when(runtimeService.createMessageCorrelation(Mockito.anyString())) @@ -99,8 +124,9 @@ public void testCreateBusinessKey() taskBeforeUpdate .getInput().stream().filter( Objects::nonNull) - .flatMap(i -> i.getType().getCoding().stream().filter(c -> BpmnMessage.URL.equals(c.getSystem()) - && BpmnMessage.Codes.BUSINESS_KEY.equals(c.getCode()))) + .flatMap(i -> i.getType().getCoding().stream() + .filter(c -> Constants.BPMN_MESSAGE_URL.equals(c.getSystem()) + && Constants.BPMN_MESSAGE_BUSINESS_KEY.equals(c.getCode()))) .count()); taskHandler.onResource(taskBeforeUpdate); @@ -110,8 +136,9 @@ public void testCreateBusinessKey() taskAfterUpdate .getValue().getInput().stream().filter( Objects::nonNull) - .flatMap(i -> i.getType().getCoding().stream().filter(c -> BpmnMessage.URL.equals(c.getSystem()) - && BpmnMessage.Codes.BUSINESS_KEY.equals(c.getCode()))) + .flatMap(i -> i.getType().getCoding().stream() + .filter(c -> Constants.BPMN_MESSAGE_URL.equals(c.getSystem()) + && Constants.BPMN_MESSAGE_BUSINESS_KEY.equals(c.getCode()))) .count()); } diff --git a/dsf-bpe/pom.xml b/dsf-bpe/pom.xml index 3095b4336..80503d3fd 100755 --- a/dsf-bpe/pom.xml +++ b/dsf-bpe/pom.xml @@ -11,7 +11,11 @@ + dsf-bpe-process-api dsf-bpe-process-api-v1 + dsf-bpe-process-api-v1-impl + dsf-bpe-process-api-v2 + dsf-bpe-process-api-v2-impl dsf-bpe-server dsf-bpe-server-jetty @@ -62,39 +66,33 @@ dev.dsf - dsf-bpe-process-api-v1 - ${project.version} - - - dev.dsf - dsf-fhir-websocket-client + dsf-bpe-process-api ${project.version} dev.dsf - dsf-fhir-webservice-client + dsf-bpe-process-api-v1 ${project.version} dev.dsf - dsf-fhir-server + dsf-bpe-process-api-v1-impl ${project.version} dev.dsf - dsf-fhir-validation + dsf-bpe-process-api-v2 ${project.version} dev.dsf - dsf-fhir-validation - tests - test-jar + dsf-bpe-process-api-v2-impl ${project.version} + dev.dsf - dsf-fhir-auth + dsf-fhir-websocket-client ${project.version} diff --git a/dsf-common/dsf-common-auth/pom.xml b/dsf-common/dsf-common-auth/pom.xml index 4b1af4b33..3333d99d0 100644 --- a/dsf-common/dsf-common-auth/pom.xml +++ b/dsf-common/dsf-common-auth/pom.xml @@ -13,6 +13,7 @@ ca.uhn.hapi.fhir hapi-fhir-structures-r4 + ${hapi.fhir.version} org.yaml diff --git a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/RoleConfig.java b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/RoleConfig.java index 0917a297e..19e74f06c 100644 --- a/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/RoleConfig.java +++ b/dsf-common/dsf-common-auth/src/main/java/dev/dsf/common/auth/conf/RoleConfig.java @@ -288,12 +288,12 @@ else if (mappingKey != null && mappingKey instanceof String @SuppressWarnings("unchecked") private static List getValues(Object o) { - if (o instanceof String s) - return Collections.singletonList(s); - else if (o instanceof List l) - return l; - else - return Collections.emptyList(); + return switch (o) + { + case String s -> Collections.singletonList(s); + case @SuppressWarnings("rawtypes") List l -> l; + default -> Collections.emptyList(); + }; } public List getEntries() diff --git a/dsf-common/dsf-common-ui/pom.xml b/dsf-common/dsf-common-ui/pom.xml index d6692f557..e03ba22e5 100644 --- a/dsf-common/dsf-common-ui/pom.xml +++ b/dsf-common/dsf-common-ui/pom.xml @@ -18,5 +18,9 @@ commons-codec commons-codec + + org.thymeleaf + thymeleaf + \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-auth/pom.xml b/dsf-fhir/dsf-fhir-auth/pom.xml index 9e78860ca..3171693f3 100644 --- a/dsf-fhir/dsf-fhir-auth/pom.xml +++ b/dsf-fhir/dsf-fhir-auth/pom.xml @@ -17,6 +17,7 @@ ca.uhn.hapi.fhir hapi-fhir-structures-r4 + ${hapi.fhir.version} diff --git a/dsf-fhir/dsf-fhir-auth/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelperImpl.java b/dsf-fhir/dsf-fhir-auth/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelperImpl.java index 3702b79fc..e598c0c11 100644 --- a/dsf-fhir/dsf-fhir-auth/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelperImpl.java +++ b/dsf-fhir/dsf-fhir-auth/src/main/java/dev/dsf/fhir/authorization/process/ProcessAuthorizationHelperImpl.java @@ -293,19 +293,18 @@ private boolean isRecipientValid(Extension recipient, Predicate orga private Optional recipientFrom(Coding coding, Predicate organizationWithIdentifierExists, Predicate organizationRoleExists) { - switch (coding.getCode()) + return switch (coding.getCode()) { - case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL: - return All.fromRecipient(coding); + case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ALL -> All.fromRecipient(coding); - case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION: - return Organization.fromRecipient(coding, organizationWithIdentifierExists); + case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ORGANIZATION -> + Organization.fromRecipient(coding, organizationWithIdentifierExists); - case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE: - return Role.fromRecipient(coding, organizationWithIdentifierExists, organizationRoleExists); - } + case ProcessAuthorizationHelper.PROCESS_AUTHORIZATION_VALUE_LOCAL_ROLE -> + Role.fromRecipient(coding, organizationWithIdentifierExists, organizationRoleExists); - return Optional.empty(); + default -> Optional.empty(); + }; } @Override diff --git a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml index 3208437f1..f2316424f 100755 --- a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml +++ b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml @@ -13,6 +13,7 @@ ca.uhn.hapi.fhir hapi-fhir-structures-r4 + ${hapi.fhir.version} jakarta.ws.rs diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/AbstractAdapter.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/AbstractAdapter.java deleted file mode 100644 index 61e99217d..000000000 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/AbstractAdapter.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.dsf.fhir.adapter; - -import java.util.Set; -import java.util.function.Supplier; - -import ca.uhn.fhir.parser.IParser; -import jakarta.ws.rs.core.MediaType; - -public abstract class AbstractAdapter -{ - public static final String PRETTY = "pretty"; - public static final String SUMMARY = "summary"; - - protected IParser getParser(MediaType mediaType, Supplier parserFactor) - { - /* Parsers are not guaranteed to be thread safe */ - IParser p = parserFactor.get(); - p.setStripVersionsFromReferences(false); - p.setOverrideResourceIdWithBundleEntryFullUrl(false); - - if (mediaType != null) - { - if ("true".equals(mediaType.getParameters().getOrDefault(PRETTY, "false"))) - p.setPrettyPrint(true); - - switch (mediaType.getParameters().getOrDefault(SUMMARY, "false")) - { - case "true" -> p.setSummaryMode(true); - case "text" -> p.setEncodeElements(Set.of("*.text", "*.id", "*.meta", "*.(mandatory)")); - case "data" -> p.setSuppressNarratives(true); - } - } - - return p; - } -} diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/FhirAdapter.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/FhirAdapter.java index e3c8faf62..ff4effbd3 100755 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/FhirAdapter.java +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/FhirAdapter.java @@ -7,15 +7,16 @@ import java.io.OutputStreamWriter; import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import java.util.Set; +import java.util.function.Supplier; import org.hl7.fhir.r4.model.BaseResource; -import org.hl7.fhir.r4.model.Binary; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.IdType; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.Constants; +import dev.dsf.fhir.service.ReferenceCleaner; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Produces; import jakarta.ws.rs.WebApplicationException; @@ -30,14 +31,41 @@ Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON }) @Produces({ Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML, Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON, MediaType.APPLICATION_JSON }) -public class FhirAdapter extends AbstractAdapter - implements MessageBodyReader, MessageBodyWriter +public class FhirAdapter implements MessageBodyReader, MessageBodyWriter { + public static final String PRETTY = "pretty"; + public static final String SUMMARY = "summary"; + private final FhirContext fhirContext; + private final ReferenceCleaner referenceCleaner; - public FhirAdapter(FhirContext fhirContext) + public FhirAdapter(FhirContext fhirContext, ReferenceCleaner referenceCleaner) { this.fhirContext = fhirContext; + this.referenceCleaner = referenceCleaner; + } + + private IParser getParser(MediaType mediaType, Supplier parserFactor) + { + /* Parsers are not guaranteed to be thread safe */ + IParser p = parserFactor.get(); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + + if (mediaType != null) + { + if ("true".equals(mediaType.getParameters().getOrDefault(PRETTY, "false"))) + p.setPrettyPrint(true); + + switch (mediaType.getParameters().getOrDefault(SUMMARY, "false")) + { + case "true" -> p.setSummaryMode(true); + case "text" -> p.setEncodeElements(Set.of("*.text", "*.id", "*.meta", "*.(mandatory)")); + case "data" -> p.setSuppressNarratives(true); + } + } + + return p; } private IParser getParser(MediaType mediaType) @@ -77,49 +105,11 @@ public BaseResource readFrom(Class type, Type genericType, Annotat MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { - return fixResource(getParser(mediaType).parseResource(type, new InputStreamReader(entityStream))); - } + BaseResource resource = getParser(mediaType).parseResource(type, new InputStreamReader(entityStream)); - private BaseResource fixResource(BaseResource resource) - { + // HAPI FHIR parser adds contained resources to bundle references if (resource instanceof Bundle b) - return fixBundle(b); - else if (resource instanceof Binary b) - return fixBinary(b); - else - return resource; - } - - private BaseResource fixBundle(Bundle resource) - { - if (resource.hasIdElement() && resource.getIdElement().hasIdPart() - && !resource.getIdElement().hasVersionIdPart() && resource.hasMeta() - && resource.getMeta().hasVersionId()) - { - // TODO Bugfix HAPI is removing version information from bundle.id - IdType fixedId = new IdType(resource.getResourceType().name(), resource.getIdElement().getIdPart(), - resource.getMeta().getVersionId()); - resource.setIdElement(fixedId); - } - - // TODO Bugfix HAPI is removing version information from bundle.id - resource.getEntry().stream().filter(e -> e.hasResource() && e.getResource() instanceof Bundle) - .map(e -> (Bundle) e.getResource()).forEach(this::fixResource); - - return resource; - } - - private BaseResource fixBinary(Binary resource) - { - if (resource.hasIdElement() && resource.getIdElement().hasIdPart() - && !resource.getIdElement().hasVersionIdPart() && resource.hasMeta() - && resource.getMeta().hasVersionId()) - { - // TODO Bugfix HAPI is removing version information from binary.id - IdType fixedId = new IdType(resource.getResourceType().name(), resource.getIdElement().getIdPart(), - resource.getMeta().getVersionId()); - resource.setIdElement(fixedId); - } + resource = referenceCleaner.cleanReferenceResourcesIfBundle(b); return resource; } diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractor.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractor.java index c7433b5d4..d0131084a 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractor.java +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractor.java @@ -2,90 +2,9 @@ import java.util.stream.Stream; -import org.hl7.fhir.r4.model.ActivityDefinition; -import org.hl7.fhir.r4.model.Binary; -import org.hl7.fhir.r4.model.CodeSystem; -import org.hl7.fhir.r4.model.DocumentReference; -import org.hl7.fhir.r4.model.Endpoint; -import org.hl7.fhir.r4.model.Group; -import org.hl7.fhir.r4.model.HealthcareService; -import org.hl7.fhir.r4.model.Library; -import org.hl7.fhir.r4.model.Location; -import org.hl7.fhir.r4.model.Measure; -import org.hl7.fhir.r4.model.MeasureReport; -import org.hl7.fhir.r4.model.NamingSystem; -import org.hl7.fhir.r4.model.OperationOutcome; -import org.hl7.fhir.r4.model.Organization; -import org.hl7.fhir.r4.model.OrganizationAffiliation; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Practitioner; -import org.hl7.fhir.r4.model.PractitionerRole; -import org.hl7.fhir.r4.model.Provenance; -import org.hl7.fhir.r4.model.Questionnaire; -import org.hl7.fhir.r4.model.QuestionnaireResponse; -import org.hl7.fhir.r4.model.ResearchStudy; import org.hl7.fhir.r4.model.Resource; -import org.hl7.fhir.r4.model.StructureDefinition; -import org.hl7.fhir.r4.model.Subscription; -import org.hl7.fhir.r4.model.Task; -import org.hl7.fhir.r4.model.ValueSet; public interface ReferenceExtractor { Stream getReferences(Resource resource); - - Stream getReferences(ActivityDefinition resource); - - Stream getReferences(Binary resource); - - // Not implemented yet, special rules apply for tmp ids - // Stream getReferences(Bundle resource); - - Stream getReferences(CodeSystem resource); - - Stream getReferences(DocumentReference resource); - - Stream getReferences(Endpoint resource); - - Stream getReferences(Group resource); - - Stream getReferences(HealthcareService resource); - - Stream getReferences(Library resource); - - Stream getReferences(Location resource); - - Stream getReferences(Measure resource); - - Stream getReferences(MeasureReport resource); - - Stream getReferences(NamingSystem resource); - - Stream getReferences(OperationOutcome resource); - - Stream getReferences(Organization resource); - - Stream getReferences(OrganizationAffiliation resource); - - Stream getReferences(Patient resource); - - Stream getReferences(Practitioner resource); - - Stream getReferences(PractitionerRole resource); - - Stream getReferences(Provenance resource); - - Stream getReferences(Questionnaire resource); - - Stream getReferences(QuestionnaireResponse resource); - - Stream getReferences(ResearchStudy resource); - - Stream getReferences(StructureDefinition resource); - - Stream getReferences(Subscription resource); - - Stream getReferences(Task resource); - - Stream getReferences(ValueSet resource); } \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java index 9af14321d..05f3e500d 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java +++ b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/service/ReferenceExtractorImpl.java @@ -327,83 +327,53 @@ else if (streams.length == 2) @Override public Stream getReferences(Resource resource) { - if (resource == null) - return Stream.empty(); - - if (resource instanceof ActivityDefinition ad) - return getReferences(ad); - // not implemented yet, special rules apply for tmp ids - // else if (resource instanceof Bundle b) - // return getReferences(b); - else if (resource instanceof Binary b) - return getReferences(b); - else if (resource instanceof CodeSystem cs) - return getReferences(cs); - else if (resource instanceof DocumentReference dr) - return getReferences(dr); - else if (resource instanceof Endpoint e) - return getReferences(e); - else if (resource instanceof Group g) - return getReferences(g); - else if (resource instanceof HealthcareService hs) - return getReferences(hs); - else if (resource instanceof Library l) - return getReferences(l); - else if (resource instanceof Location l) - return getReferences(l); - else if (resource instanceof Measure m) - return getReferences(m); - else if (resource instanceof MeasureReport mr) - return getReferences(mr); - else if (resource instanceof NamingSystem ns) - return getReferences(ns); - else if (resource instanceof OperationOutcome oo) - return getReferences(oo); - else if (resource instanceof Organization o) - return getReferences(o); - else if (resource instanceof OrganizationAffiliation oa) - return getReferences(oa); - else if (resource instanceof Patient p) - return getReferences(p); - else if (resource instanceof Practitioner p) - return getReferences(p); - else if (resource instanceof PractitionerRole pr) - return getReferences(pr); - else if (resource instanceof Provenance p) - return getReferences(p); - else if (resource instanceof Questionnaire q) - return getReferences(q); - else if (resource instanceof QuestionnaireResponse qr) - return getReferences(qr); - else if (resource instanceof ResearchStudy rs) - return getReferences(rs); - else if (resource instanceof StructureDefinition sd) - return getReferences(sd); - else if (resource instanceof Subscription s) - return getReferences(s); - else if (resource instanceof Task t) - return getReferences(t); - else if (resource instanceof ValueSet vs) - return getReferences(vs); - else if (resource instanceof DomainResource) - { - logger.debug("DomainResource of type {} not supported, returning extension references only", - resource.getClass().getName()); - return getExtensionReferences((DomainResource) resource); - } - else + return switch (resource) { - logger.debug("Resource of type {} not supported, returning no references", resource.getClass().getName()); - return Stream.empty(); - } - } - - @Override - public Stream getReferences(ActivityDefinition resource) + case null -> Stream.empty(); + + case ActivityDefinition ad -> getReferences(ad); + case Binary b -> getReferences(b); + case CodeSystem cs -> getReferences(cs); + case DocumentReference dr -> getReferences(dr); + case Endpoint e -> getReferences(e); + case Group g -> getReferences(g); + case HealthcareService hs -> getReferences(hs); + case Library l -> getReferences(l); + case Location l -> getReferences(l); + case Measure m -> getReferences(m); + case MeasureReport mr -> getReferences(mr); + case NamingSystem ns -> getReferences(ns); + case OperationOutcome oo -> getReferences(oo); + case Organization o -> getReferences(o); + case OrganizationAffiliation oa -> getReferences(oa); + case Patient p -> getReferences(p); + case Practitioner p -> getReferences(p); + case PractitionerRole pr -> getReferences(pr); + case Provenance p -> getReferences(p); + case Questionnaire q -> getReferences(q); + case QuestionnaireResponse qr -> getReferences(qr); + case ResearchStudy rs -> getReferences(rs); + case StructureDefinition sd -> getReferences(sd); + case Subscription s -> getReferences(s); + case Task t -> getReferences(t); + case ValueSet vs -> getReferences(vs); + + case DomainResource dr -> { + logger.debug("DomainResource of type {} not supported, returning extension references only", + dr.getClass().getName()); + yield getExtensionReferences(dr); + } + + default -> { + logger.debug("Resource of type {} not supported, returning no references", + resource.getClass().getName()); + yield Stream.empty(); + } + }; + } + + private Stream getReferences(ActivityDefinition resource) { - if (resource == null) - return Stream.empty(); - var subjectReference = getReference(resource, ActivityDefinition::hasSubjectReference, ActivityDefinition::getSubjectReference, "ActivityDefinition.subjectReference", Group.class); var location = getReference(resource, ActivityDefinition::hasLocation, ActivityDefinition::getLocation, @@ -429,35 +399,23 @@ public Stream getReferences(ActivityDefinition resource) observationResultRequirement, relatedArtifacts, extensionReferences); } - @Override - public Stream getReferences(Binary resource) + private Stream getReferences(Binary resource) { - if (resource == null) - return Stream.empty(); - var securityContext = getReference(resource, Binary::hasSecurityContext, Binary::getSecurityContext, "Binary.securityContext"); return securityContext; } - @Override - public Stream getReferences(CodeSystem resource) + private Stream getReferences(CodeSystem resource) { - if (resource == null) - return Stream.empty(); - var extensionReferences = getExtensionReferences(resource); return extensionReferences; } - @Override - public Stream getReferences(DocumentReference resource) + private Stream getReferences(DocumentReference resource) { - if (resource == null) - return null; - var subject = getReference(resource, DocumentReference::hasSubject, DocumentReference::getSubject, "DocumentReference.subject", Patient.class, Practitioner.class, Group.class, Device.class); var author = getReferences(resource, DocumentReference::hasAuthor, DocumentReference::getAuthor, @@ -493,12 +451,8 @@ public Stream getReferences(DocumentReference resource) contextSourcePatientInfo, contextRelated, contentAttachment, extensionReferences); } - @Override - public Stream getReferences(Endpoint resource) + private Stream getReferences(Endpoint resource) { - if (resource == null) - return Stream.empty(); - var managingOrganization = getReference(resource, Endpoint::hasManagingOrganization, Endpoint::getManagingOrganization, "Endpoint.managingOrganization", Organization.class); @@ -507,12 +461,8 @@ public Stream getReferences(Endpoint resource) return concat(managingOrganization, extensionReferences); } - @Override - public Stream getReferences(Group resource) + private Stream getReferences(Group resource) { - if (resource == null) - return Stream.empty(); - var managingEntity = getReference(resource, Group::hasManagingEntity, Group::getManagingEntity, "Group.managingEntity", Organization.class, RelatedPerson.class, Practitioner.class, PractitionerRole.class); @@ -527,12 +477,8 @@ public Stream getReferences(Group resource) return concat(managingEntity, memberEntities, extensionReferences); } - @Override - public Stream getReferences(HealthcareService resource) + private Stream getReferences(HealthcareService resource) { - if (resource == null) - return Stream.empty(); - var providedBy = getReference(resource, HealthcareService::hasProvidedBy, HealthcareService::getProvidedBy, "HealthcareService.providedBy", Organization.class); var locations = getReferences(resource, HealthcareService::hasLocation, HealthcareService::getLocation, @@ -547,12 +493,8 @@ public Stream getReferences(HealthcareService resource) return concat(providedBy, locations, coverageAreas, endpoints, extensionReferences); } - @Override - public Stream getReferences(Library resource) + private Stream getReferences(Library resource) { - if (resource == null) - return Stream.empty(); - var subject = getReference(resource, Library::hasSubjectReference, Library::getSubjectReference, "Library.subject", Group.class); var relatedArtifact = getRelatedArtifacts(resource, Library::hasRelatedArtifact, Library::getRelatedArtifact, @@ -564,12 +506,8 @@ public Stream getReferences(Library resource) return concat(subject, relatedArtifact, content, extensionReferences); } - @Override - public Stream getReferences(Location resource) + private Stream getReferences(Location resource) { - if (resource == null) - return Stream.empty(); - var managingOrganization = getReference(resource, Location::hasManagingOrganization, Location::getManagingOrganization, "Location.managingOrganization", Organization.class); var partOf = getReference(resource, Location::hasPartOf, Location::getPartOf, "Location.partOf", @@ -582,12 +520,8 @@ public Stream getReferences(Location resource) return concat(managingOrganization, partOf, endpoints, extensionReferences); } - @Override - public Stream getReferences(Measure resource) + private Stream getReferences(Measure resource) { - if (resource == null) - return Stream.empty(); - var subject = getReference(resource, Measure::hasSubjectReference, Measure::getSubjectReference, "Measure.subject", Group.class); var relatedArtifacts = getRelatedArtifacts(resource, Measure::hasRelatedArtifact, Measure::getRelatedArtifact, @@ -598,12 +532,8 @@ public Stream getReferences(Measure resource) return concat(subject, relatedArtifacts, extensionReferences); } - @Override - public Stream getReferences(MeasureReport resource) + private Stream getReferences(MeasureReport resource) { - if (resource == null) - return Stream.empty(); - var subject = getReference(resource, MeasureReport::hasSubject, MeasureReport::getSubject, "MeasureReport.subject", Patient.class, Practitioner.class, PractitionerRole.class, Location.class, Device.class, RelatedPerson.class, Group.class); @@ -630,32 +560,20 @@ public Stream getReferences(MeasureReport resource) return concat(subject, reporter, subjectResults1, subjectResults2, evaluatedResource, extensionReferences); } - @Override - public Stream getReferences(NamingSystem resource) + private Stream getReferences(NamingSystem resource) { - if (resource == null) - return Stream.empty(); - var extensionReferences = getExtensionReferences(resource); return extensionReferences; } - @Override - public Stream getReferences(OperationOutcome resource) + private Stream getReferences(OperationOutcome resource) { - if (resource == null) - return Stream.empty(); - return getExtensionReferences(resource); } - @Override - public Stream getReferences(Organization resource) + private Stream getReferences(Organization resource) { - if (resource == null) - return Stream.empty(); - var partOf = getReference(resource, Organization::hasPartOf, Organization::getPartOf, "Organization.partOf", Organization.class); var endpoints = getReferences(resource, Organization::hasEndpoint, Organization::getEndpoint, @@ -666,12 +584,8 @@ public Stream getReferences(Organization resource) return concat(partOf, endpoints, extensionReferences); } - @Override - public Stream getReferences(OrganizationAffiliation resource) + private Stream getReferences(OrganizationAffiliation resource) { - if (resource == null) - return Stream.empty(); - var organization = getReference(resource, OrganizationAffiliation::hasOrganization, OrganizationAffiliation::getOrganization, "OrganizationAffiliation.organization", Organization.class); var participatingOrganization = getReference(resource, OrganizationAffiliation::hasParticipatingOrganization, @@ -693,12 +607,8 @@ public Stream getReferences(OrganizationAffiliation resource) extensionReferences); } - @Override - public Stream getReferences(Patient resource) + private Stream getReferences(Patient resource) { - if (resource == null) - return Stream.empty(); - var contactsOrganization = getBackboneElementsReference(resource, Patient::hasContact, Patient::getContact, ContactComponent::hasOrganization, ContactComponent::getOrganization, "Patient.contact.organization", Organization.class); @@ -717,12 +627,8 @@ public Stream getReferences(Patient resource) extensionReferences); } - @Override - public Stream getReferences(Practitioner resource) + private Stream getReferences(Practitioner resource) { - if (resource == null) - return Stream.empty(); - var qualificationsIssuer = getBackboneElementsReference(resource, Practitioner::hasQualification, Practitioner::getQualification, PractitionerQualificationComponent::hasIssuer, PractitionerQualificationComponent::getIssuer, "Practitioner.qualification.issuer", Organization.class); @@ -732,12 +638,8 @@ public Stream getReferences(Practitioner resource) return concat(qualificationsIssuer, extensionReferences); } - @Override - public Stream getReferences(PractitionerRole resource) + private Stream getReferences(PractitionerRole resource) { - if (resource == null) - return Stream.empty(); - var practitioner = getReference(resource, PractitionerRole::hasPractitioner, PractitionerRole::getPractitioner, "PractitionerRole.practitioner", Practitioner.class); var organization = getReference(resource, PractitionerRole::hasOrganization, PractitionerRole::getOrganization, @@ -754,12 +656,8 @@ public Stream getReferences(PractitionerRole resource) return concat(practitioner, organization, locations, healthcareServices, endpoints, extensionReferences); } - @Override - public Stream getReferences(Provenance resource) + private Stream getReferences(Provenance resource) { - if (resource == null) - return Stream.empty(); - var targets = getReferences(resource, Provenance::hasTarget, Provenance::getTarget, "Provenance.target"); var location = getReference(resource, Provenance::hasLocation, Provenance::getLocation, "Provenance.location", Location.class); @@ -779,12 +677,8 @@ public Stream getReferences(Provenance resource) return concat(targets, location, agentsWho, agentsOnBehalfOf, entitiesWhat, extensionReferences); } - @Override - public Stream getReferences(Questionnaire resource) + private Stream getReferences(Questionnaire resource) { - if (resource == null) - return Stream.empty(); - var enableWhen = getBackboneElements2Reference(resource, Questionnaire::hasItem, Questionnaire::getItem, Questionnaire.QuestionnaireItemComponent::hasEnableWhen, Questionnaire.QuestionnaireItemComponent::getEnableWhen, @@ -811,12 +705,8 @@ public Stream getReferences(Questionnaire resource) return concat(enableWhen, answerOption, initial, extensionReferences); } - @Override - public Stream getReferences(QuestionnaireResponse resource) + private Stream getReferences(QuestionnaireResponse resource) { - if (resource == null) - return Stream.empty(); - var author = getReference(resource, QuestionnaireResponse::hasAuthor, QuestionnaireResponse::getAuthor, "QuestionnaireResponse.author", Device.class, Organization.class, Patient.class, Practitioner.class, PractitionerRole.class, RelatedPerson.class); @@ -842,12 +732,8 @@ public Stream getReferences(QuestionnaireResponse resource) return concat(author, basedOn, encounter, partOf, source, subject, extensionReferences); } - @Override - public Stream getReferences(ResearchStudy resource) + private Stream getReferences(ResearchStudy resource) { - if (resource == null) - return Stream.empty(); - var protocols = getReferences(resource, ResearchStudy::hasProtocol, ResearchStudy::getProtocol, "ResearchStudy.protocol", PlanDefinition.class); var partOfs = getReferences(resource, ResearchStudy::hasPartOf, ResearchStudy::getPartOf, @@ -870,34 +756,22 @@ public Stream getReferences(ResearchStudy resource) extensionReferences); } - @Override - public Stream getReferences(StructureDefinition resource) + private Stream getReferences(StructureDefinition resource) { - if (resource == null) - return Stream.empty(); - var extensionReferences = getExtensionReferences(resource); return extensionReferences; } - @Override - public Stream getReferences(Subscription resource) + private Stream getReferences(Subscription resource) { - if (resource == null) - return Stream.empty(); - var extensionReferences = getExtensionReferences(resource); return extensionReferences; } - @Override - public Stream getReferences(Task resource) + private Stream getReferences(Task resource) { - if (resource == null) - return Stream.empty(); - var basedOns = getReferences(resource, Task::hasBasedOn, Task::getBasedOn, "Task.basedOn"); var partOfs = getReferences(resource, Task::hasPartOf, Task::getPartOf, "Task.partOf", Task.class); var focus = getReference(resource, Task::hasFocus, Task::getFocus, "Task.focus"); @@ -960,12 +834,8 @@ private Stream getOutputReferences(Task resource) return Stream.concat(outputReferences, outputExtensionReferences); } - @Override - public Stream getReferences(ValueSet resource) + private Stream getReferences(ValueSet resource) { - if (resource == null) - return Stream.empty(); - var extensionReferences = getExtensionReferences(resource); return extensionReferences; diff --git a/dsf-fhir/dsf-fhir-server-jetty/conf/log4j2.xml b/dsf-fhir/dsf-fhir-server-jetty/conf/log4j2.xml index 20ab24b03..7bc2f73d0 100755 --- a/dsf-fhir/dsf-fhir-server-jetty/conf/log4j2.xml +++ b/dsf-fhir/dsf-fhir-server-jetty/conf/log4j2.xml @@ -17,6 +17,7 @@ + diff --git a/dsf-fhir/dsf-fhir-server-jetty/docker/conf/log4j2.xml b/dsf-fhir/dsf-fhir-server-jetty/docker/conf/log4j2.xml index ddc27f007..88c646847 100644 --- a/dsf-fhir/dsf-fhir-server-jetty/docker/conf/log4j2.xml +++ b/dsf-fhir/dsf-fhir-server-jetty/docker/conf/log4j2.xml @@ -30,6 +30,7 @@ + diff --git a/dsf-fhir/dsf-fhir-server/pom.xml b/dsf-fhir/dsf-fhir-server/pom.xml index 1f2588523..9b1276930 100644 --- a/dsf-fhir/dsf-fhir-server/pom.xml +++ b/dsf-fhir/dsf-fhir-server/pom.xml @@ -123,10 +123,12 @@ ca.uhn.hapi.fhir hapi-fhir-structures-r4 + ${hapi.fhir.version} ca.uhn.hapi.fhir hapi-fhir-structures-r5 + ${hapi.fhir.version} ca.uhn.hapi.fhir @@ -137,14 +139,17 @@ commons-logging + ${hapi.fhir.version} ca.uhn.hapi.fhir hapi-fhir-validation-resources-r4 + ${hapi.fhir.version} ca.uhn.hapi.fhir hapi-fhir-validation-resources-r5 + ${hapi.fhir.version} @@ -170,6 +175,11 @@ com.sun.mail jakarta.mail + + + de.hs-heilbronn.mi + crypto-utils + dev.dsf diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceQuestionnaireResponse.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceQuestionnaireResponse.java index 9cea0cae9..2458e4ea8 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceQuestionnaireResponse.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceQuestionnaireResponse.java @@ -102,44 +102,47 @@ private Item toItem(boolean show, String id, String label, Type typedValue) { String fhirType = typedValue.getClass().getAnnotation(DatatypeDef.class).name(); - // TODO use switch expression with pattern matching after switching to java 21 - if (typedValue instanceof BooleanType b) - return new Item(show, id, "boolean", label, fhirType, null, null, b.hasValue() ? b.getValue() : null); - else if (typedValue instanceof DecimalType d) - return new Item(show, id, "number", label, fhirType, d.hasValue() ? String.valueOf(d.getValue()) : null, - null, null); - else if (typedValue instanceof IntegerType i) - return new Item(show, id, "number", label, fhirType, i.hasValue() ? String.valueOf(i.getValue()) : null, - null, null); - else if (typedValue instanceof DateType d) - return new Item(show, id, "date", label, fhirType, d.hasValue() ? format(d.getValue(), DATE_FORMAT) : null, - null, null); - else if (typedValue instanceof DateTimeType dt) - return new Item(show, id, "datetime-local", label, fhirType, - dt.hasValue() ? format(dt.getValue(), DATE_TIME_FORMAT) : null, null, null); - else if (typedValue instanceof TimeType t) - return new Item(show, id, "time", label, fhirType, t.hasValue() ? t.getValue() : null, null, null); - else if (typedValue instanceof StringType s) - return new Item(show, id, "text", label, fhirType, s.hasValue() ? s.getValue() : null, null, null); - else if (typedValue instanceof UriType u) - return new Item(show, id, "url", label, fhirType, u.hasValue() ? u.getValue() : null, null, null); - // else if (typedValue instanceof Attachment a) - // return TODO - else if (typedValue instanceof Coding c) - return new Item(show, id, "coding", label, fhirType, null, ElementSystemValue.from(c), null); - // else if(typedValue instanceof Quantity q) - // return TODO - else if (typedValue instanceof Reference r) + return switch (typedValue) { - if (r.hasReferenceElement()) - return new Item(show, id, "url", label, fhirType + ".reference", - r.getReferenceElement().hasValue() ? r.getReferenceElement().getValue() : null, null, null); - else if (r.hasIdentifier()) - return new Item(show, id, "identifier", label, fhirType + ".identifier", null, - ElementSystemValue.from(r.getIdentifier()), null); - } - - logger.warn("Element of type {}, not supported", fhirType); - return null; + case BooleanType b -> + new Item(show, id, "boolean", label, fhirType, null, null, b.hasValue() ? b.getValue() : null); + + case DecimalType d -> new Item(show, id, "number", label, fhirType, + d.hasValue() ? String.valueOf(d.getValue()) : null, null, null); + + case IntegerType i -> new Item(show, id, "number", label, fhirType, + i.hasValue() ? String.valueOf(i.getValue()) : null, null, null); + + case DateType d -> new Item(show, id, "date", label, fhirType, + d.hasValue() ? format(d.getValue(), DATE_FORMAT) : null, null, null); + + case DateTimeType dt -> new Item(show, id, "datetime-local", label, fhirType, + dt.hasValue() ? format(dt.getValue(), DATE_TIME_FORMAT) : null, null, null); + + case TimeType t -> + new Item(show, id, "time", label, fhirType, t.hasValue() ? t.getValue() : null, null, null); + + case StringType s -> + new Item(show, id, "text", label, fhirType, s.hasValue() ? s.getValue() : null, null, null); + + case UriType u -> + new Item(show, id, "url", label, fhirType, u.hasValue() ? u.getValue() : null, null, null); + + case Coding c -> new Item(show, id, "coding", label, fhirType, null, ElementSystemValue.from(c), null); + + case Reference r when r.hasReferenceElement() -> new Item(show, id, "url", label, fhirType + ".reference", + r.getReferenceElement().hasValue() ? r.getReferenceElement().getValue() : null, null, null); + + case Reference r when r.hasIdentifier() -> new Item(show, id, "identifier", label, fhirType + ".identifier", + null, ElementSystemValue.from(r.getIdentifier()), null); + + // TODO case Attachment a -> + // TODO case Quantity q -> + + default -> { + logger.warn("Element of type {}, not supported", fhirType); + yield null; + } + }; } } \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceTask.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceTask.java index dbdf7cb8f..ce5be2b7a 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceTask.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/ResourceTask.java @@ -219,93 +219,77 @@ private OutputItem toOutputItem(String label, String labelTitle, Type typedValue private String getHtmlInputType(Type typedValue) { - // TODO use switch expression with pattern matching after switching to java 21 - if (typedValue instanceof BooleanType) - return "boolean"; - else if (typedValue instanceof DecimalType) - return "number"; - else if (typedValue instanceof IntegerType) - return "number"; - else if (typedValue instanceof DateType) - return "date"; - else if (typedValue instanceof DateTimeType) - return "datetime-local"; - else if (typedValue instanceof TimeType) - return "time"; - else if (typedValue instanceof InstantType) - return "datetime-local"; - else if (typedValue instanceof StringType) - return "text"; - else if (typedValue instanceof UriType) - return "url"; - else if (typedValue instanceof Coding) - return "coding"; - else if (typedValue instanceof Identifier) - return "identifier"; - else if (typedValue instanceof Reference r && r.hasReferenceElement()) - return "url"; - else if (typedValue instanceof Reference r && r.hasIdentifier()) - return "identifier"; - else - return null; + return switch (typedValue) + { + case BooleanType b -> "boolean"; + case DecimalType d -> "number"; + case IntegerType i -> "number"; + case DateType d -> "date"; + case DateTimeType dt -> "datetime-local"; + case TimeType t -> "time"; + case InstantType i -> "datetime-local"; + case StringType s -> "text"; + case UriType u -> "url"; + case Coding c -> "coding"; + case Identifier i -> "identifier"; + case Reference r when r.hasReferenceElement() -> "url"; + case Reference r when r.hasIdentifier() -> "identifier"; + + default -> null; + }; } private String getFhirType(Type typedValue) { String type = typedValue.getClass().getAnnotation(DatatypeDef.class).name(); - // TODO use switch expression with pattern matching after switching to java 21 - if (typedValue instanceof Reference r && r.hasReferenceElement()) - return type + ".reference"; - else if (typedValue instanceof Reference r && r.hasIdentifier()) - return type + ".identifier"; - else - return type; + return switch (typedValue) + { + case Reference r when r.hasReferenceElement() -> type + ".reference"; + case Reference r when r.hasIdentifier() -> type + ".identifier"; + + default -> type; + }; } private String getStringValue(Type typedValue) { - // TODO use switch expression with pattern matching after switching to java 21 - if (typedValue instanceof DecimalType d) - return d.hasValue() ? String.valueOf(d.getValue()) : null; - else if (typedValue instanceof IntegerType i) - return i.hasValue() ? String.valueOf(i.getValue()) : null; - else if (typedValue instanceof DateType d) - return d.hasValue() ? format(d.getValue(), DATE_FORMAT) : null; - else if (typedValue instanceof DateTimeType dt) + return switch (typedValue) + { + case DecimalType d when d.hasValue() -> String.valueOf(d.getValue()); + case IntegerType i when i.hasValue() -> String.valueOf(i.getValue()); + case DateType d when d.hasValue() -> format(d.getValue(), DATE_FORMAT); + // TODO format datetime based on precision - return dt.hasValue() ? format(dt.getValue(), DATE_TIME_FORMAT) : null; - else if (typedValue instanceof TimeType t) - return t.hasValue() ? t.getValue() : null; - else if (typedValue instanceof InstantType i) - return i.hasValue() ? format(i.getValue(), DATE_TIME_FORMAT) : null; - else if (typedValue instanceof StringType s) - return s.hasValue() ? s.getValue() : null; - else if (typedValue instanceof UriType u) - return u.hasValue() ? u.getValue() : null; - else if (typedValue instanceof Reference r && r.hasReferenceElement()) - return r.getReferenceElement().hasValue() ? r.getReferenceElement().getValue() : null; - else - return null; + case DateTimeType dt when dt.hasValue() -> format(dt.getValue(), DATE_TIME_FORMAT); + + case TimeType t when t.hasValue() -> t.getValue(); + case InstantType i when i.hasValue() -> format(i.getValue(), DATE_TIME_FORMAT); + case StringType s when s.hasValue() -> s.getValue(); + case UriType u when u.hasValue() -> u.getValue(); + case Reference r when r.hasReferenceElement() && r.getReferenceElement().hasValue() -> + r.getReferenceElement().getValue(); + + default -> null; + }; } private ElementSystemValue getSystemValueValue(Type typedValue) { - // TODO use switch expression with pattern matching after switching to java 21 - if (typedValue instanceof Coding c) - return ElementSystemValue.from(c); - else if (typedValue instanceof Identifier i) - return ElementSystemValue.from(i); - else if (typedValue instanceof Reference r && r.hasIdentifier()) - return ElementSystemValue.from(r.getIdentifier()); - else - return null; + return switch (typedValue) + { + case Coding c -> ElementSystemValue.from(c); + case Identifier i -> ElementSystemValue.from(i); + case Reference r when r.hasIdentifier() -> ElementSystemValue.from(r.getIdentifier()); + + default -> null; + }; } private Boolean getBooleanValue(Type typedValue) { - if (typedValue instanceof BooleanType b) - return b.hasValue() ? b.getValue() : null; + if (typedValue instanceof BooleanType b && b.hasValue()) + return b.getValue(); else return null; } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractPreparedStatementFactory.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractPreparedStatementFactory.java index 100f1bbc6..4b983a71a 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractPreparedStatementFactory.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/AbstractPreparedStatementFactory.java @@ -4,8 +4,6 @@ import java.util.Objects; import java.util.UUID; -import org.hl7.fhir.r4.model.DomainResource; -import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Resource; import org.postgresql.util.PGobject; @@ -46,17 +44,7 @@ public IParser getJsonParser() protected final R jsonToResource(String json) { - R resource = getJsonParser().parseResource(resourceType, json); - - // TODO Bugfix HAPI is not setting version information from bundle.id while parsing non DomainResource - if (!(resource instanceof DomainResource)) - { - IdType fixedId = new IdType(resource.getResourceType().name(), resource.getIdElement().getIdPart(), - resource.getMeta().getVersionId()); - resource.setIdElement(fixedId); - } - - return resource; + return getJsonParser().parseResource(resourceType, json); } @Override diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BinaryDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BinaryDaoJdbc.java index bf582858c..b08dff933 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BinaryDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BinaryDaoJdbc.java @@ -9,7 +9,6 @@ import javax.sql.DataSource; import org.hl7.fhir.r4.model.Binary; -import org.hl7.fhir.r4.model.IdType; import org.postgresql.util.PGobject; import ca.uhn.fhir.context.FhirContext; @@ -34,17 +33,6 @@ protected Binary copy(Binary resource) return resource.copy(); } - @Override - protected Binary getResource(ResultSet result, int index) throws SQLException - { - // TODO Bugfix HAPI is removing version information from binary.id - Binary binary = super.getResource(result, index); - IdType fixedId = new IdType(binary.getResourceType().name(), binary.getIdElement().getIdPart(), - binary.getMeta().getVersionId()); - binary.setIdElement(fixedId); - return binary; - } - @Override protected void modifySearchResultResource(Binary resource, Connection connection) throws SQLException { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BundleDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BundleDaoJdbc.java index c81524b29..c1581a72a 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BundleDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/BundleDaoJdbc.java @@ -1,13 +1,10 @@ package dev.dsf.fhir.dao.jdbc; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.List; import javax.sql.DataSource; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.IdType; import ca.uhn.fhir.context.FhirContext; import dev.dsf.fhir.dao.BundleDao; @@ -29,15 +26,4 @@ protected Bundle copy(Bundle resource) { return resource.copy(); } - - @Override - protected Bundle getResource(ResultSet result, int index) throws SQLException - { - // TODO Bugfix HAPI is removing version information from bundle.id - Bundle bundle = super.getResource(result, index); - IdType fixedId = new IdType(bundle.getResourceType().name(), bundle.getIdElement().getIdPart(), - bundle.getMeta().getVersionId()); - bundle.setIdElement(fixedId); - return bundle; - } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HistroyDaoJdbc.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HistroyDaoJdbc.java index cb89536d6..b4be34a97 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HistroyDaoJdbc.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/dao/jdbc/HistroyDaoJdbc.java @@ -16,8 +16,6 @@ import javax.sql.DataSource; import org.hl7.fhir.r4.model.Binary; -import org.hl7.fhir.r4.model.DomainResource; -import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Resource; import org.postgresql.util.PGobject; import org.springframework.beans.factory.InitializingBean; @@ -184,21 +182,10 @@ private Resource jsonToResource(String json, Class resourceT if (json == null) return null; - Resource resource; if (resourceType != null) - resource = getJsonParser().parseResource(resourceType, json); + return getJsonParser().parseResource(resourceType, json); else - resource = (Resource) getJsonParser().parseResource(json); - - // TODO Bugfix HAPI is not setting version information from bundle.id while parsing non DomainResource - if (!(resource instanceof DomainResource)) - { - IdType fixedId = new IdType(resource.getResourceType().name(), resource.getIdElement().getIdPart(), - resource.getMeta().getVersionId()); - resource.setIdElement(fixedId); - } - - return resource; + return (Resource) getJsonParser().parseResource(json); } private String createCountSql(boolean forId, boolean forResource, List filter, diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java index b0abdc92b..907e9e2d0 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/help/ParameterConverter.java @@ -16,7 +16,7 @@ import org.slf4j.LoggerFactory; import ca.uhn.fhir.rest.api.Constants; -import dev.dsf.fhir.adapter.AbstractAdapter; +import dev.dsf.fhir.adapter.FhirAdapter; import dev.dsf.fhir.prefer.PreferHandlingType; import dev.dsf.fhir.prefer.PreferReturnType; import jakarta.ws.rs.WebApplicationException; @@ -145,9 +145,9 @@ private MediaType mediaType(String type, String subtype, boolean pretty, Summary { Map parameters = new HashMap<>(); if (pretty) - parameters.put(AbstractAdapter.PRETTY, "true"); + parameters.put(FhirAdapter.PRETTY, "true"); if (summaryMode != null) - parameters.put(AbstractAdapter.SUMMARY, summaryMode.toString()); + parameters.put(FhirAdapter.SUMMARY, summaryMode.toString()); return new MediaType(type, subtype, parameters); } diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java index 9464eca57..0e06d6d56 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/QuestionnaireResponseSubject.java @@ -162,20 +162,16 @@ protected boolean resourceMatches(QuestionnaireResponse resource) { if (ReferenceSearchType.IDENTIFIER.equals(valueAndType.type)) { - if (resource.getSubject().getResource() instanceof Organization o) - return o.getIdentifier().stream() + return switch (resource.getSubject().getResource()) + { + case Organization o -> o.getIdentifier().stream() .anyMatch(AbstractIdentifierParameter.identifierMatches(valueAndType.identifier)); - - else if (resource.getSubject().getResource() instanceof Practitioner p) - return p.getIdentifier().stream() + case Practitioner p -> p.getIdentifier().stream() .anyMatch(AbstractIdentifierParameter.identifierMatches(valueAndType.identifier)); - - else if (resource.getSubject().getResource() instanceof PractitionerRole p) - return p.getIdentifier().stream() + case PractitionerRole r -> r.getIdentifier().stream() .anyMatch(AbstractIdentifierParameter.identifierMatches(valueAndType.identifier)); - - else - return false; + default -> false; + }; } else { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyPrincipalInvestigator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyPrincipalInvestigator.java index fe48e3065..c29fac46e 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyPrincipalInvestigator.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/ResearchStudyPrincipalInvestigator.java @@ -153,16 +153,14 @@ protected boolean resourceMatches(ResearchStudy resource) { if (ReferenceSearchType.IDENTIFIER.equals(valueAndType.type)) { - if (resource.getPrincipalInvestigator().getResource() instanceof Practitioner p) - return p.getIdentifier().stream() + return switch (resource.getPrincipalInvestigator().getResource()) + { + case Practitioner p -> p.getIdentifier().stream() .anyMatch(AbstractIdentifierParameter.identifierMatches(valueAndType.identifier)); - - else if (resource.getPrincipalInvestigator().getResource() instanceof PractitionerRole p) - return p.getIdentifier().stream() + case PractitionerRole r -> r.getIdentifier().stream() .anyMatch(AbstractIdentifierParameter.identifierMatches(valueAndType.identifier)); - - else - return false; + default -> false; + }; } else { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskRequester.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskRequester.java index e63ffa2f4..7cef81433 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskRequester.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/parameters/TaskRequester.java @@ -166,24 +166,18 @@ protected boolean resourceMatches(Task resource) { if (ReferenceSearchType.IDENTIFIER.equals(valueAndType.type)) { - if (resource.getRequester().getResource() instanceof Practitioner p) - return p.getIdentifier().stream() + return switch (resource.getRequester().getResource()) + { + case Practitioner p -> p.getIdentifier().stream() .anyMatch(AbstractIdentifierParameter.identifierMatches(valueAndType.identifier)); - - else if (resource.getRequester().getResource() instanceof Organization o) - return o.getIdentifier().stream() + case Organization o -> o.getIdentifier().stream() .anyMatch(AbstractIdentifierParameter.identifierMatches(valueAndType.identifier)); - - else if (resource.getRequester().getResource() instanceof Patient p) - return p.getIdentifier().stream() + case Patient p -> p.getIdentifier().stream() .anyMatch(AbstractIdentifierParameter.identifierMatches(valueAndType.identifier)); - - else if (resource.getRequester().getResource() instanceof PractitionerRole p) - return p.getIdentifier().stream() + case PractitionerRole r -> r.getIdentifier().stream() .anyMatch(AbstractIdentifierParameter.identifierMatches(valueAndType.identifier)); - - else - return false; + default -> false; + }; } else { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ValidationSupportWithCache.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ValidationSupportWithCache.java index d8a42acb3..f373cc2d2 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ValidationSupportWithCache.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/service/ValidationSupportWithCache.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.context.support.LookupCodeRequest; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import dev.dsf.fhir.event.Event; @@ -353,6 +354,7 @@ public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theR theValueSet); } + @Deprecated @Override public LookupCodeResult lookupCode(ValidationSupportContext theRootValidationSupport, String theSystem, String theCode) @@ -360,6 +362,13 @@ public LookupCodeResult lookupCode(ValidationSupportContext theRootValidationSup return delegate.lookupCode(theRootValidationSupport, theSystem, theCode); } + @Override + public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, + LookupCodeRequest theLookupCodeRequest) + { + return delegate.lookupCode(theValidationSupportContext, theLookupCodeRequest); + } + @Override public IBaseResource generateSnapshot(ValidationSupportContext theRootValidationSupport, IBaseResource theInput, String theUrl, String theWebUrl, String theProfileName) diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java index 88fa573ae..a451a05ad 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java @@ -54,10 +54,13 @@ public class AdapterConfig @Autowired private PropertiesConfig propertiesConfig; + @Autowired + private ReferenceConfig referenceConfig; + @Bean public FhirAdapter fhirAdapter() { - return new FhirAdapter(fhirConfig.fhirContext()); + return new FhirAdapter(fhirConfig.fhirContext(), referenceConfig.referenceCleaner()); } @Bean diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/FhirConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/FhirConfig.java index 96ffbfeb4..0e3f801bd 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/FhirConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/FhirConfig.java @@ -5,8 +5,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.fasterxml.jackson.core.StreamReadConstraints; - import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.HapiLocalizer; @@ -16,10 +14,6 @@ public class FhirConfig @Bean public FhirContext fhirContext() { - // TODO remove workaround after upgrading to HAPI 6.8+, see https://github.com/hapifhir/hapi-fhir/issues/5205 - StreamReadConstraints.overrideDefaultStreamReadConstraints( - StreamReadConstraints.builder().maxStringLength(Integer.MAX_VALUE).build()); - FhirContext context = FhirContext.forR4(); HapiLocalizer localizer = new HapiLocalizer() { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/RootServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/RootServiceImpl.java index cef3c812e..77fa6aecf 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/RootServiceImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/RootServiceImpl.java @@ -69,9 +69,6 @@ public Response root(UriInfo uri, HttpHeaders headers) @Override public Response handleBundle(Bundle bundle, UriInfo uri, HttpHeaders headers) { - // FIXME hapi parser bug workaround - referenceCleaner.cleanReferenceResourcesIfBundle(bundle); - CommandList commands = exceptionHandler .handleBadBundleException(() -> commandFactory.createCommands(bundle, getCurrentIdentity(), parameterConverter.getPreferReturn(headers), parameterConverter.getPreferHandling(headers))); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java index 437d829fa..bb72ab9f5 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/impl/StructureDefinitionServiceImpl.java @@ -181,7 +181,8 @@ private void afterDelete(String id) @Override public Response postSnapshotNew(String snapshotPath, Parameters parameters, UriInfo uri, HttpHeaders headers) { - Type urlType = parameters.getParameter("url"); + ParametersParameterComponent param = parameters.getParameter("url"); + Type urlType = param.getValue(); Optional resource = parameters.getParameter().stream() .filter(p -> "resource".equals(p.getName())).findFirst(); diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java index fb8ceba59..5e75eff76 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/webservice/secure/AbstractResourceServiceSecure.java @@ -108,9 +108,6 @@ private String toValidationLogMessage(ValidationResult validationResult) private Response withResourceValidation(R resource, UriInfo uri, HttpHeaders headers, String method, Supplier delegate) { - // FIXME hapi parser bug workaround - referenceCleaner.cleanReferenceResourcesIfBundle(resource); - ValidationResult validationResult = resourceValidator.validate(resource); if (validationResult.getMessages().stream().anyMatch(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/BundleTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/BundleTest.java index 8cde91b95..8b448fe0b 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/BundleTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/BundleTest.java @@ -2,9 +2,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.UUID; import org.hl7.fhir.r4.model.Bundle; @@ -14,6 +16,7 @@ import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Reference; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,9 +28,11 @@ public class BundleTest { private static final Logger logger = LoggerFactory.getLogger(BundleTest.class); + private static FhirContext fhirContext = FhirContext.forR4(); + private IParser newXmlParser() { - IParser p = FhirContext.forR4().newXmlParser(); + IParser p = fhirContext.newXmlParser(); p.setStripVersionsFromReferences(false); p.setOverrideResourceIdWithBundleEntryFullUrl(false); return p; @@ -35,7 +40,7 @@ private IParser newXmlParser() private IParser newJsonParser() { - IParser p = FhirContext.forR4().newJsonParser(); + IParser p = fhirContext.newJsonParser(); p.setStripVersionsFromReferences(false); p.setOverrideResourceIdWithBundleEntryFullUrl(false); return p; @@ -94,15 +99,9 @@ private void testBundleWithParser(IParser parser) assertTrue(bundle2.getEntry().get(0).getResource() instanceof Organization); assertNotNull(((Organization) bundle2.getEntry().get(0).getResource()).getEndpointFirstRep().getResource()); - // FIXME workaround hapi parser bug - ((Organization) bundle2.getEntry().get(0).getResource()).getEndpointFirstRep().setResource(null); - assertTrue(bundle2.getEntry().get(1).getResource() instanceof Endpoint); assertNotNull(((Endpoint) bundle2.getEntry().get(1).getResource()).getManagingOrganization().getResource()); - // FIXME workaround hapi parser bug - ((Endpoint) bundle2.getEntry().get(1).getResource()).getManagingOrganization().setResource(null); - String bundle2String = parser.encodeResourceToString(bundle2); logger.debug("Bundle2: {}", bundle2String); @@ -123,10 +122,34 @@ public void testBundleVersionTag() throws Exception Bundle bRead = newXmlParser().parseResource(Bundle.class, bundleTxt); assertEquals("123", bRead.getMeta().getVersionId()); - assertNull(bRead.getIdElement().getVersionIdPart()); - - // FIXME workaround hapi parser bug - bRead.setIdElement(bRead.getIdElement().withVersion(bRead.getMeta().getVersionId())); assertEquals("123", bRead.getIdElement().getVersionIdPart()); } + + @Test + public void testParseBundleCheckNoContainedResources() throws Exception + { + try (InputStream in = Files.newInputStream(Paths.get("src/test/resources/bundle.xml"))) + { + Bundle bundle = newXmlParser().parseResource(Bundle.class, in); + assertNotNull(bundle); + assertNotNull(bundle.getEntry()); + assertEquals(8, bundle.getEntry().size()); + + assertNotNull(bundle.getEntry().get(0)); + assertNotNull(bundle.getEntry().get(0).getResource()); + assertTrue(bundle.getEntry().get(0).getResource() instanceof Organization); + + Organization org = (Organization) bundle.getEntry().get(0).getResource(); + assertNotNull(org.getEndpoint()); + assertEquals(1, org.getEndpoint().size()); + + Reference eRef = org.getEndpoint().get(0); + assertNotNull(eRef); + + // FIXME HAPI FHIR parser adds contained resources to bundle references, getResource() should return null + assertNotNull( + "HAPI FHIR parser does not add contained resources to bunlde references anymore, remove workaounds using ReferenceCleaner", + eRef.getResource()); + } + } } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/ParametersTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/ParametersTest.java index e98f60247..94b61042b 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/ParametersTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/ParametersTest.java @@ -1,6 +1,7 @@ package dev.dsf.fhir.hapi; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.util.Optional; @@ -36,8 +37,13 @@ public void testParametersWithoutResource() throws Exception FhirContext context = FhirContext.forR4(); logger.info("Parameters: {}", context.newXmlParser().encodeResourceToString(parameters)); - assertEquals(mode, parameters.getParameter("mode")); - assertEquals(uri, parameters.getParameter("uri")); + ParametersParameterComponent modeParam = parameters.getParameter("mode"); + assertNotNull(modeParam); + assertEquals(mode, modeParam.getValue()); + + ParametersParameterComponent uriParam = parameters.getParameter("uri"); + assertNotNull(modeParam); + assertEquals(uri, uriParam.getValue()); } @Test @@ -56,8 +62,12 @@ public void testParametersWithResource() throws Exception FhirContext context = FhirContext.forR4(); logger.info("Parameters: {}", context.newXmlParser().encodeResourceToString(parameters)); - assertEquals(mode, parameters.getParameter("mode")); - assertEquals(uri, parameters.getParameter("uri")); + ParametersParameterComponent modeParam = parameters.getParameter("mode"); + assertNotNull(modeParam); + assertEquals(mode, modeParam.getValue()); + ParametersParameterComponent uriParam = parameters.getParameter("uri"); + assertNotNull(uriParam); + assertEquals(uri, uriParam.getValue()); Optional resource = parameters.getParameter().stream() .filter(p -> "resource".equals(p.getName())).findFirst(); @@ -95,6 +105,8 @@ public void testParametersSnapshotOperationInWithUrl() throws Exception FhirContext context = FhirContext.forR4(); logger.info("Parameters: {}", context.newXmlParser().encodeResourceToString(parameters)); - assertEquals(uri, parameters.getParameter("url")); + ParametersParameterComponent urlParam = parameters.getParameter("url"); + assertNotNull(urlParam); + assertEquals(uri, urlParam.getValue()); } } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/ParserTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/ParserTest.java index a392930d7..23e6af24e 100755 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/ParserTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/hapi/ParserTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import java.io.InputStream; import java.nio.file.Files; @@ -45,16 +44,10 @@ public void testParseBundleWithVersion() throws Exception Bundle bundle = fhirContext.newJsonParser().parseResource(Bundle.class, bundleJson); assertNotNull(bundle); assertEquals("1", bundle.getMeta().getVersionId()); - // TODO HAPI bug -> null - // assertEquals("Bundle.id.version", "1", bundle.getIdElement().getVersionIdPart()); - assertNull("Bug in HAPI fixed, if method returns 1", bundle.getIdElement().getVersionIdPart()); - // TODO remove workaround in BundleDaoJdbc#getResource if bug is fixed in HAPI + assertEquals("Bundle.id.version", "1", bundle.getIdElement().getVersionIdPart()); } - // TODO HAPI bug -> StackOverflowError - // TODO remove workaround in WebserviceClientJersey#read(Class, String) - // and WebserviceClientJersey#read(Class, String, String) - @Test(expected = StackOverflowError.class) + @Test public void testParseBundleWithEntriesWithCircularReferences() throws Exception { Organization org = new Organization(); @@ -76,10 +69,7 @@ public void testParseBundleWithEntriesWithCircularReferences() throws Exception configureParser(fhirContext.newXmlParser()).encodeResourceToString(read); } - // TODO HAPI bug -> StackOverflowError - // TODO remove workaround in WebserviceClientJersey#read(Class, String) - // and WebserviceClientJersey#read(Class, String, String) - @Test(expected = StackOverflowError.class) + @Test public void testParseBundleWithEntriesWithCircularReferencesFile() throws Exception { try (InputStream in = Files.newInputStream(Paths.get("src/test/resources/bundle.xml"))) diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/AbstractIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/AbstractIntegrationTest.java index f53b174bb..8d1bd2602 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/AbstractIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/AbstractIntegrationTest.java @@ -284,20 +284,21 @@ protected static void writeBundle(Path bundleFile, Bundle bundle) protected static IParser newXmlParser() { - IParser parser = fhirContext.newXmlParser(); - parser.setStripVersionsFromReferences(false); - parser.setOverrideResourceIdWithBundleEntryFullUrl(false); - parser.setPrettyPrint(true); - return parser; + return newParser(fhirContext::newXmlParser); } protected static IParser newJsonParser() { - IParser parser = fhirContext.newJsonParser(); - parser.setStripVersionsFromReferences(false); - parser.setOverrideResourceIdWithBundleEntryFullUrl(false); - parser.setPrettyPrint(true); - return parser; + return newParser(fhirContext::newJsonParser); + } + + private static IParser newParser(Supplier supplier) + { + IParser p = supplier.get(); + p.setStripVersionsFromReferences(false); + p.setOverrideResourceIdWithBundleEntryFullUrl(false); + p.setPrettyPrint(true); + return p; } private static void createTestBundle(ClientCertificate clientCertificate, @@ -320,9 +321,6 @@ private static void createTestBundle(ClientCertificate clientCertificate, externalThumbprintExtension .setValue(new StringType(externalClientCertificate.getCertificateSha512ThumbprintHex())); - // FIXME hapi parser can't handle embedded resources and creates them while parsing bundles - new ReferenceCleanerImpl(new ReferenceExtractorImpl()).cleanReferenceResourcesIfBundle(testBundle); - writeBundle(FHIR_BUNDLE_FILE, testBundle); } diff --git a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java index 7a56bb32d..f7d3607fc 100644 --- a/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java +++ b/dsf-fhir/dsf-fhir-server/src/test/java/dev/dsf/fhir/integration/BundleIntegrationTest.java @@ -277,8 +277,7 @@ private Bundle createTestBundle(BundleType type, IdType resourceToDelete) private void checkReturnBundle(BundleType type, Bundle rBundle, int expectedEntrySize, List expectedStatus) { - logger.debug("Return Bundle:\n{}", - fhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(rBundle)); + logger.debug("Return Bundle:\n{}", newJsonParser().setPrettyPrint(true).encodeResourceToString(rBundle)); assertNotNull(rBundle); assertEquals(type, rBundle.getType()); diff --git a/dsf-fhir/dsf-fhir-validation/pom.xml b/dsf-fhir/dsf-fhir-validation/pom.xml index 1875ffe39..4dd9ad4a1 100644 --- a/dsf-fhir/dsf-fhir-validation/pom.xml +++ b/dsf-fhir/dsf-fhir-validation/pom.xml @@ -10,13 +10,20 @@ + + ca.uhn.hapi.fhir + hapi-fhir-caching-caffeine + ${hapi.fhir.version} + ca.uhn.hapi.fhir hapi-fhir-structures-r4 + ${hapi.fhir.version} ca.uhn.hapi.fhir hapi-fhir-structures-r5 + ${hapi.fhir.version} ca.uhn.hapi.fhir @@ -27,14 +34,17 @@ commons-logging + ${hapi.fhir.version} ca.uhn.hapi.fhir hapi-fhir-validation-resources-r4 + ${hapi.fhir.version} ca.uhn.hapi.fhir hapi-fhir-validation-resources-r5 + ${hapi.fhir.version} diff --git a/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/ResourceValidatorImpl.java b/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/ResourceValidatorImpl.java index 5b4ceae55..7a9c68b36 100755 --- a/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/ResourceValidatorImpl.java +++ b/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/ResourceValidatorImpl.java @@ -1,9 +1,20 @@ package dev.dsf.fhir.validation; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Locale; +import java.util.Set; import java.util.regex.Pattern; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidatorExtension; +import org.hl7.fhir.common.hapi.validation.validator.FixedVersionSpecificWorkerContextWrapper; +import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.model.CanonicalResource; +import org.hl7.fhir.r5.utils.validation.IResourceValidator; +import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; @@ -20,12 +31,86 @@ public class ResourceValidatorImpl implements ResourceValidator public ResourceValidatorImpl(FhirContext context, IValidationSupport validationSupport) { - this.validator = configureValidator(context.newValidator(), validationSupport); + this.validator = configureValidator(context, validationSupport); } - protected FhirValidator configureValidator(FhirValidator validator, IValidationSupport validationSupport) + protected FhirValidator configureValidator(FhirContext fhirContext, IValidationSupport validationSupport) { - FhirInstanceValidator instanceValidator = new FhirInstanceValidator(validationSupport); + FhirValidator validator = fhirContext.newValidator(); + + IWorkerContext workerContext = FixedVersionSpecificWorkerContextWrapper + .newVersionSpecificWorkerContextWrapper(validationSupport); + + IValidatorResourceFetcher resourceFetcher = new IValidatorResourceFetcher() + { + @Override + public IValidatorResourceFetcher setLocale(Locale locale) + { + return this; + } + + @Override + public boolean resolveURL(IResourceValidator validator, Object appContext, String path, String url, + String type, boolean canonical) throws IOException, FHIRException + { + if (("urn:ietf:bcp:13".equals(url) || "urn:ietf:bcp:13|4.0.1".equals(url) + || "urn:ietf:rfc:3986".equals(url)) && "uri".equals(type) && !canonical) + return true; + else if (url != null && url.startsWith("urn:uuid:") && url.length() == 45 + && ("uri".equals(type) || "url".equals(type)) && !canonical) + return true; + else if (url != null && (url.startsWith("http://") || url.startsWith("https://")) + && ("uri".equals(type) || "canonical".equals(type))) + return true; + else if (path != null && (path.startsWith("ActivityDefinition") || path.startsWith("Binary") + || path.startsWith("Bundle") || path.startsWith("CodeSystem") + || path.startsWith("DocumentReference") || path.startsWith("Endpoint") + || path.startsWith("Library") || path.startsWith("Organization") + || path.startsWith("QuestionnaireResponse") || path.startsWith("ResearchStudy") + || path.startsWith("StructureDefinition") || path.startsWith("Task"))) + return true; + + System.err.println("!!!!!!! " + path + ", " + url + ", " + type + ", " + canonical); + return false; + } + + @Override + public boolean fetchesCanonicalResource(IResourceValidator validator, String url) + { + return false; + } + + @Override + public byte[] fetchRaw(IResourceValidator validator, String url) throws IOException + { + return null; + } + + @Override + public Set fetchCanonicalResourceVersions(IResourceValidator validator, Object appContext, + String url) + { + return Set.of(); + } + + @Override + public CanonicalResource fetchCanonicalResource(IResourceValidator validator, Object appContext, String url) + throws URISyntaxException + { + return null; + } + + @Override + public org.hl7.fhir.r5.elementmodel.Element fetch(IResourceValidator validator, Object appContext, + String url) throws FHIRException, IOException + { + return null; + } + }; + + FhirInstanceValidator instanceValidator = new FhirInstanceValidatorExtension(validationSupport, resourceFetcher, + workerContext); + validator.registerValidatorModule(instanceValidator); return validator; } diff --git a/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/SnapshotGeneratorImpl.java b/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/SnapshotGeneratorImpl.java index 28575f517..23023b0dd 100755 --- a/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/SnapshotGeneratorImpl.java +++ b/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/SnapshotGeneratorImpl.java @@ -3,10 +3,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import org.hl7.fhir.r4.conformance.ProfileUtilities; import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; +import org.hl7.fhir.r4.model.ElementDefinition; +import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.slf4j.Logger; @@ -71,6 +74,24 @@ public SnapshotWithValidationMessages generateSnapshot(StructureDefinition diffe m.getLine(), m.getMessage())); } + // FIXME workaround HAPI ProfileUtilities bug + if ("http://dsf.dev/fhir/StructureDefinition/task-base".equals(differential.getBaseDefinition())) + { + Optional taskInputValueX = differential.getSnapshot().getElement().stream() + .filter(e -> "Task.input.value[x]".equals(e.getId()) && e.getFixed() instanceof StringType s + && s.getValue() != null) + .findFirst(); + + taskInputValueX.ifPresent(e -> + { + logger.warn("Removing fixedString value '{}' from StructureDefinition '{}|{}' snapshot element '{}'", + ((StringType) e.getFixed()).getValue(), differential.getUrl(), differential.getVersion(), + e.getId()); + + e.setFixed(null); + }); + } + return new SnapshotWithValidationMessages(differential, messages); } } diff --git a/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/ValidationSupportRule.java b/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/ValidationSupportRule.java index 37c4dfe48..0ee6d1ca9 100644 --- a/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/ValidationSupportRule.java +++ b/dsf-fhir/dsf-fhir-validation/src/main/java/dev/dsf/fhir/validation/ValidationSupportRule.java @@ -21,6 +21,7 @@ import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.r4.model.ActivityDefinition; import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.ValueSet; import org.junit.rules.ExternalResource; import org.slf4j.Logger; @@ -207,4 +208,15 @@ public ActivityDefinition readActivityDefinition(Path file) throws IOException return context.newXmlParser().parseResource(ActivityDefinition.class, read); } } + + public Task readTask(Path file) throws IOException + { + try (InputStream in = Files.newInputStream(file)) + { + String read = IOUtils.toString(in, StandardCharsets.UTF_8); + read = replaceVersionAndDate(read, version, date); + + return context.newXmlParser().parseResource(Task.class, read); + } + } } diff --git a/dsf-fhir/dsf-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/FhirInstanceValidatorExtension.java b/dsf-fhir/dsf-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/FhirInstanceValidatorExtension.java new file mode 100644 index 000000000..a8c8cd258 --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/FhirInstanceValidatorExtension.java @@ -0,0 +1,31 @@ +package org.hl7.fhir.common.hapi.validation.validator; + +import java.util.List; + +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; +import org.hl7.fhir.utilities.validation.ValidationMessage; + +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.validation.IValidationContext; + +public class FhirInstanceValidatorExtension extends FhirInstanceValidator +{ + private final IValidatorResourceFetcher resourceFetcher; + private final IWorkerContext workerContext; + + public FhirInstanceValidatorExtension(IValidationSupport validationSupport, + IValidatorResourceFetcher resourceFetcher, IWorkerContext workerContext) + { + super(validationSupport); + + this.resourceFetcher = resourceFetcher; + this.workerContext = workerContext; + } + + @Override + protected List validate(IValidationContext validationContext) + { + return ValidationWrapperExtension.create(resourceFetcher).validate(workerContext, validationContext); + } +} diff --git a/dsf-fhir/dsf-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/FixedVersionSpecificWorkerContextWrapper.java b/dsf-fhir/dsf-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/FixedVersionSpecificWorkerContextWrapper.java new file mode 100644 index 000000000..d13f8848b --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/FixedVersionSpecificWorkerContextWrapper.java @@ -0,0 +1,1163 @@ +package org.hl7.fhir.common.hapi.validation.validator; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.fhir.ucum.UcumService; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportUtils; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.TerminologyServiceException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.context.IContextResourceLoader; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.context.IWorkerContextManager; +import org.hl7.fhir.r5.model.CodeSystem; +import org.hl7.fhir.r5.model.CodeableConcept; +import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.ElementDefinition; +import org.hl7.fhir.r5.model.NamingSystem; +import org.hl7.fhir.r5.model.OperationOutcome; +import org.hl7.fhir.r5.model.PackageInformation; +import org.hl7.fhir.r5.model.Parameters; +import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.model.StringType; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.profilemodel.PEBuilder; +import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome; +import org.hl7.fhir.r5.terminologies.utilities.CodingValidationRequest; +import org.hl7.fhir.r5.terminologies.utilities.TerminologyServiceErrorClass; +import org.hl7.fhir.r5.terminologies.utilities.ValidationResult; +import org.hl7.fhir.r5.utils.validation.IResourceValidator; +import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier; +import org.hl7.fhir.utilities.FhirPublication; +import org.hl7.fhir.utilities.TimeTracker; +import org.hl7.fhir.utilities.i18n.I18nBase; +import org.hl7.fhir.utilities.npm.BasePackageCacheManager; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.support.ConceptValidationOptions; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.sl.cache.CacheFactory; +import ca.uhn.fhir.sl.cache.LoadingCache; +import ca.uhn.fhir.system.HapiSystemProperties; +import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; + +//copied and modified from hapi-fhir-validation version 7.4.0 +public class FixedVersionSpecificWorkerContextWrapper extends I18nBase implements IWorkerContext +{ + private static final Logger ourLog = LoggerFactory.getLogger(FixedVersionSpecificWorkerContextWrapper.class); + private final ValidationSupportContext myValidationSupportContext; + private final VersionCanonicalizer myVersionCanonicalizer; + private final LoadingCache myFetchResourceCache; + private volatile List myAllStructures; + private Parameters myExpansionProfile; + + public FixedVersionSpecificWorkerContextWrapper(ValidationSupportContext theValidationSupportContext, + VersionCanonicalizer theVersionCanonicalizer) + { + myValidationSupportContext = theValidationSupportContext; + myVersionCanonicalizer = theVersionCanonicalizer; + + long timeoutMillis = HapiSystemProperties.getTestValidationResourceCachesMs(); + + myFetchResourceCache = CacheFactory.build(timeoutMillis, 10000, key -> + { + String fetchResourceName = key.getResourceName(); + if (myValidationSupportContext.getRootValidationSupport().getFhirContext().getVersion() + .getVersion() == FhirVersionEnum.DSTU2) + { + if ("CodeSystem".equals(fetchResourceName)) + { + fetchResourceName = "ValueSet"; + } + } + + Class fetchResourceType; + if (fetchResourceName.equals("Resource")) + { + fetchResourceType = null; + } + else + { + fetchResourceType = myValidationSupportContext.getRootValidationSupport().getFhirContext() + .getResourceDefinition(fetchResourceName).getImplementingClass(); + } + + IBaseResource fetched = myValidationSupportContext.getRootValidationSupport() + .fetchResource(fetchResourceType, key.getUri()); + + Resource canonical = myVersionCanonicalizer.resourceToValidatorCanonical(fetched); + + if (canonical instanceof StructureDefinition) + { + StructureDefinition canonicalSd = (StructureDefinition) canonical; + if (canonicalSd.getSnapshot().isEmpty()) + { + ourLog.info("Generating snapshot for StructureDefinition: {}", canonicalSd.getUrl()); + fetched = myValidationSupportContext.getRootValidationSupport() + .generateSnapshot(theValidationSupportContext, fetched, "", null, ""); + Validate.isTrue(fetched != null, + "StructureDefinition %s has no snapshot, and no snapshot generator is configured", + key.getUri()); + canonical = myVersionCanonicalizer.resourceToValidatorCanonical(fetched); + } + } + + return canonical; + }); + + setValidationMessageLanguage(getLocale()); + } + + @Override + public Set getBinaryKeysAsSet() + { + throw new UnsupportedOperationException(Msg.code(2118)); + } + + @Override + public boolean hasBinaryKey(String s) + { + return myValidationSupportContext.getRootValidationSupport().fetchBinary(s) != null; + } + + @Override + public byte[] getBinaryForKey(String s) + { + return myValidationSupportContext.getRootValidationSupport().fetchBinary(s); + } + + @Override + public int loadFromPackage(NpmPackage pi, IContextResourceLoader loader) throws FHIRException + { + throw new UnsupportedOperationException(Msg.code(652)); + } + + @Override + public int loadFromPackage(NpmPackage pi, IContextResourceLoader loader, List types) + throws FileNotFoundException, IOException, FHIRException + { + throw new UnsupportedOperationException(Msg.code(653)); + } + + @Override + public int loadFromPackageAndDependencies(NpmPackage pi, IContextResourceLoader loader, BasePackageCacheManager pcm) + throws FHIRException + { + throw new UnsupportedOperationException(Msg.code(654)); + } + + @Override + public boolean hasPackage(String id, String ver) + { + throw new UnsupportedOperationException(Msg.code(655)); + } + + @Override + public boolean hasPackage(PackageInformation packageInformation) + { + return false; + } + + @Override + public PackageInformation getPackage(String id, String ver) + { + return null; + } + + @Override + public int getClientRetryCount() + { + throw new UnsupportedOperationException(Msg.code(656)); + } + + @Override + public IWorkerContext setClientRetryCount(int value) + { + throw new UnsupportedOperationException(Msg.code(657)); + } + + @Override + public TimeTracker clock() + { + return null; + } + + @Override + public IWorkerContextManager.IPackageLoadingTracker getPackageTracker() + { + throw new UnsupportedOperationException(Msg.code(2235)); + } + + @Override + public IWorkerContext setPackageTracker(IWorkerContextManager.IPackageLoadingTracker packageTracker) + { + throw new UnsupportedOperationException(Msg.code(2266)); + } + + @Override + public String getSpecUrl() + { + return ""; + } + + @Override + public PEBuilder getProfiledElementBuilder(PEBuilder.PEElementPropertiesPolicy thePEElementPropertiesPolicy, + boolean theB) + { + throw new UnsupportedOperationException(Msg.code(2264)); + } + + @Override + public PackageInformation getPackageForUrl(String s) + { + throw new UnsupportedOperationException(Msg.code(2236)); + } + + @Override + public Parameters getExpansionParameters() + { + return myExpansionProfile; + } + + @Override + public void setExpansionParameters(Parameters expParameters) + { + setExpansionProfile(expParameters); + } + + public void setExpansionProfile(Parameters expParameters) + { + myExpansionProfile = expParameters; + } + + private List allStructures() + { + + List retVal = myAllStructures; + if (retVal == null) + { + retVal = new ArrayList<>(); + for (IBaseResource next : myValidationSupportContext.getRootValidationSupport() + .fetchAllStructureDefinitions()) + { + try + { + StructureDefinition converted = myVersionCanonicalizer.structureDefinitionToCanonical(next); + retVal.add(converted); + } + catch (FHIRException e) + { + throw new InternalErrorException(Msg.code(659) + e); + } + } + myAllStructures = retVal; + } + + return retVal; + } + + @Override + public void cacheResource(Resource res) + { + } + + @Override + public void cacheResourceFromPackage(Resource res, PackageInformation packageDetails) throws FHIRException + { + } + + @Override + public void cachePackage(PackageInformation packageInformation) + { + } + + @Nonnull + private ValidationResult convertValidationResult(String theSystem, + @Nullable IValidationSupport.CodeValidationResult theResult) + { + ValidationResult retVal = null; + if (theResult != null) + { + String code = theResult.getCode(); + String display = theResult.getDisplay(); + + String issueSeverityCode = theResult.getSeverityCode(); + String message = theResult.getMessage(); + ValidationMessage.IssueSeverity issueSeverity = null; + if (issueSeverityCode != null) + { + issueSeverity = ValidationMessage.IssueSeverity.fromCode(issueSeverityCode); + } + else if (isNotBlank(message)) + { + issueSeverity = ValidationMessage.IssueSeverity.INFORMATION; + } + + CodeSystem.ConceptDefinitionComponent conceptDefinitionComponent = null; + if (code != null) + { + conceptDefinitionComponent = new CodeSystem.ConceptDefinitionComponent().setCode(code) + .setDisplay(display); + } + + retVal = new ValidationResult(issueSeverity, message, theSystem, theResult.getCodeSystemVersion(), + conceptDefinitionComponent, display, + getIssuesForCodeValidation(theResult.getCodeValidationIssues())); + } + + if (retVal == null) + { + retVal = new ValidationResult(ValidationMessage.IssueSeverity.ERROR, "Validation failed", null); + } + + return retVal; + } + + private List getIssuesForCodeValidation( + List codeValidationIssues) + { + List issues = new ArrayList<>(); + + for (IValidationSupport.CodeValidationIssue codeValidationIssue : codeValidationIssues) + { + + CodeableConcept codeableConcept = new CodeableConcept().setText(codeValidationIssue.getMessage()); + codeableConcept.addCoding("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + getIssueCodingFromCodeValidationIssue(codeValidationIssue), null); + + OperationOutcome.OperationOutcomeIssueComponent issue = new OperationOutcome.OperationOutcomeIssueComponent() + .setSeverity(getIssueSeverityFromCodeValidationIssue(codeValidationIssue)) + .setCode(getIssueTypeFromCodeValidationIssue(codeValidationIssue)).setDetails(codeableConcept); + issue.getDetails().setText(codeValidationIssue.getMessage()); + issue.addExtension().setUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id") + .setValue(new StringType("Terminology_PassThrough_TX_Message")); + issues.add(issue); + } + return issues; + } + + @SuppressWarnings("incomplete-switch") + private String getIssueCodingFromCodeValidationIssue(IValidationSupport.CodeValidationIssue codeValidationIssue) + { + switch (codeValidationIssue.getCoding()) + { + case VS_INVALID: + return "vs-invalid"; + case NOT_FOUND: + return "not-found"; + case NOT_IN_VS: + return "not-in-vs"; + case INVALID_CODE: + return "invalid-code"; + case INVALID_DISPLAY: + return "invalid-display"; + } + return null; + } + + @SuppressWarnings("incomplete-switch") + private OperationOutcome.IssueType getIssueTypeFromCodeValidationIssue( + IValidationSupport.CodeValidationIssue codeValidationIssue) + { + switch (codeValidationIssue.getCode()) + { + case NOT_FOUND: + return OperationOutcome.IssueType.NOTFOUND; + case CODE_INVALID: + return OperationOutcome.IssueType.CODEINVALID; + case INVALID: + return OperationOutcome.IssueType.INVALID; + } + return null; + } + + private OperationOutcome.IssueSeverity getIssueSeverityFromCodeValidationIssue( + IValidationSupport.CodeValidationIssue codeValidationIssue) + { + switch (codeValidationIssue.getSeverity()) + { + case FATAL: + return OperationOutcome.IssueSeverity.FATAL; + case ERROR: + return OperationOutcome.IssueSeverity.ERROR; + case WARNING: + return OperationOutcome.IssueSeverity.WARNING; + case INFORMATION: + return OperationOutcome.IssueSeverity.INFORMATION; + } + return null; + } + + @Override + public ValueSetExpansionOutcome expandVS(ValueSet source, boolean cacheOk, boolean Hierarchical) + { + IBaseResource convertedSource; + try + { + convertedSource = myVersionCanonicalizer.valueSetFromValidatorCanonical(source); + } + catch (FHIRException e) + { + throw new InternalErrorException(Msg.code(661) + e); + } + IValidationSupport.ValueSetExpansionOutcome expanded = myValidationSupportContext.getRootValidationSupport() + .expandValueSet(myValidationSupportContext, null, convertedSource); + + ValueSet convertedResult = null; + if (expanded.getValueSet() != null) + { + try + { + convertedResult = myVersionCanonicalizer.valueSetToValidatorCanonical(expanded.getValueSet()); + } + catch (FHIRException e) + { + throw new InternalErrorException(Msg.code(662) + e); + } + } + + String error = expanded.getError(); + TerminologyServiceErrorClass result = null; + + return new ValueSetExpansionOutcome(convertedResult, error, result, expanded.getErrorIsFromServer()); + } + + @Override + public ValueSetExpansionOutcome expandVS(Resource src, ElementDefinition.ElementDefinitionBindingComponent binding, + boolean cacheOk, boolean Hierarchical) + { + ValueSet valueSet = fetchResource(ValueSet.class, binding.getValueSet(), src); + return expandVS(valueSet, cacheOk, Hierarchical); + } + + @Override + public ValueSetExpansionOutcome expandVS(ValueSet.ConceptSetComponent inc, boolean hierarchical, boolean noInactive) + throws TerminologyServiceException + { + throw new UnsupportedOperationException(Msg.code(664)); + } + + @Override + public Locale getLocale() + { + return myValidationSupportContext.getRootValidationSupport().getFhirContext().getLocalizer().getLocale(); + } + + @Override + public void setLocale(Locale locale) + { + // ignore + } + + @Override + public CodeSystem fetchCodeSystem(String system) + { + IBaseResource fetched = myValidationSupportContext.getRootValidationSupport().fetchCodeSystem(system); + if (fetched == null) + { + return null; + } + try + { + return myVersionCanonicalizer.codeSystemToValidatorCanonical(fetched); + } + catch (FHIRException e) + { + throw new InternalErrorException(Msg.code(665) + e); + } + } + + @Override + public CodeSystem fetchCodeSystem(String system, String verison) + { + IBaseResource fetched = myValidationSupportContext.getRootValidationSupport().fetchCodeSystem(system); + if (fetched == null) + { + return null; + } + try + { + return myVersionCanonicalizer.codeSystemToValidatorCanonical(fetched); + } + catch (FHIRException e) + { + throw new InternalErrorException(Msg.code(1992) + e); + } + } + + @Override + public CodeSystem fetchCodeSystem(String system, FhirPublication fhirVersion) + { + return null; + } + + @Override + public CodeSystem fetchCodeSystem(String system, String version, FhirPublication fhirVersion) + { + return null; + } + + @Override + public CodeSystem fetchSupplementedCodeSystem(String system) + { + return null; + } + + @Override + public CodeSystem fetchSupplementedCodeSystem(String system, String version) + { + return null; + } + + @Override + public CodeSystem fetchSupplementedCodeSystem(String system, FhirPublication fhirVersion) + { + return null; + } + + @Override + public CodeSystem fetchSupplementedCodeSystem(String system, String version, FhirPublication fhirVersion) + { + return null; + } + + @Override + public T fetchResourceRaw(Class class_, String uri) + { + return fetchResource(class_, uri); + } + + @Override + public T fetchResource(Class class_, String theUri) + { + if (isBlank(theUri)) + { + return null; + } + + ResourceKey key = new ResourceKey(class_.getSimpleName(), theUri); + @SuppressWarnings("unchecked") + T retVal = (T) myFetchResourceCache.get(key); + + return retVal; + } + + @Override + public Resource fetchResourceById(String type, String uri) + { + throw new UnsupportedOperationException(Msg.code(666)); + } + + @Override + public Resource fetchResourceById(String type, String uri, FhirPublication fhirVersion) + { + return null; + } + + @Override + public T fetchResourceWithException(Class class_, String uri) throws FHIRException + { + T retVal = fetchResource(class_, uri); + if (retVal == null) + { + throw new FHIRException( + Msg.code(667) + "Can not find resource of type " + class_.getSimpleName() + " with uri " + uri); + } + return retVal; + } + + @Override + public T fetchResource(Class class_, String uri, String version) + { + return fetchResource(class_, uri + "|" + version); + } + + @Override + public T fetchResource(Class class_, String uri, FhirPublication fhirVersion) + { + return null; + } + + @Override + public T fetchResource(Class class_, String uri, String version, + FhirPublication fhirVersion) + { + return null; + } + + @Override + public T fetchResource(Class class_, String uri, Resource canonicalForSource) + { + return fetchResource(class_, uri); + } + + @Override + public List fetchResourcesByType(Class class_, FhirPublication fhirVersion) + { + return null; + } + + @Override + public T fetchResourceWithException(Class class_, String uri, Resource sourceOfReference) + throws FHIRException + { + throw new UnsupportedOperationException(Msg.code(2214)); + } + + @Override + public List getResourceNames() + { + return new ArrayList<>( + myValidationSupportContext.getRootValidationSupport().getFhirContext().getResourceTypes()); + } + + @Override + public List getResourceNames(FhirPublication fhirVersion) + { + return null; + } + + @Override + public Set getResourceNamesAsSet() + { + return myValidationSupportContext.getRootValidationSupport().getFhirContext().getResourceTypes(); + } + + @Override + public Set getResourceNamesAsSet(FhirPublication theFhirVersion) + { + return null; + } + + @Override + public StructureDefinition fetchTypeDefinition(String theTypeName) + { + return fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + theTypeName); + } + + @Override + public StructureDefinition fetchTypeDefinition(String theTypeName, FhirPublication theFhirVersion) + { + return null; + } + + @Override + public List fetchTypeDefinitions(String theTypeName) + { + List allStructures = new ArrayList<>(allStructures()); + allStructures.removeIf(sd -> !sd.hasType() || !sd.getType().equals(theTypeName)); + return allStructures; + } + + @Override + public List fetchTypeDefinitions(String theTypeName, FhirPublication theFhirVersion) + { + return null; + } + + @Override + public boolean isPrimitiveType(String theType) + { + List allStructures = new ArrayList<>(allStructures()); + return allStructures.stream() + .filter(structureDefinition -> structureDefinition + .getKind() == StructureDefinition.StructureDefinitionKind.PRIMITIVETYPE) + .anyMatch(structureDefinition -> theType.equals(structureDefinition.getName())); + } + + @Override + public boolean isDataType(String theType) + { + return !isPrimitiveType(theType); + } + + @Override + public UcumService getUcumService() + { + throw new UnsupportedOperationException(Msg.code(676)); + } + + @Override + public void setUcumService(UcumService ucumService) + { + throw new UnsupportedOperationException(Msg.code(677)); + } + + @Override + public String getVersion() + { + return myValidationSupportContext.getRootValidationSupport().getFhirContext().getVersion().getVersion() + .getFhirVersionString(); + } + + @Override + public boolean hasResource(Class class_, String uri) + { + if (isBlank(uri)) + { + return false; + } + + ResourceKey key = new ResourceKey(class_.getSimpleName(), uri); + return myFetchResourceCache.get(key) != null; + } + + @Override + public boolean hasResource(Class class_, String uri, Resource sourceOfReference) + { + return false; + } + + @Override + public boolean hasResource(Class class_, String uri, FhirPublication fhirVersion) + { + return false; + } + + @Override + public boolean isNoTerminologyServer() + { + return false; + } + + @Override + public Set getCodeSystemsUsed() + { + throw new UnsupportedOperationException(Msg.code(681)); + } + + @Override + public IResourceValidator newValidator() + { + throw new UnsupportedOperationException(Msg.code(684)); + } + + @Override + public Map getNSUrlMap() + { + throw new UnsupportedOperationException(Msg.code(2265)); + } + + @Override + public org.hl7.fhir.r5.context.ILoggingService getLogger() + { + return null; + } + + @Override + public void setLogger(org.hl7.fhir.r5.context.ILoggingService logger) + { + throw new UnsupportedOperationException(Msg.code(687)); + } + + @Override + public boolean supportsSystem(String system) + { + return myValidationSupportContext.getRootValidationSupport().isCodeSystemSupported(myValidationSupportContext, + system); + } + + @Override + public boolean supportsSystem(String system, FhirPublication fhirVersion) throws TerminologyServiceException + { + return supportsSystem(system); + } + + @Override + public ValueSetExpansionOutcome expandVS(ValueSet source, boolean cacheOk, boolean heiarchical, + boolean incompleteOk) + { + return null; + } + + @Override + public ValidationResult validateCode(ValidationOptions theOptions, String system, String version, String code, + String display) + { + ConceptValidationOptions validationOptions = convertConceptValidationOptions(theOptions); + return doValidation(null, validationOptions, system, code, display); + } + + @Override + public ValidationResult validateCode(ValidationOptions theOptions, String theSystem, String version, String theCode, + String display, ValueSet theValueSet) + { + IBaseResource convertedVs = null; + + try + { + if (theValueSet != null) + { + convertedVs = myVersionCanonicalizer.valueSetFromValidatorCanonical(theValueSet); + } + } + catch (FHIRException e) + { + throw new InternalErrorException(Msg.code(689) + e); + } + + ConceptValidationOptions validationOptions = convertConceptValidationOptions(theOptions); + + return doValidation(convertedVs, validationOptions, theSystem, theCode, display); + } + + @Override + public ValidationResult validateCode(ValidationOptions theOptions, String code, ValueSet theValueSet) + { + IBaseResource convertedVs = null; + try + { + if (theValueSet != null) + { + convertedVs = myVersionCanonicalizer.valueSetFromValidatorCanonical(theValueSet); + } + } + catch (FHIRException e) + { + throw new InternalErrorException(Msg.code(690) + e); + } + + String system = ValidationSupportUtils.extractCodeSystemForCode(theValueSet, code); + + ConceptValidationOptions validationOptions = convertConceptValidationOptions(theOptions).setInferSystem(true); + + return doValidation(convertedVs, validationOptions, system, code, null); + } + + @Override + public ValidationResult validateCode(ValidationOptions theOptions, Coding theCoding, ValueSet theValueSet) + { + IBaseResource convertedVs = null; + + try + { + if (theValueSet != null) + { + convertedVs = myVersionCanonicalizer.valueSetFromValidatorCanonical(theValueSet); + } + } + catch (FHIRException e) + { + throw new InternalErrorException(Msg.code(691) + e); + } + + ConceptValidationOptions validationOptions = convertConceptValidationOptions(theOptions); + String system = theCoding.getSystem(); + String code = theCoding.getCode(); + String display = theCoding.getDisplay(); + + return doValidation(convertedVs, validationOptions, system, code, display); + } + + @Override + public ValidationResult validateCode(ValidationOptions options, Coding code, ValueSet vs, + ValidationContextCarrier ctxt) + { + return validateCode(options, code, vs); + } + + @Override + public void validateCodeBatch(ValidationOptions options, List codes, ValueSet vs) + { + for (CodingValidationRequest next : codes) + { + ValidationResult outcome = validateCode(options, next.getCoding(), vs); + next.setResult(outcome); + } + } + + @Override + public void validateCodeBatchByRef(ValidationOptions validationOptions, + List list, String s) + { + ValueSet valueSet = fetchResource(ValueSet.class, s); + validateCodeBatch(validationOptions, list, valueSet); + } + + @Nonnull + private ValidationResult doValidation(IBaseResource theValueSet, ConceptValidationOptions theValidationOptions, + String theSystem, String theCode, String theDisplay) + { + IValidationSupport.CodeValidationResult result; + if (theValueSet != null) + { + result = validateCodeInValueSet(theValueSet, theValidationOptions, theSystem, theCode, theDisplay); + } + else + { + result = validateCodeInCodeSystem(theValidationOptions, theSystem, theCode, theDisplay); + } + return convertValidationResult(theSystem, result); + } + + private IValidationSupport.CodeValidationResult validateCodeInValueSet(IBaseResource theValueSet, + ConceptValidationOptions theValidationOptions, String theSystem, String theCode, String theDisplay) + { + IValidationSupport.CodeValidationResult result = myValidationSupportContext.getRootValidationSupport() + .validateCodeInValueSet(myValidationSupportContext, theValidationOptions, theSystem, theCode, + theDisplay, theValueSet); + if (result != null) + { + /* + * We got a value set result, which could be successful, or could contain errors/warnings. The code might + * also be invalid in the code system, so we will check that as well and add those issues to our result. + */ + IValidationSupport.CodeValidationResult codeSystemResult = validateCodeInCodeSystem(theValidationOptions, + theSystem, theCode, theDisplay); + final boolean valueSetResultContainsInvalidDisplay = result.getCodeValidationIssues().stream() + .anyMatch(codeValidationIssue -> codeValidationIssue + .getCoding() == IValidationSupport.CodeValidationIssueCoding.INVALID_DISPLAY); + if (codeSystemResult != null) + { + for (IValidationSupport.CodeValidationIssue codeValidationIssue : codeSystemResult + .getCodeValidationIssues()) + { + /* + * Value set validation should already have checked the display name. If we get INVALID_DISPLAY + * issues from code system validation, they will only repeat what was already caught. + */ + if (codeValidationIssue.getCoding() != IValidationSupport.CodeValidationIssueCoding.INVALID_DISPLAY + || !valueSetResultContainsInvalidDisplay) + { + result.addCodeValidationIssue(codeValidationIssue); + } + } + } + } + return result; + } + + private IValidationSupport.CodeValidationResult validateCodeInCodeSystem( + ConceptValidationOptions theValidationOptions, String theSystem, String theCode, String theDisplay) + { + return myValidationSupportContext.getRootValidationSupport().validateCode(myValidationSupportContext, + theValidationOptions, theSystem, theCode, theDisplay, null); + } + + @Override + public ValidationResult validateCode(ValidationOptions theOptions, CodeableConcept code, ValueSet theVs) + { + + List validationResultsOk = new ArrayList<>(); + List issues = new ArrayList<>(); + for (Coding next : code.getCoding()) + { + if (!next.hasSystem()) + { + String message = "Coding has no system. A code with no system has no defined meaning, and it cannot be validated. A system should be provided"; + OperationOutcome.OperationOutcomeIssueComponent issue = new OperationOutcome.OperationOutcomeIssueComponent() + .setSeverity(OperationOutcome.IssueSeverity.WARNING) + .setCode(OperationOutcome.IssueType.NOTFOUND).setDiagnostics(message) + .setDetails(new CodeableConcept().setText(message)); + + issues.add(issue); + } + ValidationResult retVal = validateCode(theOptions, next, theVs); + if (retVal.isOk()) + { + validationResultsOk.add(retVal); + } + else + { + for (OperationOutcome.OperationOutcomeIssueComponent issue : retVal.getIssues()) + { + issues.add(issue); + } + } + } + + if (code.getCoding().size() > 0) + { + if (!myValidationSupportContext.isEnabledValidationForCodingsLogicalAnd()) + { + if (validationResultsOk.size() == code.getCoding().size()) + { + return validationResultsOk.get(0); + } + } + else + { + if (validationResultsOk.size() > 0) + { + return validationResultsOk.get(0); + } + } + } + + return new ValidationResult(ValidationMessage.IssueSeverity.ERROR, null, issues); + } + + public void invalidateCaches() + { + myFetchResourceCache.invalidateAll(); + } + + @SuppressWarnings("unchecked") + @Override + public List fetchResourcesByType(Class theClass) + { + if (theClass.equals(StructureDefinition.class)) + { + return (List) allStructures(); + } + throw new UnsupportedOperationException(Msg.code(650) + "Unable to fetch resources of type: " + theClass); + } + + @Override + public List fetchResourcesByUrl(Class class_, String url) + { + throw new UnsupportedOperationException(Msg.code(2509) + "Can't fetch all resources of url: " + url); + } + + @Override + public boolean isForPublication() + { + return false; + } + + @Override + public void setForPublication(boolean b) + { + throw new UnsupportedOperationException(Msg.code(2351)); + } + + @Override + public OIDSummary urlsForOid(String oid, String resourceType) + { + return null; + } + + @Override + public T findTxResource(Class class_, String canonical, Resource sourceOfReference) + { + if (canonical == null) + { + return null; + } + return fetchResource(class_, canonical, sourceOfReference); + } + + @Override + public T findTxResource(Class class_, String canonical) + { + if (canonical == null) + { + return null; + } + + return fetchResource(class_, canonical); + } + + @Override + public T findTxResource(Class class_, String canonical, String version) + { + if (canonical == null) + { + return null; + } + + return fetchResource(class_, canonical, version); + } + + public static ConceptValidationOptions convertConceptValidationOptions(ValidationOptions theOptions) + { + ConceptValidationOptions retVal = new ConceptValidationOptions(); + if (theOptions.isGuessSystem()) + { + retVal = retVal.setInferSystem(true); + } + return retVal; + } + + @Nonnull + public static FixedVersionSpecificWorkerContextWrapper newVersionSpecificWorkerContextWrapper( + IValidationSupport theValidationSupport) + { + VersionCanonicalizer versionCanonicalizer = new VersionCanonicalizer(theValidationSupport.getFhirContext()); + return new FixedVersionSpecificWorkerContextWrapper(new ValidationSupportContext(theValidationSupport), + versionCanonicalizer); + } + + private static class ResourceKey + { + private final int myHashCode; + private final String myResourceName; + private final String myUri; + + private ResourceKey(String theResourceName, String theUri) + { + myResourceName = theResourceName; + myUri = theUri; + myHashCode = new HashCodeBuilder(17, 37).append(myResourceName).append(myUri).toHashCode(); + } + + @Override + public boolean equals(Object theO) + { + if (this == theO) + { + return true; + } + + if (theO == null || getClass() != theO.getClass()) + { + return false; + } + + ResourceKey that = (ResourceKey) theO; + + return new EqualsBuilder().append(myResourceName, that.myResourceName).append(myUri, that.myUri).isEquals(); + } + + public String getResourceName() + { + return myResourceName; + } + + public String getUri() + { + return myUri; + } + + @Override + public int hashCode() + { + return myHashCode; + } + } + + @Override + public Boolean subsumes(ValidationOptions optionsArg, Coding parent, Coding child) + { + throw new UnsupportedOperationException(Msg.code(2489)); + } + + @Override + public boolean isServerSideSystem(String url) + { + return false; + } +} diff --git a/dsf-fhir/dsf-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidationWrapperExtension.java b/dsf-fhir/dsf-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidationWrapperExtension.java new file mode 100644 index 000000000..b57c73f9a --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidationWrapperExtension.java @@ -0,0 +1,22 @@ +package org.hl7.fhir.common.hapi.validation.validator; + +import java.util.Collections; + +import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; +import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; + +public class ValidationWrapperExtension extends ValidatorWrapper +{ + public ValidationWrapperExtension() + { + } + + public static ValidatorWrapper create(IValidatorResourceFetcher validatorResourceFetcher) + { + return new ValidationWrapperExtension().setAnyExtensionsAllowed(true) + .setBestPracticeWarningLevel(BestPracticeWarningLevel.Ignore).setErrorForUnknownProfiles(true) + .setExtensionDomains(Collections.emptyList()).setValidationPolicyAdvisor(new FhirDefaultPolicyAdvisor()) + .setNoTerminologyChecks(false).setNoExtensibleWarnings(false).setNoBindingMsgSuppressed(false) + .setValidatorResourceFetcher(validatorResourceFetcher).setAssumeValidRestReferences(false); + } +} diff --git a/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/ActivityDefinitionProfileTest.java b/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/ActivityDefinitionProfileTest.java index d62819379..6eeadb018 100644 --- a/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/ActivityDefinitionProfileTest.java +++ b/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/ActivityDefinitionProfileTest.java @@ -78,7 +78,9 @@ private void logMessages(ValidationResult result) private void logResource(Resource resource) { logger.trace("{}", - validationRule.getFhirContext().newJsonParser().setPrettyPrint(false).encodeResourceToString(resource)); + validationRule.getFhirContext().newJsonParser().setStripVersionsFromReferences(false) + .setOverrideResourceIdWithBundleEntryFullUrl(false).setPrettyPrint(false) + .encodeResourceToString(resource)); } @Test diff --git a/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/QuestionnaireProfileTest.java b/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/QuestionnaireProfileTest.java index 6aa674a24..5d532cb55 100644 --- a/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/QuestionnaireProfileTest.java +++ b/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/QuestionnaireProfileTest.java @@ -178,8 +178,8 @@ private void testQuestionnaireInvalidType(String profileVersion, Questionnaire.Q result.getMessages().stream() .filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) || ResultSeverityEnum.FATAL.equals(m.getSeverity())) - .filter(m -> m.getMessage() != null).filter(m -> m.getMessage().startsWith("type-code")) - .count()); + .filter(m -> m.getMessage() != null) + .filter(m -> m.getMessage().startsWith("Constraint failed: type-code")).count()); } private Questionnaire createQuestionnaire(String profileVersion, Questionnaire.QuestionnaireItemType type) diff --git a/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/QuestionnaireResponseProfileTest.java b/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/QuestionnaireResponseProfileTest.java index a98dba282..1f222177d 100644 --- a/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/QuestionnaireResponseProfileTest.java +++ b/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/QuestionnaireResponseProfileTest.java @@ -173,8 +173,8 @@ public void testQuestionnaireResponseInvalidCompletedNoAuthorAndNoAuthored() .filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) || ResultSeverityEnum.FATAL.equals(m.getSeverity())) .filter(m -> m.getMessage() != null) - .filter(m -> m.getMessage().startsWith("authored-if-completed") - || m.getMessage().startsWith("author-if-completed")) + .filter(m -> m.getMessage().startsWith("Constraint failed: authored-if-completed") + || m.getMessage().startsWith("Constraint failed: author-if-completed")) .count()); } @@ -195,7 +195,7 @@ public void testQuestionnaireResponseInvalidCompletedWithAuthorReferenceAndAutho .filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) || ResultSeverityEnum.FATAL.equals(m.getSeverity())) .filter(m -> m.getMessage() != null) - .filter(m -> m.getMessage().startsWith("author-if-completed")).count()); + .filter(m -> m.getMessage().startsWith("Constraint failed: author-if-completed")).count()); } private QuestionnaireResponse createQuestionnaireResponseWithBusinessKey(Type type) diff --git a/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/TaskProfileTest.java b/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/TaskProfileTest.java index 05cdaa47c..2848c42dc 100755 --- a/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/TaskProfileTest.java +++ b/dsf-fhir/dsf-fhir-validation/src/test/java/dev/dsf/fhir/profiles/TaskProfileTest.java @@ -10,6 +10,8 @@ import org.hl7.fhir.r4.model.ResourceType; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.TaskIntent; +import org.hl7.fhir.r4.model.Task.TaskStatus; import org.junit.ClassRule; import org.junit.Test; import org.slf4j.Logger; @@ -28,8 +30,9 @@ public class TaskProfileTest @ClassRule public static final ValidationSupportRule validationRule = new ValidationSupportRule( - List.of("dsf-task-base-1.0.0.xml"), List.of("dsf-bpmn-message-1.0.0.xml"), - List.of("dsf-bpmn-message-1.0.0.xml")); + List.of("dsf-task-base-1.0.0.xml", "dsf-task-test.xml"), + List.of("dsf-bpmn-message-1.0.0.xml", "dsf-test.xml"), + List.of("dsf-bpmn-message-1.0.0.xml", "dsf-test.xml")); private final ResourceValidator resourceValidator = new ResourceValidatorImpl(validationRule.getFhirContext(), validationRule.getValidationSupport()); @@ -146,4 +149,30 @@ private ValidationResult validate(Task task) return result; } + + @Test + public void testTaskValidationWithAdditionalInputNotInDsfBaseTask() + { + Task task = new Task(); + task.getMeta().addProfile("http://dsf.dev/fhir/StructureDefinition/task-test"); + task.setInstantiatesCanonical("http://dsf.dev/bpe/Process/test|1.4"); + task.setStatus(TaskStatus.REQUESTED); + task.setIntent(TaskIntent.ORDER); + task.setAuthoredOn(new Date()); + task.getRequester().setType(ResourceType.Organization.name()).getIdentifier() + .setSystem("http://dsf.dev/sid/organization-identifier").setValue("Test_DIC_1"); + task.getRestriction().addRecipient().setType(ResourceType.Organization.name()).getIdentifier() + .setSystem("http://dsf.dev/sid/organization-identifier").setValue("Test_DIC_1"); + + task.addInput().setValue(new StringType("test")).getType().getCodingFirstRep() + .setSystem("http://dsf.dev/fhir/CodeSystem/bpmn-message").setCode("message-name"); + task.addInput().setValue(new StringType("some-test-string")).getType().getCodingFirstRep() + .setSystem("http://dsf.dev/fhir/CodeSystem/test").setCode("string-example"); + + ValidationResult result = resourceValidator.validate(task); + ValidationSupportRule.logValidationMessages(logger, result); + + assertEquals(0, result.getMessages().stream().filter(m -> ResultSeverityEnum.ERROR.equals(m.getSeverity()) + || ResultSeverityEnum.FATAL.equals(m.getSeverity())).count()); + } } diff --git a/dsf-fhir/dsf-fhir-validation/src/test/resources/fhir/CodeSystem/dsf-test.xml b/dsf-fhir/dsf-fhir-validation/src/test/resources/fhir/CodeSystem/dsf-test.xml new file mode 100644 index 000000000..6b498fe64 --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/test/resources/fhir/CodeSystem/dsf-test.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + <status value="active" /> + <experimental value="false" /> + <date value="2023-12-03" /> + <publisher value="DSF" /> + <description value="CodeSystem with standard values for a test process" /> + <caseSensitive value="true" /> + <hierarchyMeaning value="grouped-by" /> + <versionNeeded value="false" /> + <content value="complete" /> + <concept> + <code value="string-example" /> + <display value="String Example" /> + <definition value="Example string input value" /> + </concept> +</CodeSystem> \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-validation/src/test/resources/fhir/StructureDefinition/dsf-task-test.xml b/dsf-fhir/dsf-fhir-validation/src/test/resources/fhir/StructureDefinition/dsf-task-test.xml new file mode 100644 index 000000000..11d228862 --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/test/resources/fhir/StructureDefinition/dsf-task-test.xml @@ -0,0 +1,86 @@ +<StructureDefinition xmlns="http://hl7.org/fhir"> + <meta> + <tag> + <system value="http://dsf.dev/fhir/CodeSystem/read-access-tag" /> + <code value="ALL" /> + </tag> + </meta> + <url value="http://dsf.dev/fhir/StructureDefinition/task-test" /> + <version value="1.4" /> + <name value="testProcess" /> + <status value="active" /> + <experimental value="false" /> + <date value="2023-12-03" /> + <fhirVersion value="4.0.1" /> + <kind value="resource" /> + <abstract value="false" /> + <type value="Task" /> + <baseDefinition value="http://dsf.dev/fhir/StructureDefinition/task-base" /> + <derivation value="constraint" /> + <differential> + <element id="Task.instantiatesUri"> + <path value="Task.instantiatesUri" /> + <fixedUri value="http://dsf.dev/bpe/Process/test/1.4" /> + </element> + <element id="Task.input"> + <extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name"> + <valueString value="Parameter" /> + </extension> + <path value="Task.input" /> + </element> + <element id="Task.input:message-name"> + <extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name"> + <valueString value="Parameter" /> + </extension> + <path value="Task.input" /> + <sliceName value="message-name" /> + </element> + <element id="Task.input:message-name.value[x]"> + <path value="Task.input.value[x]" /> + <fixedString value="test" /> + </element> + <element id="Task.input:correlation-key"> + <extension url="http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name"> + <valueString value="Parameter" /> + </extension> + <path value="Task.input" /> + <sliceName value="correlation-key" /> + <max value="0" /> + </element> + <element id="Task.input:string-example"> + <path value="Task.input" /> + <sliceName value="string-example" /> + <min value="1" /> + <max value="1" /> + </element> + <element id="Task.input:string-example.type"> + <path value="Task.input.type" /> + <binding> + <strength value="required" /> + <valueSet value="http://dsf.dev/fhir/ValueSet/test|1.4" /> + </binding> + </element> + <element id="Task.input:string-example.type.coding"> + <path value="Task.input.type.coding" /> + <min value="1" /> + <max value="1" /> + </element> + <element id="Task.input:string-example.system"> + <path value="Task.input.type.coding.system" /> + <min value="1" /> + <fixedUri value="http://dsf.dev/fhir/CodeSystem/test" /> + </element> + <element id="Task.input:string-example.type.coding.code"> + <path value="Task.input.type.coding.code" /> + <min value="1" /> + <fixedCode value="string-example" /> + </element> + <element id="Task.input:string-example.value[x]"> + <path value="Task.input.value[x]" /> + <type> + <code value="string" /> + </type> + <min value="1" /> + </element> + </differential> +</StructureDefinition> \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-validation/src/test/resources/fhir/ValueSet/dsf-test.xml b/dsf-fhir/dsf-fhir-validation/src/test/resources/fhir/ValueSet/dsf-test.xml new file mode 100644 index 000000000..7e0bdf2c7 --- /dev/null +++ b/dsf-fhir/dsf-fhir-validation/src/test/resources/fhir/ValueSet/dsf-test.xml @@ -0,0 +1,24 @@ +<ValueSet xmlns="http://hl7.org/fhir"> + <meta> + <tag> + <system value="http://dsf.dev/fhir/CodeSystem/read-access-tag" /> + <code value="ALL" /> + </tag> + </meta> + <url value="http://dsf.dev/fhir/ValueSet/test" /> + <version value="1.4" /> + <name value="DSF_Test" /> + <title value="DSF Test" /> + <status value="active" /> + <experimental value="false" /> + <date value="2023-12-03" /> + <publisher value="DSF" /> + <description value="ValueSet with standard values for a test process" /> + <immutable value="true" /> + <compose> + <include> + <system value="http://dsf.dev/fhir/CodeSystem/test" /> + <version value="1.4" /> + </include> + </compose> +</ValueSet> \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-webservice-client/pom.xml b/dsf-fhir/dsf-fhir-webservice-client/pom.xml index b027b2c26..c72d451e0 100755 --- a/dsf-fhir/dsf-fhir-webservice-client/pom.xml +++ b/dsf-fhir/dsf-fhir-webservice-client/pom.xml @@ -17,11 +17,7 @@ <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-r4</artifactId> - </dependency> - - <dependency> - <groupId>de.hs-heilbronn.mi</groupId> - <artifactId>crypto-utils</artifactId> + <version>${hapi.fhir.version}</version> </dependency> <dependency> diff --git a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java index b4cdce0af..8a1257ad8 100755 --- a/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java +++ b/dsf-fhir/dsf-fhir-webservice-client/src/main/java/dev/dsf/fhir/client/FhirWebserviceClientJersey.java @@ -71,8 +71,6 @@ private static Class<?> getFhirClass(ResourceType type) } } - private final ReferenceCleaner referenceCleaner; - private final PreferReturnMinimalWithRetry preferReturnMinimal; private final PreferReturnOutcomeWithRetry preferReturnOutcome; @@ -82,10 +80,8 @@ public FhirWebserviceClientJersey(String baseUrl, KeyStore trustStore, KeyStore ReferenceCleaner referenceCleaner) { super(baseUrl, trustStore, keyStore, keyStorePassword, objectMapper, - Collections.singleton(new FhirAdapter(fhirContext)), proxySchemeHostPort, proxyUserName, proxyPassword, - connectTimeout, readTimeout, logRequests, userAgentValue); - - this.referenceCleaner = referenceCleaner; + Collections.singleton(new FhirAdapter(fhirContext, referenceCleaner)), proxySchemeHostPort, + proxyUserName, proxyPassword, connectTimeout, readTimeout, logRequests, userAgentValue); preferReturnMinimal = new PreferReturnMinimalWithRetryImpl(this); preferReturnOutcome = new PreferReturnOutcomeWithRetryImpl(this); @@ -134,11 +130,7 @@ private PreferReturn toPreferReturn(PreferReturnType returnType, Class<? extends { return switch (returnType) { - case REPRESENTATION -> { - // TODO remove workaround if HAPI bug fixed - Resource resource = referenceCleaner.cleanReferenceResourcesIfBundle(response.readEntity(resourceType)); - yield PreferReturn.resource(resource); - } + case REPRESENTATION -> PreferReturn.resource(response.readEntity(resourceType)); case MINIMAL -> PreferReturn.minimal(response.getLocation()); case OPERATION_OUTCOME -> PreferReturn.outcome(response.readEntity(OperationOutcome.class)); default -> @@ -300,8 +292,7 @@ Bundle postBundle(PreferReturnType returnType, Bundle bundle) logStatusAndHeaders(response); if (Status.OK.getStatusCode() == response.getStatus()) - // TODO remove workaround if HAPI bug fixed - return referenceCleaner.cleanReferenceResourcesIfBundle(response.readEntity(Bundle.class)); + return response.readEntity(Bundle.class); else throw handleError(response); } @@ -427,9 +418,7 @@ public Resource read(String resourceTypeName, String id) logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), response.getStatusInfo().getReasonPhrase()); if (Status.OK.getStatusCode() == response.getStatus()) - // TODO remove workaround if HAPI bug fixed - return referenceCleaner.cleanReferenceResourcesIfBundle( - (Resource) response.readEntity(RESOURCE_TYPES_BY_NAME.get(resourceTypeName))); + return (Resource) response.readEntity(RESOURCE_TYPES_BY_NAME.get(resourceTypeName)); else throw handleError(response); } @@ -468,7 +457,7 @@ private <R extends Resource> R read(Class<R> resourceType, String id, R oldValue { String dateValue = formatRfc7231(oldValue.getMeta().getLastUpdated()); request.header(HttpHeaders.IF_MODIFIED_SINCE, dateValue); - logger.trace("Sending {} Header with value '{}'", HttpHeaders.IF_MODIFIED_SINCE, dateValue.toString()); + logger.trace("Sending {} Header with value '{}'", HttpHeaders.IF_MODIFIED_SINCE, dateValue); } } @@ -477,8 +466,7 @@ private <R extends Resource> R read(Class<R> resourceType, String id, R oldValue logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), response.getStatusInfo().getReasonPhrase()); if (Status.OK.getStatusCode() == response.getStatus()) - // TODO remove workaround if HAPI bug fixed - return referenceCleaner.cleanReferenceResourcesIfBundle(response.readEntity(resourceType)); + return response.readEntity(resourceType); else if (oldValue != null && oldValue.hasMeta() && (oldValue.getMeta().hasVersionId() || oldValue.getMeta().hasLastUpdated()) && Status.NOT_MODIFIED.getStatusCode() == response.getStatus()) @@ -549,9 +537,7 @@ public Resource read(String resourceTypeName, String id, String version) logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), response.getStatusInfo().getReasonPhrase()); if (Status.OK.getStatusCode() == response.getStatus()) - // TODO remove workaround if HAPI bug fixed - return referenceCleaner.cleanReferenceResourcesIfBundle( - (Resource) response.readEntity(RESOURCE_TYPES_BY_NAME.get(resourceTypeName))); + return (Resource) response.readEntity(RESOURCE_TYPES_BY_NAME.get(resourceTypeName)); else throw handleError(response); } @@ -569,8 +555,7 @@ public <R extends Resource> R read(Class<R> resourceType, String id, String vers logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), response.getStatusInfo().getReasonPhrase()); if (Status.OK.getStatusCode() == response.getStatus()) - // TODO remove workaround if HAPI bug fixed - return referenceCleaner.cleanReferenceResourcesIfBundle(response.readEntity(resourceType)); + return response.readEntity(resourceType); else throw handleError(response); } @@ -656,8 +641,7 @@ public Bundle search(Class<? extends Resource> resourceType, Map<String, List<St logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), response.getStatusInfo().getReasonPhrase()); if (Status.OK.getStatusCode() == response.getStatus()) - // TODO remove workaround if HAPI bug fixed - return referenceCleaner.cleanReferenceResourcesIfBundle(response.readEntity(Bundle.class)); + return response.readEntity(Bundle.class); else throw handleError(response); } @@ -680,8 +664,7 @@ public Bundle searchWithStrictHandling(Class<? extends Resource> resourceType, M logger.debug("HTTP {}: {}", response.getStatusInfo().getStatusCode(), response.getStatusInfo().getReasonPhrase()); if (Status.OK.getStatusCode() == response.getStatus()) - // TODO remove workaround if HAPI bug fixed - return referenceCleaner.cleanReferenceResourcesIfBundle(response.readEntity(Bundle.class)); + return response.readEntity(Bundle.class); else throw handleError(response); } diff --git a/dsf-fhir/dsf-fhir-websocket-client/pom.xml b/dsf-fhir/dsf-fhir-websocket-client/pom.xml index d7f2a5104..95638d7c3 100755 --- a/dsf-fhir/dsf-fhir-websocket-client/pom.xml +++ b/dsf-fhir/dsf-fhir-websocket-client/pom.xml @@ -13,6 +13,7 @@ <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-r4</artifactId> + <version>${hapi.fhir.version}</version> </dependency> <dependency> diff --git a/dsf-tools/dsf-tools-bundle-generator/pom.xml b/dsf-tools/dsf-tools-bundle-generator/pom.xml index 7e5575f19..9c610dab0 100755 --- a/dsf-tools/dsf-tools-bundle-generator/pom.xml +++ b/dsf-tools/dsf-tools-bundle-generator/pom.xml @@ -13,10 +13,12 @@ <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-r4</artifactId> + <version>${hapi.fhir.version}</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-r5</artifactId> + <version>${hapi.fhir.version}</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> @@ -27,14 +29,17 @@ <groupId>commons-logging</groupId> </exclusion> </exclusions> + <version>${hapi.fhir.version}</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-validation-resources-r4</artifactId> + <version>${hapi.fhir.version}</version> </dependency> <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-validation-resources-r5</artifactId> + <version>${hapi.fhir.version}</version> </dependency> <dependency> diff --git a/dsf-tools/dsf-tools-bundle-generator/src/main/java/dev/dsf/tools/generator/BundleGenerator.java b/dsf-tools/dsf-tools-bundle-generator/src/main/java/dev/dsf/tools/generator/BundleGenerator.java index 989a633ba..050ea3520 100755 --- a/dsf-tools/dsf-tools-bundle-generator/src/main/java/dev/dsf/tools/generator/BundleGenerator.java +++ b/dsf-tools/dsf-tools-bundle-generator/src/main/java/dev/dsf/tools/generator/BundleGenerator.java @@ -229,22 +229,14 @@ private ToIntFunction<BundleEntryComponent> getSortCriteria2(List<String> urlsSo private Function<BundleEntryComponent, String> getSortCriteria3() { - return (BundleEntryComponent e) -> + return (BundleEntryComponent e) -> switch (e.getResource()) { - if (e.getResource() == null) - return ""; - else if (e.getResource() instanceof CodeSystem c) - return c.getUrl() + "|" + c.getVersion(); - else if (e.getResource() instanceof NamingSystem n) - return n.getName(); - else if (e.getResource() instanceof ValueSet v) - return v.getUrl() + "|" + v.getVersion(); - else if (e.getResource() instanceof StructureDefinition s) - return s.getUrl() + "|" + s.getVersion(); - else if (e.getResource() instanceof Subscription s) - return s.getReason(); - else - return ""; + case CodeSystem c -> c.getUrl() + "|" + c.getVersion(); + case NamingSystem n -> n.getName(); + case ValueSet v -> v.getUrl() + "|" + v.getVersion(); + case StructureDefinition s -> s.getUrl() + "|" + s.getVersion(); + case Subscription s -> s.getReason(); + default -> ""; }; } diff --git a/dsf-tools/dsf-tools-test-data-generator/pom.xml b/dsf-tools/dsf-tools-test-data-generator/pom.xml index 4e3db6f03..1b9fe1d34 100755 --- a/dsf-tools/dsf-tools-test-data-generator/pom.xml +++ b/dsf-tools/dsf-tools-test-data-generator/pom.xml @@ -31,6 +31,7 @@ <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-r4</artifactId> + <version>${hapi.fhir.version}</version> </dependency> <dependency> diff --git a/dsf-tools/dsf-tools-test-data-generator/src/main/java/dev/dsf/tools/generator/BundleGenerator.java b/dsf-tools/dsf-tools-test-data-generator/src/main/java/dev/dsf/tools/generator/BundleGenerator.java index 041d48a5f..37835d513 100644 --- a/dsf-tools/dsf-tools-test-data-generator/src/main/java/dev/dsf/tools/generator/BundleGenerator.java +++ b/dsf-tools/dsf-tools-test-data-generator/src/main/java/dev/dsf/tools/generator/BundleGenerator.java @@ -19,10 +19,6 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; -import dev.dsf.fhir.service.ReferenceCleaner; -import dev.dsf.fhir.service.ReferenceCleanerImpl; -import dev.dsf.fhir.service.ReferenceExtractor; -import dev.dsf.fhir.service.ReferenceExtractorImpl; import dev.dsf.tools.generator.CertificateGenerator.CertificateFiles; public class BundleGenerator @@ -30,8 +26,6 @@ public class BundleGenerator private static final Logger logger = LoggerFactory.getLogger(BundleGenerator.class); private final FhirContext fhirContext = FhirContext.forR4(); - private final ReferenceExtractor extractor = new ReferenceExtractorImpl(); - private final ReferenceCleaner cleaner = new ReferenceCleanerImpl(extractor); private Bundle testBundle; @@ -39,10 +33,7 @@ private Bundle readAndCleanBundle(Path bundleTemplateFile) { try (InputStream in = Files.newInputStream(bundleTemplateFile)) { - Bundle bundle = newXmlParser().parseResource(Bundle.class, in); - - // FIXME hapi parser can't handle embedded resources and creates them while parsing bundles - return cleaner.cleanReferenceResourcesIfBundle(bundle); + return newXmlParser().parseResource(Bundle.class, in); } catch (IOException e) { diff --git a/pom.xml b/pom.xml index 3bca0e691..44682aa65 100755 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,9 @@ <spring.version>6.1.10</spring.version> <jackson.version>2.17.2</jackson.version> <camunda.version>7.21.0</camunda.version> - <hapi.fhir.version>5.1.0</hapi.fhir.version> + <hapi.fhir.version.v1>5.1.0</hapi.fhir.version.v1> + <hapi.fhir.version.v2>7.4.0</hapi.fhir.version.v2> + <hapi.fhir.version>7.4.0</hapi.fhir.version> <bouncycastle.version>1.78.1</bouncycastle.version> </properties> @@ -356,48 +358,11 @@ <version>0.10.2</version> </dependency> - <!-- FHIR --> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-jaxrsserver-base</artifactId> - <version>${hapi.fhir.version}</version> - </dependency> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-client</artifactId> - <version>${hapi.fhir.version}</version> - </dependency> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-structures-r4</artifactId> - <version>${hapi.fhir.version}</version> - </dependency> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-structures-r5</artifactId> - <version>${hapi.fhir.version}</version> - </dependency> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-validation</artifactId> - <version>${hapi.fhir.version}</version> - </dependency> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-validation-resources-r4</artifactId> - <version>${hapi.fhir.version}</version> - </dependency> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-validation-resources-r5</artifactId> - <version>${hapi.fhir.version}</version> - </dependency> - <!-- managing out-dated dependencies from hapi fhir --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-compress</artifactId> - <version>1.26.2</version> + <version>1.27.1</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> @@ -412,7 +377,7 @@ <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> - <version>32.1.3-jre</version> + <version>33.3.0-jre</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId>