From 86b4026bcfaed73eda1a435f76c23e0c23e95307 Mon Sep 17 00:00:00 2001
From: Paul-Christian Volkmer
Date: Wed, 11 Nov 2026 15:13:46 +0100
Subject: feat: initial support for data(base) migration infrastructure
---
README.md | 11 ++
build.gradle | 1 +
.../dnpm/oshelper/analyzer/MigrationAnalyzer.java | 106 +++++++++++++++++++
.../migration/DatabaseMigrationService.java | 83 +++++++++++++++
.../dnpm/oshelper/migration/MigrationUtils.java | 62 +++++++++++
.../dev/dnpm/oshelper/migration/updatednpm_001.sql | 1 +
.../migration/DatabaseMigrationServiceTest.java | 113 +++++++++++++++++++++
7 files changed, 377 insertions(+)
create mode 100644 src/main/java/dev/dnpm/oshelper/analyzer/MigrationAnalyzer.java
create mode 100644 src/main/java/dev/dnpm/oshelper/migration/DatabaseMigrationService.java
create mode 100644 src/main/java/dev/dnpm/oshelper/migration/MigrationUtils.java
create mode 100644 src/main/resources/dev/dnpm/oshelper/migration/updatednpm_001.sql
create mode 100644 src/test/java/dev/dnpm/oshelper/migration/DatabaseMigrationServiceTest.java
diff --git a/README.md b/README.md
index c25a02e..394b13a 100644
--- a/README.md
+++ b/README.md
@@ -170,6 +170,17 @@ VALUES (
);
```
+## Datenbankmigration
+
+Zur Durchführung von Aktualisierungen in den Datenkatalogtabellen dieses Plugins ist ab Version 2.3.0 eine
+Datenbankmigration enthalten, die nach Aktualisierung des Plugins und Neustart von Onkostar automatisch durchgeführt
+wird.
+
+Dadurch ist es möglich, einige Anpassungen an neue Formularinhalte bzw. für neue Versionen des DNPM-Datenmodells 2.1
+durchzuführen, ohne wie bisher ein Formular zu öffnen und nach Abarbeitung von Formularscripts zu speichern.
+
+Dies ist vor allem zur Vorbereitung auf eine Migration von Onkostar 2.x zu Onkostar 3.8 gedacht.
+
## Berechtigungsprüfung
Dieses Plugin unterstützt eine Berechtigungsprüfung anhand von personenstammbasierten als auch formularbasierten
diff --git a/build.gradle b/build.gradle
index 670af4b..e6a0ba0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -42,6 +42,7 @@ dependencies {
implementation 'javax.xml.bind:jaxb-api:2.3.0'
implementation 'org.apache.commons:commons-csv:1.2'
implementation 'commons-lang:commons-lang:2.6'
+ implementation 'jakarta.annotation:jakarta.annotation-api:1.3.5'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.13.3'
diff --git a/src/main/java/dev/dnpm/oshelper/analyzer/MigrationAnalyzer.java b/src/main/java/dev/dnpm/oshelper/analyzer/MigrationAnalyzer.java
new file mode 100644
index 0000000..3299a58
--- /dev/null
+++ b/src/main/java/dev/dnpm/oshelper/analyzer/MigrationAnalyzer.java
@@ -0,0 +1,106 @@
+/*
+ * This file is part of DNPMHelper
+ *
+ * Copyright (C) 2023-2026 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package dev.dnpm.oshelper.analyzer;
+
+import de.itc.onkostar.api.Disease;
+import de.itc.onkostar.api.IOnkostarApi;
+import de.itc.onkostar.api.Procedure;
+import de.itc.onkostar.api.analysis.AnalyseTriggerEvent;
+import de.itc.onkostar.api.analysis.AnalyzerRequirement;
+import dev.dnpm.oshelper.migration.DatabaseMigrationService;
+import java.util.Set;
+import javax.annotation.PostConstruct;
+import javax.sql.DataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+
+/**
+ * Diese Klasse implementiert einen Analyzer zur Durchführung von Migrationen des Plugins
+ *
+ * @since 2.3.0
+ */
+@Component
+public class MigrationAnalyzer extends Analyzer {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private final IOnkostarApi onkostarApi;
+
+ private final DataSource dataSource;
+
+ private final DatabaseMigrationService databaseMigrationService;
+
+ public MigrationAnalyzer(
+ final IOnkostarApi onkostarApi,
+ final DataSource dataSource,
+ final ResourcePatternResolver resourcePatternResolver) {
+ logger.info("Ctor migration");
+ this.onkostarApi = onkostarApi;
+ this.dataSource = dataSource;
+ this.databaseMigrationService =
+ new DatabaseMigrationService(new JdbcTemplate(dataSource), resourcePatternResolver);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Migration des Plugins";
+ }
+
+ /**
+ * @deprecated
+ */
+ @Override
+ public boolean isRelevantForDeletedProcedure() {
+ return false;
+ }
+
+ @Override
+ public boolean isRelevantForAnalyzer(Procedure procedure, Disease disease) {
+ return null != procedure && procedure.getFormName().startsWith("DNPM");
+ }
+
+ @Override
+ public boolean isSynchronous() {
+ return true;
+ }
+
+ @Override
+ public AnalyzerRequirement getRequirement() {
+ return AnalyzerRequirement.PROCEDURE;
+ }
+
+ @Override
+ public Set getTriggerEvents() {
+ return Set.of(AnalyseTriggerEvent.REORG);
+ }
+
+ @Override
+ public void analyze(Procedure procedure, Disease disease) {
+ this.init();
+ }
+
+ @PostConstruct
+ public void init() {
+ this.databaseMigrationService.migrate();
+ }
+}
diff --git a/src/main/java/dev/dnpm/oshelper/migration/DatabaseMigrationService.java b/src/main/java/dev/dnpm/oshelper/migration/DatabaseMigrationService.java
new file mode 100644
index 0000000..e052286
--- /dev/null
+++ b/src/main/java/dev/dnpm/oshelper/migration/DatabaseMigrationService.java
@@ -0,0 +1,83 @@
+/*
+ * This file is part of DNPMHelper
+ *
+ * Copyright (C) 2023-2026 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package dev.dnpm.oshelper.migration;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.dao.DataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Diese Klasse implementiert die Durchführung einer Datenbankmigration
+ *
+ * @since 2.3.0
+ */
+public class DatabaseMigrationService {
+
+ private final Logger log = LoggerFactory.getLogger(DatabaseMigrationService.class);
+
+ private final JdbcTemplate jdbcTemplate;
+ private final ResourcePatternResolver resourcePatternResolver;
+
+ public DatabaseMigrationService(
+ final JdbcTemplate jdbcTemplate, final ResourcePatternResolver resourcePatternResolver) {
+ this.jdbcTemplate = jdbcTemplate;
+ this.resourcePatternResolver = resourcePatternResolver;
+ }
+
+ /**
+ * Diese Methode führt eine Datenbankmigration durch. Es werden alle Migrationen durchgeführt, die
+ * noch nicht angewendet wurden.
+ */
+ public void migrate() {
+ log.info("Starting database migration");
+ try {
+ for (var migration : this.getNewMigrationIds()) {
+ log.info("Apply database migration: '{}'", migration);
+ try {
+ jdbcTemplate.execute(MigrationUtils.getSqlQuery(resourcePatternResolver, migration));
+ } catch (DataAccessException e) {
+ jdbcTemplate.update(
+ "INSERT INTO update_db (date, update_script, failed, fail_description) VALUES (NOW(), ?, ?, ?)",
+ migration,
+ true,
+ e.getMessage().length() > 1000 ? e.getMessage().substring(0, 1000) : e.getMessage());
+ continue;
+ }
+ jdbcTemplate.update(
+ "INSERT INTO update_db (date, update_script, failed) VALUES (NOW(), ?, ?)",
+ migration,
+ false);
+ }
+ } catch (Exception e) {
+ log.error("Error while migrating database", e);
+ }
+ }
+
+ List getNewMigrationIds() throws Exception {
+ var existingMigrations = MigrationUtils.getExecutedDatabaseMigrations(this.jdbcTemplate);
+ return MigrationUtils.getDatabaseMigrationScripts(this.resourcePatternResolver).stream()
+ .filter(migration -> !existingMigrations.contains(migration))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/dev/dnpm/oshelper/migration/MigrationUtils.java b/src/main/java/dev/dnpm/oshelper/migration/MigrationUtils.java
new file mode 100644
index 0000000..bde2612
--- /dev/null
+++ b/src/main/java/dev/dnpm/oshelper/migration/MigrationUtils.java
@@ -0,0 +1,62 @@
+/*
+ * This file is part of DNPMHelper
+ *
+ * Copyright (C) 2023-2026 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package dev.dnpm.oshelper.migration;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * In dieser Klasse werden Hilfsmethoden zur Durchführung von Migrationen des Plugins implementiert
+ *
+ * @since 2.3.0
+ */
+class MigrationUtils {
+
+ static List getExecutedDatabaseMigrations(JdbcTemplate jdbcTemplate) {
+ return jdbcTemplate.query(
+ "SELECT id FROM update_db WHERE update_script LIKE 'updatednpm_%'",
+ (rs, rowNum) -> rs.getString("update_script"));
+ }
+
+ static List getDatabaseMigrationScripts(ResourcePatternResolver resourcePatternResolver)
+ throws IOException {
+ var resources =
+ List.of(
+ resourcePatternResolver.getResources("classpath:dev/dnpm/oshelper/migration/*.sql"));
+ return resources.stream()
+ .map(Resource::getFilename)
+ .filter(s -> s.startsWith("updatednpm_") && s.endsWith(".sql"))
+ .collect(Collectors.toList());
+ }
+
+ static String getSqlQuery(ResourcePatternResolver resourcePatternResolver, String id)
+ throws IOException {
+ final var sql =
+ resourcePatternResolver
+ .getResource(String.format("classpath:dev/dnpm/oshelper/migration/%s", id))
+ .getInputStream()
+ .readAllBytes();
+ return new String(sql);
+ }
+}
diff --git a/src/main/resources/dev/dnpm/oshelper/migration/updatednpm_001.sql b/src/main/resources/dev/dnpm/oshelper/migration/updatednpm_001.sql
new file mode 100644
index 0000000..44c6edc
--- /dev/null
+++ b/src/main/resources/dev/dnpm/oshelper/migration/updatednpm_001.sql
@@ -0,0 +1 @@
+ALTER TABLE dk_dnpm_uf_einzelempfehlung MODIFY COLUMN studie VARCHAR(255);
\ No newline at end of file
diff --git a/src/test/java/dev/dnpm/oshelper/migration/DatabaseMigrationServiceTest.java b/src/test/java/dev/dnpm/oshelper/migration/DatabaseMigrationServiceTest.java
new file mode 100644
index 0000000..9c22c8d
--- /dev/null
+++ b/src/test/java/dev/dnpm/oshelper/migration/DatabaseMigrationServiceTest.java
@@ -0,0 +1,113 @@
+/*
+ * This file is part of DNPMHelper
+ *
+ * Copyright (C) 2023-2026 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package dev.dnpm.oshelper.migration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+@ExtendWith(MockitoExtension.class)
+class DatabaseMigrationServiceTest {
+
+ JdbcTemplate jdbcTemplate;
+ ResourcePatternResolver resourcePatternResolver;
+
+ DatabaseMigrationService databaseMigration;
+
+ @BeforeEach
+ void setUp(
+ @Mock JdbcTemplate jdbcTemplate, @Mock ResourcePatternResolver resourcePatternResolver) {
+ this.jdbcTemplate = jdbcTemplate;
+ this.resourcePatternResolver = resourcePatternResolver;
+
+ databaseMigration = new DatabaseMigrationService(jdbcTemplate, resourcePatternResolver);
+ }
+
+ @Test
+ void shouldGetNewMigrationFiles() throws Exception {
+ try (MockedStatic databaseUtils = Mockito.mockStatic(MigrationUtils.class)) {
+ databaseUtils
+ .when(
+ () -> MigrationUtils.getDatabaseMigrationScripts(any(ResourcePatternResolver.class)))
+ .thenReturn(List.of("updatednpm_001.sql", "updatednpm_002.sql", "updatednpm_003.sql"));
+
+ databaseUtils
+ .when(() -> MigrationUtils.getExecutedDatabaseMigrations(any(JdbcTemplate.class)))
+ .thenReturn(List.of("updatednpm_001.sql", "updatednpm_002.sql"));
+
+ var actual = databaseMigration.getNewMigrationIds();
+ assertThat(actual).containsExactly("updatednpm_003.sql");
+ }
+ }
+
+ @Test
+ void shouldIgnoreOldMigrationFiles() throws Exception {
+ try (MockedStatic databaseUtils = Mockito.mockStatic(MigrationUtils.class)) {
+ databaseUtils
+ .when(
+ () -> MigrationUtils.getDatabaseMigrationScripts(any(ResourcePatternResolver.class)))
+ .thenReturn(List.of("updatednpm_001.sql", "updatednpm_002.sql"));
+
+ databaseUtils
+ .when(() -> MigrationUtils.getExecutedDatabaseMigrations(any(JdbcTemplate.class)))
+ .thenReturn(List.of("updatednpm_001.sql", "updatednpm_002.sql"));
+
+ var actual = databaseMigration.getNewMigrationIds();
+ assertThat(actual).isEmpty();
+ }
+ }
+
+ @Test
+ void shouldExecuteMigrationFile() throws Exception {
+ try (MockedStatic databaseUtils = Mockito.mockStatic(MigrationUtils.class)) {
+ databaseUtils
+ .when(
+ () -> MigrationUtils.getDatabaseMigrationScripts(any(ResourcePatternResolver.class)))
+ .thenReturn(List.of("updatednpm_001.sql", "updatednpm_002.sql"));
+
+ databaseUtils
+ .when(() -> MigrationUtils.getExecutedDatabaseMigrations(any(JdbcTemplate.class)))
+ .thenReturn(List.of("updatednpm_001.sql"));
+
+ databaseUtils
+ .when(() -> MigrationUtils.getSqlQuery(any(ResourcePatternResolver.class), anyString()))
+ .thenReturn("UPDATE TABLE test;");
+
+ databaseMigration.migrate();
+
+ var argumentCaptor = ArgumentCaptor.forClass(String.class);
+ verify(jdbcTemplate, times(1)).execute(argumentCaptor.capture());
+ assertThat(argumentCaptor.getValue()).isEqualTo("UPDATE TABLE test;");
+ }
+ }
+}
--
cgit v1.2.3