From adf91f40a12733d6ea3275ab686ab3d9c79580b4 Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Sun, 6 Mar 2022 21:50:30 +0100
Subject: [PATCH] #663 - migrator: initial setup

---
 BudgetMasterDatabaseMigrator/pom.xml          | 39 +++++++++
 .../databasemigrator/BatchConfiguration.java  | 60 +++++++++++++
 .../DatabaseMigratorMain.java                 | 13 +++
 .../databasemigrator/SchedulerConfig.java     | 50 +++++++++++
 .../DestinationDatabaseConfiguration.java     | 50 +++++++++++
 .../category/DestinationCategory.java         | 85 +++++++++++++++++++
 .../DestinationCategoryRepository.java        |  7 ++
 .../source/SourceDatabaseConfiguration.java   | 53 ++++++++++++
 .../source/category/CategoryType.java         |  6 ++
 .../source/category/SourceCategory.java       | 82 ++++++++++++++++++
 .../category/SourceCategoryRepository.java    |  7 ++
 .../steps/category/CategoryChunkListener.java | 36 ++++++++
 .../steps/category/CategoryProcessor.java     | 26 ++++++
 .../steps/category/CategoryReader.java        | 50 +++++++++++
 .../steps/category/CategoryStepListener.java  | 27 ++++++
 .../steps/category/CategoryWriter.java        | 35 ++++++++
 .../src/main/resources/application.properties | 19 +++++
 BudgetMasterServer/pom.xml                    |  7 --
 pom.xml                                       |  1 +
 19 files changed, 646 insertions(+), 7 deletions(-)
 create mode 100644 BudgetMasterDatabaseMigrator/pom.xml
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/BatchConfiguration.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/DatabaseMigratorMain.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/SchedulerConfig.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/DestinationDatabaseConfiguration.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/category/DestinationCategory.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/category/DestinationCategoryRepository.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/SourceDatabaseConfiguration.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/CategoryType.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/SourceCategory.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/SourceCategoryRepository.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryChunkListener.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryProcessor.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryReader.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryStepListener.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryWriter.java
 create mode 100644 BudgetMasterDatabaseMigrator/src/main/resources/application.properties

diff --git a/BudgetMasterDatabaseMigrator/pom.xml b/BudgetMasterDatabaseMigrator/pom.xml
new file mode 100644
index 000000000..9e084b805
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>BudgetMaster</artifactId>
+        <groupId>de.deadlocker8</groupId>
+        <version>2.10.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>BudgetMasterDatabaseMigrator</artifactId>
+
+    <properties>
+        <h2database.version>1.4.199</h2database.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-batch</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <version>${h2database.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/BatchConfiguration.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/BatchConfiguration.java
new file mode 100644
index 000000000..81ec491cc
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/BatchConfiguration.java
@@ -0,0 +1,60 @@
+package de.deadlocker8.budgetmaster.databasemigrator;
+
+import de.deadlocker8.budgetmaster.databasemigrator.destination.category.DestinationCategory;
+import de.deadlocker8.budgetmaster.databasemigrator.source.category.SourceCategory;
+import de.deadlocker8.budgetmaster.databasemigrator.steps.category.*;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.Step;
+import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
+import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
+import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
+import org.springframework.batch.core.explore.JobExplorer;
+import org.springframework.batch.core.launch.support.RunIdIncrementer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableBatchProcessing
+public class BatchConfiguration
+{
+	final JobBuilderFactory jobBuilderFactory;
+	final StepBuilderFactory stepBuilderFactory;
+	final JobExplorer jobExplorer;
+
+	final CategoryReader categoryReader;
+	final CategoryWriter categoryWriter;
+	final CategoryProcessor categoryProcessor;
+
+	public BatchConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, JobExplorer jobExplorer, CategoryReader categoryReader, CategoryWriter categoryWriter, CategoryProcessor categoryProcessor)
+	{
+		this.jobBuilderFactory = jobBuilderFactory;
+		this.stepBuilderFactory = stepBuilderFactory;
+		this.jobExplorer = jobExplorer;
+		this.categoryReader = categoryReader;
+		this.categoryWriter = categoryWriter;
+		this.categoryProcessor = categoryProcessor;
+	}
+
+	@Bean
+	public Job createJob()
+	{
+		return jobBuilderFactory.get("Migrate from h2 to postgresql")
+				.incrementer(new RunIdIncrementer())
+				.flow(createStepForCategoryMigration())
+				.end()
+				.build();
+	}
+
+	@Bean
+	public Step createStepForCategoryMigration()
+	{
+		return stepBuilderFactory.get("Migrate categories")
+				.<SourceCategory, DestinationCategory>chunk(1)
+				.reader(categoryReader)
+				.processor(categoryProcessor)
+				.writer(categoryWriter)
+				.listener(new CategoryChunkListener())
+				.listener(new CategoryStepListener())
+				.build();
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/DatabaseMigratorMain.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/DatabaseMigratorMain.java
new file mode 100644
index 000000000..f176618ca
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/DatabaseMigratorMain.java
@@ -0,0 +1,13 @@
+package de.deadlocker8.budgetmaster.databasemigrator;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class DatabaseMigratorMain
+{
+	public static void main(String[] args)
+	{
+		SpringApplication.run(DatabaseMigratorMain.class, args);
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/SchedulerConfig.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/SchedulerConfig.java
new file mode 100644
index 000000000..c40ba71b3
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/SchedulerConfig.java
@@ -0,0 +1,50 @@
+package de.deadlocker8.budgetmaster.databasemigrator;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.batch.core.*;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.batch.core.launch.JobOperator;
+import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
+import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
+import org.springframework.batch.core.repository.JobRestartException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+@Configuration
+@EnableScheduling
+public class SchedulerConfig
+{
+	private static final Logger LOGGER = LoggerFactory.getLogger(SchedulerConfig.class);
+
+	final JobLauncher jobLauncher;
+	final JobOperator jobOperator;
+	final Job job;
+
+	@Autowired
+	public SchedulerConfig(JobLauncher jobLauncher, JobOperator jobOperator, Job job)
+	{
+		this.jobLauncher = jobLauncher;
+		this.jobOperator = jobOperator;
+		this.job = job;
+	}
+
+	@Scheduled(fixedDelay = Long.MAX_VALUE, initialDelay = 2000)
+	public void scheduleByFixedRate() throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException
+	{
+		LOGGER.info("Starting migration...");
+		final JobParameters jobParameters = new JobParametersBuilder()
+				.addString("time", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
+				.toJobParameters();
+		final JobExecution execution = jobLauncher.run(job, jobParameters);
+
+		LOGGER.info("Migration DONE");
+
+		System.exit(0);
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/DestinationDatabaseConfiguration.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/DestinationDatabaseConfiguration.java
new file mode 100644
index 000000000..877baddad
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/DestinationDatabaseConfiguration.java
@@ -0,0 +1,50 @@
+package de.deadlocker8.budgetmaster.databasemigrator.destination;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.jdbc.DataSourceBuilder;
+import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.persistence.EntityManagerFactory;
+import javax.sql.DataSource;
+
+@Configuration
+@EnableTransactionManagement
+@EnableJpaRepositories(
+		entityManagerFactoryRef = "secondaryEntityManagerFactory",
+		transactionManagerRef = "secondaryTransactionManager",
+		basePackages = {"de.deadlocker8.budgetmaster.databasemigrator.destination"}
+)
+public class DestinationDatabaseConfiguration
+{
+	@Bean(name = "secondaryDataSource")
+	@ConfigurationProperties(prefix = "spring.seconddatasource")
+	public DataSource secondaryDataSource()
+	{
+		return DataSourceBuilder.create().build();
+	}
+
+	@Bean(name = "secondaryEntityManagerFactory")
+	public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(EntityManagerFactoryBuilder builder,
+																				@Qualifier("secondaryDataSource") DataSource secondaryDataSource)
+	{
+		return builder
+				.dataSource(secondaryDataSource)
+				.packages("de.deadlocker8.budgetmaster.databasemigrator.destination")
+				.build();
+	}
+
+	@Bean(name = "secondaryTransactionManager")
+	public PlatformTransactionManager secondaryTransactionManager(
+			@Qualifier("secondaryEntityManagerFactory") EntityManagerFactory secondaryEntityManagerFactory)
+	{
+		return new JpaTransactionManager(secondaryEntityManagerFactory);
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/category/DestinationCategory.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/category/DestinationCategory.java
new file mode 100644
index 000000000..5fbe5932b
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/category/DestinationCategory.java
@@ -0,0 +1,85 @@
+package de.deadlocker8.budgetmaster.databasemigrator.destination.category;
+
+
+
+import de.deadlocker8.budgetmaster.databasemigrator.source.category.CategoryType;
+
+import javax.persistence.*;
+
+@Entity
+@Table(name = "category")
+public class DestinationCategory
+{
+	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
+	private Integer ID;
+
+	private String name;
+
+	private String color;
+
+	private CategoryType type;
+
+	public DestinationCategory()
+	{
+	}
+
+	public DestinationCategory(Integer ID, String name, String color, CategoryType type)
+	{
+		this.ID = ID;
+		this.name = name;
+		this.color = color;
+		this.type = type;
+	}
+
+	public Integer getID()
+	{
+		return ID;
+	}
+
+	public void setID(Integer ID)
+	{
+		this.ID = ID;
+	}
+
+	public String getName()
+	{
+		return name;
+	}
+
+	public void setName(String name)
+	{
+		this.name = name;
+	}
+
+	public String getColor()
+	{
+		return color;
+	}
+
+	public void setColor(String color)
+	{
+		this.color = color;
+	}
+
+	public CategoryType getType()
+	{
+		return type;
+	}
+
+	public void setType(CategoryType type)
+	{
+		this.type = type;
+	}
+
+	@Override
+	public String toString()
+	{
+		return "SourceCategory{" +
+				"ID=" + ID +
+				", name='" + name + '\'' +
+				", color='" + color + '\'' +
+				", type=" + type +
+				'}';
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/category/DestinationCategoryRepository.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/category/DestinationCategoryRepository.java
new file mode 100644
index 000000000..378d2b28f
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/destination/category/DestinationCategoryRepository.java
@@ -0,0 +1,7 @@
+package de.deadlocker8.budgetmaster.databasemigrator.destination.category;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface DestinationCategoryRepository extends JpaRepository<DestinationCategory, Integer>
+{
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/SourceDatabaseConfiguration.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/SourceDatabaseConfiguration.java
new file mode 100644
index 000000000..0e21a5a8d
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/SourceDatabaseConfiguration.java
@@ -0,0 +1,53 @@
+package de.deadlocker8.budgetmaster.databasemigrator.source;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.jdbc.DataSourceBuilder;
+import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.persistence.EntityManagerFactory;
+import javax.sql.DataSource;
+
+@Configuration
+@EnableTransactionManagement
+@EnableJpaRepositories(
+		entityManagerFactoryRef = "primaryEntityManagerFactory",
+		transactionManagerRef = "primaryTransactionManager",
+		basePackages = {"de.deadlocker8.budgetmaster.databasemigrator.source"}
+)
+public class SourceDatabaseConfiguration
+{
+	@Bean(name = "primaryDataSource")
+	@Primary
+	@ConfigurationProperties(prefix = "spring.datasource")
+	public DataSource primaryDataSource()
+	{
+		return DataSourceBuilder.create().build();
+	}
+
+	@Primary
+	@Bean(name = "primaryEntityManagerFactory")
+	public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(EntityManagerFactoryBuilder builder,
+																			  @Qualifier("primaryDataSource") DataSource primaryDataSource)
+	{
+		return builder
+				.dataSource(primaryDataSource)
+				.packages("de.deadlocker8.budgetmaster.databasemigrator.source")
+				.build();
+	}
+
+	@Bean(name = "primaryTransactionManager")
+	public PlatformTransactionManager primaryTransactionManager(
+			@Qualifier("primaryEntityManagerFactory") EntityManagerFactory primaryEntityManagerFactory)
+	{
+		return new JpaTransactionManager(primaryEntityManagerFactory);
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/CategoryType.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/CategoryType.java
new file mode 100644
index 000000000..32072d0ca
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/CategoryType.java
@@ -0,0 +1,6 @@
+package de.deadlocker8.budgetmaster.databasemigrator.source.category;
+
+public enum CategoryType
+{
+	NONE, REST, CUSTOM
+}
\ No newline at end of file
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/SourceCategory.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/SourceCategory.java
new file mode 100644
index 000000000..d3d14ae35
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/SourceCategory.java
@@ -0,0 +1,82 @@
+package de.deadlocker8.budgetmaster.databasemigrator.source.category;
+
+
+import javax.persistence.*;
+
+@Entity
+@Table(name = "category")
+public class SourceCategory
+{
+	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
+	private Integer ID;
+
+	private String name;
+
+	private String color;
+
+	private CategoryType type;
+
+	public SourceCategory()
+	{
+	}
+
+	public SourceCategory(Integer ID, String name, String color, CategoryType type)
+	{
+		this.ID = ID;
+		this.name = name;
+		this.color = color;
+		this.type = type;
+	}
+
+	public Integer getID()
+	{
+		return ID;
+	}
+
+	public void setID(Integer ID)
+	{
+		this.ID = ID;
+	}
+
+	public String getName()
+	{
+		return name;
+	}
+
+	public void setName(String name)
+	{
+		this.name = name;
+	}
+
+	public String getColor()
+	{
+		return color;
+	}
+
+	public void setColor(String color)
+	{
+		this.color = color;
+	}
+
+	public CategoryType getType()
+	{
+		return type;
+	}
+
+	public void setType(CategoryType type)
+	{
+		this.type = type;
+	}
+
+	@Override
+	public String toString()
+	{
+		return "SourceCategory{" +
+				"ID=" + ID +
+				", name='" + name + '\'' +
+				", color='" + color + '\'' +
+				", type=" + type +
+				'}';
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/SourceCategoryRepository.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/SourceCategoryRepository.java
new file mode 100644
index 000000000..395092110
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/source/category/SourceCategoryRepository.java
@@ -0,0 +1,7 @@
+package de.deadlocker8.budgetmaster.databasemigrator.source.category;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface SourceCategoryRepository extends JpaRepository<SourceCategory, Integer>
+{
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryChunkListener.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryChunkListener.java
new file mode 100644
index 000000000..af670c2fe
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryChunkListener.java
@@ -0,0 +1,36 @@
+package de.deadlocker8.budgetmaster.databasemigrator.steps.category;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.batch.core.ChunkListener;
+import org.springframework.batch.core.scope.context.ChunkContext;
+
+public class CategoryChunkListener implements ChunkListener
+{
+	private static final Logger LOGGER = LoggerFactory.getLogger(CategoryChunkListener.class);
+
+	private int numberOfProcessedItems = 0;
+
+	@Override
+	public void beforeChunk(ChunkContext context)
+	{
+		// nothing to do
+	}
+
+	@Override
+	public void afterChunk(ChunkContext context)
+	{
+		final int count = context.getStepContext().getStepExecution().getReadCount();
+		if(count > numberOfProcessedItems)
+		{
+			numberOfProcessedItems++;
+			LOGGER.info("Migrating category {}", count);
+		}
+	}
+
+	@Override
+	public void afterChunkError(ChunkContext context)
+	{
+		// nothing to do
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryProcessor.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryProcessor.java
new file mode 100644
index 000000000..0ecc2fd5d
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryProcessor.java
@@ -0,0 +1,26 @@
+package de.deadlocker8.budgetmaster.databasemigrator.steps.category;
+
+import de.deadlocker8.budgetmaster.databasemigrator.destination.category.DestinationCategory;
+import de.deadlocker8.budgetmaster.databasemigrator.source.category.SourceCategory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.batch.item.ItemProcessor;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CategoryProcessor implements ItemProcessor<SourceCategory, DestinationCategory>
+{
+	private static final Logger LOGGER = LoggerFactory.getLogger(CategoryProcessor.class);
+
+	@Override
+	public DestinationCategory process(SourceCategory category)
+	{
+		LOGGER.debug("CategoryProcessor: Processing category: {}", category);
+
+		final DestinationCategory destinationCategory = new DestinationCategory();
+		destinationCategory.setName(category.getName());
+		destinationCategory.setColor(category.getColor());
+		destinationCategory.setType(category.getType());
+		return destinationCategory;
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryReader.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryReader.java
new file mode 100644
index 000000000..d66fce1a6
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryReader.java
@@ -0,0 +1,50 @@
+package de.deadlocker8.budgetmaster.databasemigrator.steps.category;
+
+import de.deadlocker8.budgetmaster.databasemigrator.source.category.CategoryType;
+import de.deadlocker8.budgetmaster.databasemigrator.source.category.SourceCategory;
+import org.springframework.batch.item.ItemReader;
+import org.springframework.batch.item.database.JdbcCursorItemReader;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Component;
+
+import javax.sql.DataSource;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.text.MessageFormat;
+
+@Component
+public class CategoryReader extends JdbcCursorItemReader<SourceCategory> implements ItemReader<SourceCategory>
+{
+	private static final String TABLE_NAME = "category";
+
+	private static class DatabaseColumns
+	{
+		public static final String ID = "ID";
+		public static final String NAME = "NAME";
+		public static final String COLOR = "COLOR";
+		public static final String TYPE = "TYPE";
+	}
+
+	public CategoryReader(@Autowired DataSource primaryDataSource)
+	{
+		setDataSource(primaryDataSource);
+		setSql(MessageFormat.format("SELECT * FROM {0}", TABLE_NAME));
+		setFetchSize(100);
+		setRowMapper(new CategoryRowMapper());
+	}
+
+	public static class CategoryRowMapper implements RowMapper<SourceCategory>
+	{
+		@Override
+		public SourceCategory mapRow(ResultSet rs, int rowNum) throws SQLException
+		{
+			final SourceCategory category = new SourceCategory();
+			category.setID(rs.getInt(DatabaseColumns.ID));
+			category.setName(rs.getString(DatabaseColumns.NAME));
+			category.setColor(rs.getString(DatabaseColumns.COLOR));
+			category.setType(CategoryType.values()[rs.getInt(DatabaseColumns.TYPE)]);
+			return category;
+		}
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryStepListener.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryStepListener.java
new file mode 100644
index 000000000..cbf684459
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryStepListener.java
@@ -0,0 +1,27 @@
+package de.deadlocker8.budgetmaster.databasemigrator.steps.category;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.StepExecutionListener;
+
+public class CategoryStepListener implements StepExecutionListener
+{
+	private static final Logger LOGGER = LoggerFactory.getLogger(CategoryStepListener.class);
+
+	@Override
+	public void beforeStep(StepExecution stepExecution)
+	{
+		LOGGER.info("\n");
+		LOGGER.info(">>> Migrate categories...");
+	}
+
+	@Override
+	public ExitStatus afterStep(StepExecution stepExecution)
+	{
+		final int count = stepExecution.getReadCount();
+		LOGGER.info(">>> Successfully migrated {} categories\n", count);
+		return null;
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryWriter.java b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryWriter.java
new file mode 100644
index 000000000..2ac67a699
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/java/de/deadlocker8/budgetmaster/databasemigrator/steps/category/CategoryWriter.java
@@ -0,0 +1,35 @@
+package de.deadlocker8.budgetmaster.databasemigrator.steps.category;
+
+import de.deadlocker8.budgetmaster.databasemigrator.destination.category.DestinationCategory;
+import de.deadlocker8.budgetmaster.databasemigrator.destination.category.DestinationCategoryRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.batch.item.ItemWriter;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.List;
+
+@Component
+public class CategoryWriter implements ItemWriter<DestinationCategory>
+{
+	private static final Logger LOGGER = LoggerFactory.getLogger(CategoryWriter.class);
+
+	final DestinationCategoryRepository destinationCategoryRepository;
+
+	public CategoryWriter(DestinationCategoryRepository destinationCategoryRepository)
+	{
+		this.destinationCategoryRepository = destinationCategoryRepository;
+	}
+
+	@Override
+	public void write(List<? extends DestinationCategory> list) throws Exception
+	{
+		for(DestinationCategory data : list)
+		{
+			LOGGER.debug("CategoryWriter: Writing category: {}", data);
+			destinationCategoryRepository.save(data);
+		}
+	}
+}
diff --git a/BudgetMasterDatabaseMigrator/src/main/resources/application.properties b/BudgetMasterDatabaseMigrator/src/main/resources/application.properties
new file mode 100644
index 000000000..eee50fdfc
--- /dev/null
+++ b/BudgetMasterDatabaseMigrator/src/main/resources/application.properties
@@ -0,0 +1,19 @@
+spring.datasource.jdbc-url=jdbc:h2:/C:/Users/RobertG/AppData/Roaming/Deadlocker/BudgetMaster/debug/budgetmaster
+spring.datasource.username=sa
+spring.datasource.password=
+spring.datasource.driver-class-name=org.h2.Driver
+
+spring.seconddatasource.jdbc-url=jdbc:postgresql://localhost:5432/budgetmaster
+spring.seconddatasource.username=budgetmaster
+spring.seconddatasource.password=BudgetMaster
+spring.seconddatasource.driver-class-name=org.postgresql.Driver
+
+spring.jpa.database=default
+spring.jpa.show-sql=true
+spring.jpa.hibernate.ddl-auto=update
+
+spring.batch.jdbc.initialize-schema=always
+spring.batch.job.enabled=false
+
+logging.level.root=INFO
+logging.level.de.deadlocker8=DEBUG
\ No newline at end of file
diff --git a/BudgetMasterServer/pom.xml b/BudgetMasterServer/pom.xml
index 0c6715f2f..42eb9c36d 100644
--- a/BudgetMasterServer/pom.xml
+++ b/BudgetMasterServer/pom.xml
@@ -37,7 +37,6 @@
         <assertj-core.version>3.22.0</assertj-core.version>
         <jgit.version>6.0.0.202111291000-r</jgit.version>
         <natorder.version>1.1.2</natorder.version>
-        <h2database.version>1.4.199</h2database.version>
         <itextpdf.version>5.5.13.2</itextpdf.version>
         <vanilla-picker.version>2.12.1</vanilla-picker.version>
         <jacoco-maven-plugin.version>0.8.7</jacoco-maven-plugin.version>
@@ -112,12 +111,6 @@
             </exclusions>
         </dependency>
 
-        <dependency>
-            <groupId>com.h2database</groupId>
-            <artifactId>h2</artifactId>
-            <version>${h2database.version}</version>
-        </dependency>
-
         <dependency>
             <groupId>org.postgresql</groupId>
             <artifactId>postgresql</artifactId>
diff --git a/pom.xml b/pom.xml
index 3b2e278b3..109be0af2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,6 +12,7 @@
 
     <modules>
         <module>BudgetMasterServer</module>
+        <module>BudgetMasterDatabaseMigrator</module>
     </modules>
 
     <repositories>
-- 
GitLab