From 9df0b910225776132fa2e2fcade8c6f3d7879891 Mon Sep 17 00:00:00 2001 From: Robert Goldmann <deadlocker@gmx.de> Date: Tue, 30 Jun 2020 21:41:20 +0200 Subject: [PATCH] Fixed #526 - duplicated recurring transactions after old database is migrated to BudgetMaster 2.4.0 --- pom.xml | 4 +- .../budgetmaster/settings/Settings.java | 13 + .../settings/SettingsService.java | 4 + .../transactions/TransactionRepository.java | 1 + .../utils/eventlistener/DateRepair.java | 222 ++++++++++++++++++ .../eventlistener/UpdateInstalledVersion.java | 40 ++++ .../resources/templates/settings/settings.ftl | 1 + 7 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/DateRepair.java create mode 100644 src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/UpdateInstalledVersion.java diff --git a/pom.xml b/pom.xml index a49fdb4ff..e6659eee5 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ <groupId>de.deadlocker8</groupId> <artifactId>BudgetMaster</artifactId> - <version>2.4.0</version> + <version>2.4.1</version> <name>BudgetMaster</name> <repositories> @@ -70,7 +70,7 @@ <app.versionDate>${maven.build.timestamp}</app.versionDate> <maven.build.timestamp.format>dd.MM.yy</maven.build.timestamp.format> - <app.versionCode>23</app.versionCode> + <app.versionCode>24</app.versionCode> <app.author>Robert Goldmann</app.author> <project.outputDirectory>build/${project.version}</project.outputDirectory> diff --git a/src/main/java/de/deadlocker8/budgetmaster/settings/Settings.java b/src/main/java/de/deadlocker8/budgetmaster/settings/Settings.java index 4dd8a5984..7d03c617c 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/settings/Settings.java +++ b/src/main/java/de/deadlocker8/budgetmaster/settings/Settings.java @@ -28,6 +28,7 @@ public class Settings private Integer autoBackupDays; private AutoBackupTime autoBackupTime; private Integer autoBackupFilesToKeep; + private Integer installedVersionCode; public Settings() { @@ -48,6 +49,7 @@ public class Settings defaultSettings.setAutoBackupDays(1); defaultSettings.setAutoBackupTime(AutoBackupTime.TIME_00); defaultSettings.setAutoBackupFilesToKeep(3); + defaultSettings.setInstalledVersionCode(0); return defaultSettings; } @@ -186,6 +188,16 @@ public class Settings this.autoBackupFilesToKeep = autoBackupFilesToKeep; } + public Integer getInstalledVersionCode() + { + return installedVersionCode; + } + + public void setInstalledVersionCode(Integer installedVersionCode) + { + this.installedVersionCode = installedVersionCode; + } + @Override public String toString() { @@ -203,6 +215,7 @@ public class Settings ", autoBackupDays=" + autoBackupDays + ", autoBackupTime=" + autoBackupTime + ", autoBackupFilesToKeep=" + autoBackupFilesToKeep + + ", installedVersionCode=" + installedVersionCode + '}'; } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsService.java b/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsService.java index 26f437d33..27ea656ec 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsService.java @@ -77,6 +77,10 @@ public class SettingsService { settings.setAutoBackupFilesToKeep(defaultSettings.getAutoBackupFilesToKeep()); } + if(settings.getInstalledVersionCode() == null) + { + settings.setInstalledVersionCode(defaultSettings.getInstalledVersionCode()); + } } @SuppressWarnings("OptionalGetWithoutIsPresent") diff --git a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionRepository.java b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionRepository.java index 5ad31f0dc..f418aab76 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionRepository.java +++ b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionRepository.java @@ -11,6 +11,7 @@ import org.springframework.data.jpa.repository.Query; import java.util.List; +@SuppressWarnings("SqlResolve") public interface TransactionRepository extends JpaRepository<Transaction, Integer>, JpaSpecificationExecutor<Transaction> { List<Transaction> findAllByAccountAndDateBetweenOrderByDateDesc(Account account, DateTime startDate, DateTime endDate); diff --git a/src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/DateRepair.java b/src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/DateRepair.java new file mode 100644 index 000000000..6733de828 --- /dev/null +++ b/src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/DateRepair.java @@ -0,0 +1,222 @@ +package de.deadlocker8.budgetmaster.utils.eventlistener; + +import de.deadlocker8.budgetmaster.repeating.RepeatingOption; +import de.deadlocker8.budgetmaster.repeating.RepeatingOptionRepository; +import de.deadlocker8.budgetmaster.repeating.RepeatingTransactionUpdater; +import de.deadlocker8.budgetmaster.settings.SettingsService; +import de.deadlocker8.budgetmaster.transactions.Transaction; +import de.deadlocker8.budgetmaster.transactions.TransactionRepository; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class DateRepair +{ + private static final Logger LOGGER = LoggerFactory.getLogger(DateRepair.class); + + private static final int DATE_REPAIR_VERSION_CODE = 23; + + private final TransactionRepository transactionRepository; + private final RepeatingOptionRepository repeatingOptionRepository; + private final RepeatingTransactionUpdater repeatingTransactionUpdater; + private final SettingsService settingsService; + private final EntityManager entityManager; + + @Autowired + public DateRepair(TransactionRepository transactionRepository, RepeatingOptionRepository repeatingOptionRepository, RepeatingTransactionUpdater repeatingTransactionUpdater, SettingsService settingsService, EntityManager entityManager) + { + this.transactionRepository = transactionRepository; + this.repeatingOptionRepository = repeatingOptionRepository; + this.repeatingTransactionUpdater = repeatingTransactionUpdater; + this.settingsService = settingsService; + this.entityManager = entityManager; + } + + @EventListener + @Transactional + @Order(1) + public void onApplicationEvent(ApplicationStartedEvent event) + { + checkForPossibleDuplicates(); + if(settingsService.getSettings().getInstalledVersionCode() <= DATE_REPAIR_VERSION_CODE) + { + deleteOldRepeatingTransactions(); + + repairTransactionDates(); + repairRepeatingOptionsDates(); + + LOGGER.debug("Re-created repeating transactions"); + repeatingTransactionUpdater.updateRepeatingTransactions(DateTime.now()); + } + } + + private void deleteOldRepeatingTransactions() + { + int numberOfDeletedItems = 0; + + for(RepeatingOption repeatingOption : repeatingOptionRepository.findAll()) + { + final List<Transaction> referringTransactions = repeatingOption.getReferringTransactions(); + final List<Transaction> transactionsSorted = referringTransactions.stream() + .sorted(Comparator.comparing(Transaction::getDate)) + .collect(Collectors.toList()); + + final List<Transaction> transactions = transactionsSorted.subList(1, transactionsSorted.size()); + numberOfDeletedItems += transactions.size(); + + for(Transaction transaction : transactions) + { + final Query nativeQuery = entityManager.createNativeQuery("DELETE FROM `transaction` WHERE id=:ID"); + nativeQuery.setParameter("ID", transaction.getID()); + nativeQuery.executeUpdate(); + } + } + + LOGGER.debug(MessageFormat.format("Deleted {0} wrong repeating transactions", numberOfDeletedItems)); + } + + private void checkForPossibleDuplicates() + { + final List<Transaction> transactions = transactionRepository.findAll(); + final List<Transaction> alreadyScanned = new ArrayList<>(); + final List<List<Transaction>> duplicated = new ArrayList<>(); + + for(Transaction transaction : transactions) + { + for(Transaction scannedTransaction : alreadyScanned) + { + if(isPossibleDuplicate(transaction, scannedTransaction)) + { + final ArrayList<Transaction> entry = new ArrayList<>(); + entry.add(scannedTransaction); + entry.add(transaction); + duplicated.add(entry); + } + } + + alreadyScanned.add(transaction); + } + + if(!duplicated.isEmpty()) + { + final List<List<Integer>> duplicatedIDs = duplicated.stream() + .map(entry -> entry.stream().map(Transaction::getID).collect(Collectors.toList())) + .collect(Collectors.toList()); + LOGGER.warn(MessageFormat.format("Found {0} possible duplicated transactions: {1}", duplicated.size(), duplicatedIDs)); + } + } + + private boolean isPossibleDuplicate(Transaction transaction1, Transaction transaction2) + { + if(transaction1.getRepeatingOption() == null || transaction2.getRepeatingOption() == null) + { + return false; + } + + if(!transaction1.getRepeatingOption().equals(transaction2.getRepeatingOption())) + { + return false; + } + + return areDatesSimilar(transaction1.getDate(), transaction2.getDate()); + } + + private boolean areDatesSimilar(DateTime date1, DateTime date2) + { + if(date1.getYear() != date2.getYear()) + { + return false; + } + + if(date1.getMonthOfYear() != date2.getMonthOfYear()) + { + return false; + } + + return Math.abs(date1.getDayOfMonth() - date2.getDayOfMonth()) <= 1; + } + + private void repairTransactionDates() + { + int numberOfRepairedTransactions = 0; + + final List<Transaction> transactions = transactionRepository.findAll(); + for(Transaction transaction : transactions) + { + final DateTime date = transaction.getDate(); + if(dateNeedsToBeRepaired(date)) + { + final DateTime fixedDate = getRepairedDate(date); + transaction.setDate(fixedDate); + numberOfRepairedTransactions++; + } + } + + LOGGER.debug(MessageFormat.format("Repaired {0}/{1} transaction dates", numberOfRepairedTransactions, transactions.size())); + } + + private void repairRepeatingOptionsDates() + { + int numberOfRepairedItems = 0; + + final List<RepeatingOption> repeatingOptions = repeatingOptionRepository.findAll(); + for(RepeatingOption repeatingOption : repeatingOptions) + { + final DateTime date = repeatingOption.getStartDate(); + if(dateNeedsToBeRepaired(date)) + { + final DateTime fixedDate = getRepairedDate(date); + repeatingOption.setStartDate(fixedDate); + numberOfRepairedItems++; + } + } + + LOGGER.debug(MessageFormat.format("Repaired {0}/{1} repeating option dates", numberOfRepairedItems, repeatingOptions.size())); + } + + private DateTime getRepairedDate(DateTime date) + { + return date.plusHours(6) + .withZone(DateTimeZone.UTC) + .withHourOfDay(0) + .withMinuteOfHour(0) + .withSecondOfMinute(0) + .withMillisOfSecond(0); + } + + private boolean dateNeedsToBeRepaired(DateTime date) + { + if(date.getHourOfDay() != 0) + { + return true; + } + + if(date.getMinuteOfHour() != 0) + { + return true; + } + + if(date.getSecondOfMinute() != 0) + { + return true; + } + + return date.getMillisOfSecond() != 0; + } +} diff --git a/src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/UpdateInstalledVersion.java b/src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/UpdateInstalledVersion.java new file mode 100644 index 000000000..9fc9065b8 --- /dev/null +++ b/src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/UpdateInstalledVersion.java @@ -0,0 +1,40 @@ +package de.deadlocker8.budgetmaster.utils.eventlistener; + +import de.deadlocker8.budgetmaster.Build; +import de.deadlocker8.budgetmaster.settings.SettingsService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.text.MessageFormat; + +@Component +public class UpdateInstalledVersion +{ + private static final Logger LOGGER = LoggerFactory.getLogger(UpdateInstalledVersion.class); + + private final SettingsService settingsService; + + @Autowired + public UpdateInstalledVersion(SettingsService settingsService) + { + this.settingsService = settingsService; + } + + @EventListener + @Transactional + @Order(2) + public void onApplicationEvent(ApplicationStartedEvent event) + { + final Build build = Build.getInstance(); + final int runningVersionCode = Integer.parseInt(build.getVersionCode()); + + LOGGER.debug(MessageFormat.format("Updated installedVersionCode to {0}", runningVersionCode)); + settingsService.getSettings().setInstalledVersionCode(runningVersionCode); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/settings/settings.ftl b/src/main/resources/templates/settings/settings.ftl index 434bda646..f1d99668a 100644 --- a/src/main/resources/templates/settings/settings.ftl +++ b/src/main/resources/templates/settings/settings.ftl @@ -24,6 +24,7 @@ <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <input type="hidden" name="ID" value="${settings.getID()?c}"> <input type="hidden" name="lastBackupReminderDate" value="${dateService.getLongDateString(settings.getLastBackupReminderDate())}"> + <input type="hidden" name="installedVersionCode" value="${settings.getInstalledVersionCode()}"> <#-- password --> <div class="row"> -- GitLab