diff --git a/.circleci/config.yml b/.circleci/config.yml index 3b65b72ec..d5509ba07 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1253,6 +1253,8 @@ commands: java -jar "application/target/application-${EHRbase_VERSION}.jar" \ --system.allow-template-overwrite=<< parameters.allow-template-overwrite >> \ --server.nodename=<< parameters.nodename >> \ + --plugin-manager.enable=true \ + --plugin-manager.plugin-dir='tests/test_plugin' \ --cache.enabled=<< parameters.cache-enabled >> > log & app_pid=$! timeout=60 while [ ! -f ./log ]; @@ -1426,7 +1428,7 @@ commands: cd ~/projects EHRbase_VERSION=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive exec:exec) echo ${EHRbase_VERSION} - java -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true > log & + java -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true --plugin-manager.enable=true --plugin-manager.plugin-dir='tests/test_plugin' > log & grep -m 1 "Started EhrBase in" <(tail -f log) cd ~/projects/openEHR_SDK jps @@ -1448,7 +1450,7 @@ commands: echo ${EHRbase_VERSION} cd ~/projects # NOTE: This is where the target folder w/ artifacts were persisted to in previous step. java -javaagent:/home/circleci/jacoco/lib/jacocoagent.jar=output=tcpserver,address=127.0.0.1 \ - -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true > log & + -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true --plugin-manager.enable=true --plugin-manager.plugin-dir='tests/test_plugin' > log & grep -m 1 "Started EhrBase in" <(tail -f log) cd ~/projects/openEHR_SDK jps @@ -1624,7 +1626,7 @@ commands: ls -la EHRbase_VERSION=$(mvn -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive exec:exec) echo ${EHRbase_VERSION} - java -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true > log & + java -jar application/target/application-${EHRbase_VERSION}.jar --cache.enabled=true --plugin-manager.enable=true --plugin-manager.plugin-dir='tests/test_plugin' > log & grep -m 1 "Started EhrBase in" <(tail -f log) jps diff --git a/.gitignore b/.gitignore index 1f447fad7..fd251dbaf 100644 --- a/.gitignore +++ b/.gitignore @@ -217,3 +217,4 @@ vulnerability_analysis.json # Docker .pgdata application/.pgdata +/plugin_dir/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c555624c..7af39c632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,15 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project -adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres +to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [unreleased] ### Added +- Add Plugins system ([#772](https://github.com/ehrbase/ehrbase/pull/772)). + ### Changed ### Fixed @@ -16,7 +18,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Remove unused Operational Template cache ([#759](https://github.com/ehrbase/ehrbase/pull/759)). - Allow update/adding/removal of feeder_audit/links on Composition ([#773](https://github.com/ehrbase/ehrbase/pull/773)) -## [0.19.0] +## [0.19.0] ### Added diff --git a/api/src/main/java/org/ehrbase/api/service/CompositionService.java b/api/src/main/java/org/ehrbase/api/service/CompositionService.java index 50b61860f..6300d54c6 100644 --- a/api/src/main/java/org/ehrbase/api/service/CompositionService.java +++ b/api/src/main/java/org/ehrbase/api/service/CompositionService.java @@ -32,7 +32,7 @@ import java.util.Optional; import java.util.UUID; -public interface CompositionService extends BaseService, VersionedObjectService { +public interface CompositionService extends BaseService, VersionedObjectService { /** * @param compositionId The {@link UUID} of the composition to be returned. * @param version The version to returned. If null return the latest @@ -136,4 +136,8 @@ public interface CompositionService extends BaseService, VersionedObjectService< Optional> getOriginalVersionComposition(UUID versionedObjectUid, int version); Composition buildComposition(String content, CompositionFormat format, String templateId); + + + + } diff --git a/application/src/main/java/org/ehrbase/application/config/plugin/EhrBasePluginManager.java b/application/src/main/java/org/ehrbase/application/config/plugin/EhrBasePluginManager.java new file mode 100644 index 000000000..c75da16d1 --- /dev/null +++ b/application/src/main/java/org/ehrbase/application/config/plugin/EhrBasePluginManager.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022. vitasystems GmbH and Hannover Medical School. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ehrbase.application.config.plugin; + + +import org.pf4j.spring.ExtensionsInjector; +import org.pf4j.spring.SpringPluginManager; +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; + +/** + * @author Stefan Spiska + */ +public class EhrBasePluginManager extends SpringPluginManager { + + public EhrBasePluginManager(PluginManagerProperties properties) { + super(properties.getPluginDir()); + } + + private boolean init = false; + + @Override + public void init() { + // Plugins will be initialised in initPlugins + } + + public void initPlugins() { + + + if (!init) { + + startPlugins(); + + AbstractAutowireCapableBeanFactory beanFactory = + (AbstractAutowireCapableBeanFactory) + getApplicationContext().getAutowireCapableBeanFactory(); + ExtensionsInjector extensionsInjector = new ExtensionsInjector(this, beanFactory); + extensionsInjector.injectExtensions(); + init = true; + } + } +} diff --git a/application/src/main/java/org/ehrbase/application/config/plugin/PluginConfig.java b/application/src/main/java/org/ehrbase/application/config/plugin/PluginConfig.java new file mode 100644 index 000000000..0d6ef9908 --- /dev/null +++ b/application/src/main/java/org/ehrbase/application/config/plugin/PluginConfig.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2022. vitasystems GmbH and Hannover Medical School. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ehrbase.application.config.plugin; + +import static org.ehrbase.plugin.PluginHelper.PLUGIN_MANAGER_PREFIX; + +import java.util.HashMap; +import java.util.Map; +import org.ehrbase.api.exception.InternalServerException; +import org.ehrbase.plugin.EhrBasePlugin; +import org.pf4j.PluginWrapper; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * @author Stefan Spiska + */ +@Configuration +@EnableConfigurationProperties(PluginManagerProperties.class) +@ConditionalOnProperty(prefix = PLUGIN_MANAGER_PREFIX, name = "enable", havingValue = "true") +public class PluginConfig { + + @Bean + public EhrBasePluginManager pluginManager(Environment environment) { + + return new EhrBasePluginManager(getPluginManagerProperties(environment)); + } + // since this is used in a BeanFactoryPostProcessor the PluginManagerProperties must be bound + // manually. + private PluginManagerProperties getPluginManagerProperties(Environment environment) { + return Binder.get(environment).bind(PLUGIN_MANAGER_PREFIX, PluginManagerProperties.class).get(); + } + + /** Register the {@link DispatcherServlet} for all {@link EhrBasePlugin} */ + @Bean + public BeanFactoryPostProcessor beanFactoryPostProcessor( + EhrBasePluginManager pluginManager, Environment environment) { + + PluginManagerProperties pluginManagerProperties = getPluginManagerProperties(environment); + + Map registeredUrl = new HashMap<>(); + + return beanFactory -> { + pluginManager.loadPlugins(); + + pluginManager.getPlugins().stream() + .map(PluginWrapper::getPlugin) + .filter(p -> EhrBasePlugin.class.isAssignableFrom(p.getClass())) + .map(EhrBasePlugin.class::cast) + .forEach(p -> register(beanFactory, pluginManagerProperties, registeredUrl, p)); + }; + } + + /** + * Register the {@link DispatcherServlet} for a {@link EhrBasePlugin} + * + * @param beanFactory + * @param pluginManagerProperties + * @param registeredUrl + * @param p + */ + private void register( + ConfigurableListableBeanFactory beanFactory, + PluginManagerProperties pluginManagerProperties, + Map registeredUrl, + EhrBasePlugin p) { + + String pluginId = p.getWrapper().getPluginId(); + + final String uri = + UriComponentsBuilder.newInstance() + .path(pluginManagerProperties.getPluginContextPath()) + .path(p.getContextPath()) + .path("/*") + .build() + .getPath(); + + // check for duplicate plugin uri + registeredUrl.entrySet().stream() + .filter(e -> e.getValue().equals(uri)) + .findAny() + .ifPresent( + e -> { + throw new InternalServerException( + String.format( + "uri %s for plugin %s already registered by plugin %s", + uri, pluginId, e.getKey())); + }); + + registeredUrl.put(pluginId, uri); + + ServletRegistrationBean bean = + new ServletRegistrationBean<>(p.getDispatcherServlet(), uri); + + bean.setLoadOnStartup(1); + bean.setOrder(1); + bean.setName(pluginId); + beanFactory.initializeBean(bean, pluginId); + beanFactory.autowireBean(bean); + beanFactory.registerSingleton(pluginId, bean); + } + + /** + * Create a Listener for the {@link ServletWebServerInitializedEvent } to initialise the {@link + * org.pf4j.ExtensionPoint} after all {@link DispatcherServlet} have been initialised. + * + * @param pluginManager + * @return + */ + @Bean + ApplicationListener + servletWebServerInitializedEventApplicationListener(EhrBasePluginManager pluginManager) { + + return event -> pluginManager.initPlugins(); + } +} diff --git a/application/src/main/java/org/ehrbase/application/config/plugin/PluginManagerProperties.java b/application/src/main/java/org/ehrbase/application/config/plugin/PluginManagerProperties.java new file mode 100644 index 000000000..f14c97e17 --- /dev/null +++ b/application/src/main/java/org/ehrbase/application/config/plugin/PluginManagerProperties.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022. vitasystems GmbH and Hannover Medical School. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ehrbase.application.config.plugin; + +import static org.ehrbase.plugin.PluginHelper.PLUGIN_MANAGER_PREFIX; + +import java.nio.file.Path; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Stefan Spiska + *

{@link ConfigurationProperties} for {@link EhrBasePluginManager}. + */ +@ConfigurationProperties(prefix = PLUGIN_MANAGER_PREFIX) +public class PluginManagerProperties { + + private Path pluginDir; + private boolean enable; + private String pluginContextPath; + + public void setPluginDir(Path pluginDir) { + this.pluginDir = pluginDir; + } + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public Path getPluginDir() { + return pluginDir; + } + + public void setPluginDir(String pluginDir) { + this.pluginDir = Path.of(pluginDir); + } + + public String getPluginContextPath() { + return pluginContextPath; + } + + public void setPluginContextPath(String pluginContextPath) { + this.pluginContextPath = pluginContextPath; + } +} diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml index 65287778a..6f7aa9940 100644 --- a/application/src/main/resources/application.yml +++ b/application/src/main/resources/application.yml @@ -206,4 +206,10 @@ client: # JavaMelody javamelody: - enabled: false \ No newline at end of file + enabled: false + +# plugin configuration +plugin-manager: + plugin-dir: ./plugin_dir + enable: false + plugin-context-path: /plugin diff --git a/plugin/pom.xml b/plugin/pom.xml new file mode 100644 index 000000000..2f87a3a19 --- /dev/null +++ b/plugin/pom.xml @@ -0,0 +1,51 @@ + + + + server + org.ehrbase.openehr + 0.20.0-SNAPSHOT + + 4.0.0 + + plugin + + + 11 + 11 + + + + + com.nedap.healthcare.archie + openehr-rm + + + org.pf4j + pf4j-spring + 0.7.0 + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework + spring-beans + + + org.springframework + spring-context + + + javax.annotation + javax.annotation-api + + + org.springframework + spring-webmvc + + + + \ No newline at end of file diff --git a/plugin/src/main/java/org/ehrbase/plugin/EhrBasePlugin.java b/plugin/src/main/java/org/ehrbase/plugin/EhrBasePlugin.java new file mode 100644 index 000000000..bca36059a --- /dev/null +++ b/plugin/src/main/java/org/ehrbase/plugin/EhrBasePlugin.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022. vitasystems GmbH and Hannover Medical School. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ehrbase.plugin; + +import org.pf4j.PluginWrapper; +import org.pf4j.spring.SpringPlugin; +import org.springframework.context.ApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * @author Stefan Spiska + */ +public abstract class EhrBasePlugin extends SpringPlugin { + + protected EhrBasePlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Override + protected ApplicationContext createApplicationContext() { + return getDispatcherServlet().getWebApplicationContext(); + } + + public abstract DispatcherServlet getDispatcherServlet(); + + public abstract String getContextPath(); +} diff --git a/plugin/src/main/java/org/ehrbase/plugin/dto/CompositionWithEhrId.java b/plugin/src/main/java/org/ehrbase/plugin/dto/CompositionWithEhrId.java new file mode 100644 index 000000000..7331cff37 --- /dev/null +++ b/plugin/src/main/java/org/ehrbase/plugin/dto/CompositionWithEhrId.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022. vitasystems GmbH and Hannover Medical School. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ehrbase.plugin.dto; + +import com.nedap.archie.rm.composition.Composition; + +import java.util.Objects; +import java.util.UUID; + +/** + * @author Stefan Spiska + */ +public class CompositionWithEhrId { + + private final Composition composition; + private final UUID ehrId; + + public CompositionWithEhrId(Composition composition, UUID ehrId) { + this.composition = composition; + this.ehrId = ehrId; + } + + public Composition getComposition() { + return composition; + } + + public UUID getEhrId() { + return ehrId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CompositionWithEhrId that = (CompositionWithEhrId) o; + return Objects.equals(composition, that.composition) && Objects.equals(ehrId, that.ehrId); + } + + @Override + public int hashCode() { + return Objects.hash(composition, ehrId); + } + + @Override + public String toString() { + return "CompositionMergeInput{" + + "composition=" + composition + + ", ehrId=" + ehrId + + '}'; + } +} diff --git a/plugin/src/main/java/org/ehrbase/plugin/extensionpoints/AbstractCompositionExtensionPoint.java b/plugin/src/main/java/org/ehrbase/plugin/extensionpoints/AbstractCompositionExtensionPoint.java new file mode 100644 index 000000000..6f83289dd --- /dev/null +++ b/plugin/src/main/java/org/ehrbase/plugin/extensionpoints/AbstractCompositionExtensionPoint.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022. vitasystems GmbH and Hannover Medical School. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ehrbase.plugin.extensionpoints; + +import org.ehrbase.plugin.dto.CompositionWithEhrId; + +import java.util.UUID; +import java.util.function.Function; + +/** + * Provides After and Before Interceptors for {@link CompositionExtensionPointInterface} + * + * @author Stefan Spiska + */ +public abstract class AbstractCompositionExtensionPoint + implements CompositionExtensionPointInterface { + + /** + * Called before Composition create + * + * @param input {@link com.nedap.archie.rm.composition.Composition} to be created in ehr with + * ehrId {@link UUID} + * @return input to be given to Composition create + * @see https://specifications.openehr.org/releases/SM/latest/openehr_platform.html#_i_ehr_composition_interface + */ + public CompositionWithEhrId beforeCreation(CompositionWithEhrId input) { + return input; + } + + /** + * Intercept Composition create + * + * @param output {@link UUID} of the created Composition + * @return {@link UUID} of the created Composition + * @see https://specifications.openehr.org/releases/SM/latest/openehr_platform.html#_i_ehr_composition_interface + */ + public UUID afterCreation(UUID output) { + return output; + } + + @Override + public UUID aroundCreation( + CompositionWithEhrId input, Function chain) { + return afterCreation(chain.apply(beforeCreation(input))); + } +} diff --git a/plugin/src/main/java/org/ehrbase/plugin/extensionpoints/CompositionExtensionPointInterface.java b/plugin/src/main/java/org/ehrbase/plugin/extensionpoints/CompositionExtensionPointInterface.java new file mode 100644 index 000000000..579dce2c5 --- /dev/null +++ b/plugin/src/main/java/org/ehrbase/plugin/extensionpoints/CompositionExtensionPointInterface.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022. vitasystems GmbH and Hannover Medical School. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ehrbase.plugin.extensionpoints; + +import org.ehrbase.plugin.dto.CompositionWithEhrId; +import org.pf4j.ExtensionPoint; + +import java.util.UUID; +import java.util.function.Function; + +/** + * Extension Point for Component handling. + * + * @see https://specifications.openehr.org/releases/SM/latest/openehr_platform.html#_i_ehr_composition_interface + * @author Stefan Spiska + */ +public interface CompositionExtensionPointInterface extends ExtensionPoint { + + /** + * Intercept Composition create + * + * @param input {@link com.nedap.archie.rm.composition.Composition} to be created in ehr with + * ehrId {@link UUID} + * @param chain next Extension Point + * @return {@link UUID} of the created Composition + * @see https://specifications.openehr.org/releases/SM/latest/openehr_platform.html#_i_ehr_composition_interface + */ + default UUID aroundCreation( + CompositionWithEhrId input, Function chain) { + return chain.apply(input); + } +} diff --git a/pom.xml b/pom.xml index c2693acc8..d933481f5 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ rest-openehr service test-coverage + plugin @@ -508,6 +509,11 @@ jooq-pg ${project.version} + + org.ehrbase.openehr + plugin + ${project.version} + @@ -565,6 +571,18 @@ + + org.pf4j + pf4j-spring + 0.7.0 + + + org.slf4j + slf4j-log4j12 + + + + org.apache.logging.log4j log4j-api diff --git a/rest-ehr-scape/src/main/java/org/ehrbase/rest/ehrscape/controller/CompositionController.java b/rest-ehr-scape/src/main/java/org/ehrbase/rest/ehrscape/controller/CompositionController.java index 9d689d25f..97742b324 100644 --- a/rest-ehr-scape/src/main/java/org/ehrbase/rest/ehrscape/controller/CompositionController.java +++ b/rest-ehr-scape/src/main/java/org/ehrbase/rest/ehrscape/controller/CompositionController.java @@ -19,10 +19,6 @@ package org.ehrbase.rest.ehrscape.controller; import com.nedap.archie.rm.support.identification.ObjectVersionId; -import java.net.URI; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.ehrbase.api.exception.InternalServerException; import org.ehrbase.api.exception.InvalidApiParameterException; @@ -30,23 +26,15 @@ import org.ehrbase.response.ehrscape.CompositionDto; import org.ehrbase.response.ehrscape.CompositionFormat; import org.ehrbase.response.ehrscape.StructuredString; -import org.ehrbase.rest.ehrscape.responsedata.Action; -import org.ehrbase.rest.ehrscape.responsedata.ActionRestResponseData; -import org.ehrbase.rest.ehrscape.responsedata.CompositionResponseData; -import org.ehrbase.rest.ehrscape.responsedata.CompositionWriteRestResponseData; -import org.ehrbase.rest.ehrscape.responsedata.Meta; -import org.ehrbase.rest.ehrscape.responsedata.RestHref; +import org.ehrbase.rest.ehrscape.responsedata.*; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; @RestController @RequestMapping( @@ -78,7 +66,6 @@ public ResponseEntity createComposition( var composition = compositionService.buildComposition(content, format, templateId); var compositionUuid = compositionService.create(ehrId, composition) - .map(CompositionDto::getUuid) .orElseThrow(() -> new InternalServerException("Failed to create composition")); var responseData = new CompositionWriteRestResponseData(); @@ -144,14 +131,11 @@ public ResponseEntity update(@PathVariable("uid") String var compoObj = compositionService.buildComposition(content, format, templateId); // Actual update - Optional dtoOptional = - compositionService.update(ehrId, objectVersionId, compoObj); + Optional dtoOptional = compositionService.update(ehrId, objectVersionId, compoObj); var compositionVersionUid = dtoOptional .orElseThrow(() -> new InternalServerException("Failed to create composition")) - .getComposition() - .getUid() .toString(); ActionRestResponseData responseData = new ActionRestResponseData(); responseData.setAction(Action.UPDATE); diff --git a/rest-openehr/src/main/java/org/ehrbase/rest/openehr/OpenehrCompositionController.java b/rest-openehr/src/main/java/org/ehrbase/rest/openehr/OpenehrCompositionController.java index 415dfa3d0..0c124ea27 100644 --- a/rest-openehr/src/main/java/org/ehrbase/rest/openehr/OpenehrCompositionController.java +++ b/rest-openehr/src/main/java/org/ehrbase/rest/openehr/OpenehrCompositionController.java @@ -18,19 +18,6 @@ import com.nedap.archie.rm.composition.Composition; import com.nedap.archie.rm.support.identification.ObjectVersionId; -import java.net.URI; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; -import java.util.function.Supplier; -import javax.servlet.http.HttpServletRequest; import org.ehrbase.api.exception.InternalServerException; import org.ehrbase.api.exception.ObjectNotFoundException; import org.ehrbase.api.exception.PreconditionFailedException; @@ -51,17 +38,16 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.*; +import java.util.function.Supplier; /** * Controller for /composition resource as part of the EHR sub-API of the openEHR REST API @@ -106,11 +92,10 @@ public ResponseEntity createComposition( var compoObj = compositionService.buildComposition(composition, compositionFormat, null); - Optional optionalCompositionDto = compositionService.create(ehrId, compoObj); - - var compositionUuid = optionalCompositionDto.orElseThrow(() -> - new InternalServerException("Failed to create composition")) - .getUuid(); + var compositionUuid = + compositionService + .create(ehrId, compoObj) + .orElseThrow(() -> new InternalServerException("Failed to create composition")); var uri = URI.create(this.encodePath( getBaseEnvLinkURL() + "/rest/openehr/v1/ehr/" + ehrId.toString() + "/composition/" @@ -192,15 +177,15 @@ public ResponseEntity updateComposition(String openehrVersion, try { Composition compoObj = compositionService.buildComposition(composition, compositionFormat, null); - // TODO should have EHR as parameter and check for existence as precondition - see EHR-245 (no direct EHR access in this controller) + // TODO should have EHR as parameter and check for existence as precondition - see EHR-245 (no + // direct EHR access in this controller) // ifMatch header has to be tested for correctness already above - Optional dtoOptional = compositionService - .update(ehrId, new ObjectVersionId(ifMatch), compoObj); - - var compositionVersionUid = dtoOptional.orElseThrow(() -> - new InternalServerException("Failed to create composition")) - .getComposition().getUid().toString(); + var compositionVersionUid = + compositionService + .update(ehrId, new ObjectVersionId(ifMatch), compoObj) + .orElseThrow(() -> new InternalServerException("Failed to create composition")) + .toString(); var uri = URI.create(this.encodePath( getBaseEnvLinkURL() + "/rest/openehr/v1/ehr/" + ehrId.toString() + "/composition/" diff --git a/service/pom.xml b/service/pom.xml index 9ea77f67b..437bb34fb 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -48,6 +48,10 @@ org.springframework spring-tx + + org.springframework.boot + spring-boot-starter-aop + org.springframework.boot spring-boot-starter-security @@ -80,11 +84,19 @@ org.ehrbase.openehr.sdk web-template + + org.ehrbase.openehr + plugin + + + org.pf4j + pf4j-spring + 0.7.0 + org.postgresql postgresql - org.jooq jooq diff --git a/service/src/main/java/org/ehrbase/ServiceModuleConfiguration.java b/service/src/main/java/org/ehrbase/ServiceModuleConfiguration.java index 39154ee47..dd4385818 100644 --- a/service/src/main/java/org/ehrbase/ServiceModuleConfiguration.java +++ b/service/src/main/java/org/ehrbase/ServiceModuleConfiguration.java @@ -20,8 +20,9 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration -@ComponentScan(basePackages = {"org.ehrbase.service"}) -public class ServiceModuleConfiguration { -} +@ComponentScan(basePackages = {"org.ehrbase.service", "org.ehrbase.plugin"}) +@EnableAspectJAutoProxy +public class ServiceModuleConfiguration {} diff --git a/service/src/main/java/org/ehrbase/plugin/PluginAspect.java b/service/src/main/java/org/ehrbase/plugin/PluginAspect.java new file mode 100644 index 000000000..ae4ef42db --- /dev/null +++ b/service/src/main/java/org/ehrbase/plugin/PluginAspect.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2022. vitasystems GmbH and Hannover Medical School. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ehrbase.plugin; + +import static org.ehrbase.plugin.PluginHelper.PLUGIN_MANAGER_PREFIX; + +import com.nedap.archie.rm.composition.Composition; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.ehrbase.api.exception.InternalServerException; +import org.ehrbase.plugin.dto.CompositionWithEhrId; +import org.ehrbase.plugin.extensionpoints.CompositionExtensionPointInterface; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.stereotype.Component; + +/** + * @author Stefan Spiska + */ +@Component +@Aspect +@ConditionalOnProperty(prefix = PLUGIN_MANAGER_PREFIX, name = "enable", havingValue = "true") +public class PluginAspect { + + public static final Comparator> + EXTENSION_POINTS_COMPARATOR = + // respect @Order + ((Comparator>) + (e1, e2) -> + AnnotationAwareOrderComparator.INSTANCE.compare(e1.getValue(), e2.getValue())) + .reversed() + // ensure constant ordering + .thenComparing(Map.Entry::getKey); + + + private static class Chain { + + T current; + Chain next; + } + + private final ListableBeanFactory beanFactory; + + public PluginAspect(ListableBeanFactory beanFactory) { + + this.beanFactory = beanFactory; + } + + /** + * Handle Extension-points for Composition create + * + * @param pjp + * @return + * @see https://specifications.openehr.org/releases/SM/latest/openehr_platform.html#_i_ehr_composition_interface + */ + @Around("execution(* org.ehrbase.api.service.CompositionService.create(..))") + public Object aroundCreateComposition(ProceedingJoinPoint pjp) { + + Chain chain = + buildChain( + getCompositionExtensionPointInterfaceList(), + new CompositionExtensionPointInterface() {}); + Object[] args = pjp.getArgs(); + CompositionWithEhrId input = new CompositionWithEhrId((Composition) args[1], (UUID) args[0]); + + return Optional.of( + handleChain( + chain, + l -> (l::aroundCreation), + input, + i -> { + args[1] = i.getComposition(); + args[0] = i.getEhrId(); + + return ((Optional) proceed(pjp, args)).orElseThrow(); + })); + } + + /** + * Proceed with Error handling. + * + * @param pjp + * @param args + * @return + */ + private Object proceed(ProceedingJoinPoint pjp, Object[] args) { + try { + return pjp.proceed(args); + } catch (RuntimeException e) { + // Simple rethrow to handle in Controller layer + throw e; + } catch (Exception e) { + // should never happen + throw new InternalServerException("Expedition in Plugin Aspect ", e); + } catch (Throwable e) { + // should never happen + throw new InternalServerException(e.getMessage()); + } + } + + /** + * @return Order List of {@link CompositionExtensionPointInterface} in Context. + */ + private List getCompositionExtensionPointInterfaceList() { + + return beanFactory.getBeansOfType(CompositionExtensionPointInterface.class).entrySet().stream() + .sorted(EXTENSION_POINTS_COMPARATOR) + .map(Map.Entry::getValue) + .collect(Collectors.toList()); + } + + /** + * Convert List of Extension-points to chain. + * + * @param extensionPointInterfaceList + * @param identity Extension-point which represents Identity. + * @param Class of the Extension-point + * @return + */ + private Chain buildChain(Collection extensionPointInterfaceList, T identity) { + Chain chain = new Chain<>(); + // Add fist dummy Extension-point so the code path is the same weather there are + // Extension-points or not. + chain.current = identity; + Chain first = chain; + + for (T point : extensionPointInterfaceList) { + Chain next = new Chain<>(); + next.current = point; + chain.next = next; + chain = next; + } + return first; + } + + /** + * Execute chain of responsibility + * + * @param chain Fist chain + * @param around Get the around Listener from Extension-point + * @param input Initial Input + * @param compositionFunction The intercepted Funktion + * @param Input of the intercepted Funktion + * @param Output of the intercepted Funktion + * @param Class of the Extension-point + * @return output after all Extension-points have been handelt + */ + private R handleChain( + Chain chain, + Function, R>> around, + X input, + Function compositionFunction) { + + if (chain.next != null) { + return handleChain( + chain.next, + around, + input, + i -> around.apply(chain.current).apply(i, compositionFunction)); + } else { + return around.apply(chain.current).apply(input, compositionFunction); + } + } +} diff --git a/service/src/main/java/org/ehrbase/plugin/PluginHelper.java b/service/src/main/java/org/ehrbase/plugin/PluginHelper.java new file mode 100644 index 000000000..9a0de5d49 --- /dev/null +++ b/service/src/main/java/org/ehrbase/plugin/PluginHelper.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022. vitasystems GmbH and Hannover Medical School. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ehrbase.plugin; + +/** + * @author Stefan Spiska + */ +public class PluginHelper { + + private PluginHelper() { + // Util class + } + + public static final String PLUGIN_MANAGER_PREFIX = "plugin-manager"; +} diff --git a/service/src/main/java/org/ehrbase/service/CompositionServiceImp.java b/service/src/main/java/org/ehrbase/service/CompositionServiceImp.java index e4a194e38..eda03b7c4 100644 --- a/service/src/main/java/org/ehrbase/service/CompositionServiceImp.java +++ b/service/src/main/java/org/ehrbase/service/CompositionServiceImp.java @@ -29,22 +29,8 @@ import com.nedap.archie.rm.support.identification.HierObjectId; import com.nedap.archie.rm.support.identification.ObjectRef; import com.nedap.archie.rm.support.identification.ObjectVersionId; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; import org.ehrbase.api.definitions.ServerConfig; -import org.ehrbase.api.exception.InternalServerException; -import org.ehrbase.api.exception.InvalidApiParameterException; -import org.ehrbase.api.exception.ObjectNotFoundException; -import org.ehrbase.api.exception.UnexpectedSwitchCaseException; -import org.ehrbase.api.exception.UnprocessableEntityException; -import org.ehrbase.api.exception.ValidationException; +import org.ehrbase.api.exception.*; import org.ehrbase.api.service.CompositionService; import org.ehrbase.api.service.EhrService; import org.ehrbase.api.service.ValidationService; @@ -71,6 +57,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.*; + /** * {@link CompositionService} implementation. * @@ -89,7 +80,8 @@ public class CompositionServiceImp extends BaseServiceImp implements Composition private final KnowledgeCacheService knowledgeCacheService; private final EhrService ehrService; - public CompositionServiceImp(KnowledgeCacheService knowledgeCacheService, + public CompositionServiceImp( + KnowledgeCacheService knowledgeCacheService, ValidationService validationService, EhrService ehrService, DSLContext context, @@ -102,43 +94,44 @@ public CompositionServiceImp(KnowledgeCacheService knowledgeCacheService, } @Override - public Optional create( + public Optional create( UUID ehrId, Composition objData, UUID systemId, UUID committerId, String description) { - UUID compositionId = internalCreate(ehrId, objData, systemId, committerId, description, null); - return getCompositionDto(I_CompositionAccess.retrieveInstance(getDataAccess(), compositionId)); + UUID compositionId = createInternal(ehrId, objData, systemId, committerId, description, null); + return Optional.of(compositionId); } @Override - public Optional create(UUID ehrId, Composition objData, UUID contribution) { - UUID compositionId = internalCreate(ehrId, objData, null, null, null, contribution); - return getCompositionDto(I_CompositionAccess.retrieveInstance(getDataAccess(), compositionId)); + public Optional create(UUID ehrId, Composition objData, UUID contribution) { + UUID compositionId = createInternal(ehrId, objData, null, null, null, contribution); + return Optional.of(compositionId); } @Override - public Optional create(UUID ehrId, Composition objData) { + public Optional create(UUID ehrId, Composition objData) { return create(ehrId, objData, getSystemUuid(), getUserUuid(), null); } /** * Creation of a new composition. With optional custom contribution, or one will be created. * - * @param ehrId ID of EHR - * @param composition RMObject instance of the given Composition to be created - * @param systemId Audit system; or NULL if contribution is given - * @param committerId Audit committer; or NULL if contribution is given - * @param description (Optional) Audit description; or NULL if contribution is given + * @param ehrId ID of EHR + * @param composition RMObject instance of the given Composition to be created + * @param systemId Audit system; or NULL if contribution is given + * @param committerId Audit committer; or NULL if contribution is given + * @param description (Optional) Audit description; or NULL if contribution is given * @param contributionId NULL if is not needed, or ID of given custom contribution * @return ID of created composition * @throws InternalServerException when creation failed */ - private UUID internalCreate( + private UUID createInternal( UUID ehrId, Composition composition, UUID systemId, UUID committerId, String description, UUID contributionId) { + // pre-step: validate try { validationService.check(composition); @@ -196,7 +189,7 @@ private UUID internalCreate( } @Override - public Optional update( + public Optional update( UUID ehrId, ObjectVersionId targetObjId, Composition objData, @@ -212,13 +205,12 @@ public Optional update( committerId, description, null); - return getCompositionDto( - I_CompositionAccess.retrieveInstance( - getDataAccess(), UUID.fromString(compoId.getObjectId().getValue()))); + + return Optional.of(UUID.fromString(compoId.getObjectId().getValue())); } @Override - public Optional update( + public Optional update( UUID ehrId, ObjectVersionId targetObjId, Composition objData, UUID contribution) { var compoId = @@ -229,14 +221,11 @@ public Optional update( null, null, contribution); - return getCompositionDto( - I_CompositionAccess.retrieveInstance( - getDataAccess(), UUID.fromString(compoId.getObjectId().getValue()))); + return Optional.of(UUID.fromString(compoId.getObjectId().getValue())); } @Override - public Optional update( - UUID ehrId, ObjectVersionId targetObjId, Composition objData) { + public Optional update(UUID ehrId, ObjectVersionId targetObjId, Composition objData) { return update(ehrId, targetObjId, objData, getSystemUuid(), getUserUuid(), null); } diff --git a/tests/test_plugin/simple-plugin-1.0-SNAPSHOT.jar b/tests/test_plugin/simple-plugin-1.0-SNAPSHOT.jar new file mode 100644 index 000000000..fa171180c Binary files /dev/null and b/tests/test_plugin/simple-plugin-1.0-SNAPSHOT.jar differ diff --git a/tests/test_plugin/web-plugin-1.0-SNAPSHOT.jar b/tests/test_plugin/web-plugin-1.0-SNAPSHOT.jar new file mode 100644 index 000000000..531adc79f Binary files /dev/null and b/tests/test_plugin/web-plugin-1.0-SNAPSHOT.jar differ