View Javadoc
1   package de.aikiit.bilanzanalyser.upload;
2   
3   import de.aikiit.bilanzanalyser.entity.BilanzRow;
4   import de.aikiit.bilanzanalyser.entity.database.BilanzRowEntity;
5   import de.aikiit.bilanzanalyser.entity.database.CategoryEntity;
6   import de.aikiit.bilanzanalyser.entity.database.PaymentEntity;
7   import de.aikiit.bilanzanalyser.entity.database.ShopEntity;
8   import de.aikiit.bilanzanalyser.entity.database.SourceEntity;
9   import de.aikiit.bilanzanalyser.entity.database.repository.BilanzRowRepository;
10  import de.aikiit.bilanzanalyser.entity.database.repository.CategoryRepository;
11  import de.aikiit.bilanzanalyser.entity.database.repository.PaymentRepository;
12  import de.aikiit.bilanzanalyser.entity.database.repository.ShopRepository;
13  import de.aikiit.bilanzanalyser.entity.database.repository.SourceRepository;
14  import de.aikiit.bilanzanalyser.reader.BilanzOdsReader;
15  import de.aikiit.bilanzanalyser.reader.BilanzRowParserResult;
16  import jakarta.transaction.Transactional;
17  import lombok.extern.log4j.Log4j2;
18  import org.springframework.dao.DataIntegrityViolationException;
19  import org.springframework.scheduling.annotation.Async;
20  import org.springframework.stereotype.Service;
21  
22  import java.io.IOException;
23  import java.nio.file.Path;
24  import java.util.Optional;
25  import java.util.function.Function;
26  
27  @Service
28  @Log4j2
29  public class UploadAnalysisService {
30      private final BilanzRowRepository bilanzRowRepository;
31      private final ShopRepository shopRepository;
32      private final PaymentRepository paymentRepository;
33      private final CategoryRepository categoryRepository;
34      private final SourceRepository sourceRepository;
35  
36      public UploadAnalysisService(BilanzRowRepository bilanzRowRepository, ShopRepository shopRepository, PaymentRepository paymentRepository, CategoryRepository categoryRepository, SourceRepository sourceRepository) {
37          this.bilanzRowRepository = bilanzRowRepository;
38          this.shopRepository = shopRepository;
39          this.paymentRepository = paymentRepository;
40          this.categoryRepository = categoryRepository;
41          this.sourceRepository = sourceRepository;
42      }
43  
44      /**
45       * Parses and analyses a given file.
46       *
47       * @param worksheetName selected worksheet name to process.
48       * @param spreadsheet   path to spreadsheet file.
49       * @return result container.
50       * @throws IOException in case of I/O problems.
51       */
52      BilanzRowParserResult processFile(String worksheetName, Path spreadsheet) throws IOException {
53          BilanzOdsReader reader = new BilanzOdsReader(worksheetName, spreadsheet);
54          return reader.extractData();
55      }
56  
57      @Async
58      public void flushDataIntoDatabase(BilanzRowParserResult data) {
59          long start = System.nanoTime();
60          log.info("Starting to flush data into database...");
61  
62          for (BilanzRow row : data.rows()) {
63              BilanzRowEntity entity = new BilanzRowEntity();
64              entity.setDate(row.getDate());
65              entity.setAmount(row.getAmount());
66              entity.setDescription(replaceIfEmpty(row.getDescription()));
67              entity.setShop(getOrCreateShop(row.getShop()));
68              entity.setPayment(getOrCreatePayment(row.getPayment()));
69              entity.setCategory(getOrCreateCategory(row.getCategory()));
70              entity.setSource(getOrCreateSource(row.getSource()));
71              bilanzRowRepository.save(entity);
72          }
73  
74          log.info("Successfully flushed data into database in {} ms.", (System.nanoTime() - start) / 1_000_000);
75      }
76  
77      /**
78       * Retrieves an entity by name or creates and persists it if it does not exist,
79       * assuming that the underlying entity enforces uniqueness with a database constraint on the column 'name'.
80       * <p>
81       * This method implements a "get-or-create" pattern with basic concurrency handling:
82       * it first attempts to find an existing entity using the provided {@code finder}.
83       * If none is found, it invokes the {@code creator} to create and persist a new entity.
84       * <p>
85       * In case of a concurrent insert (e.g. due to a unique constraint on the name),
86       * a {@link DataIntegrityViolationException} may be thrown during creation. In that case,
87       * the method retries the lookup and returns the entity inserted by the concurrent transaction.
88       * <p>
89       * The input name is normalized using {@link #replaceIfEmpty(String)} before lookup and creation.
90       *
91       * @param rawName the original name value; may be {@code null} or blank
92       * @param finder  function used to look up an existing entity by normalized name
93       * @param creator function that creates and persists a new entity for the normalized name;
94       *                must return a fully initialized and saved entity
95       * @param <T>     the entity type
96       * @return an existing or newly created entity corresponding to the given name.
97       */
98      @Transactional
99      <T> T getOrCreate(String rawName, Function<String, Optional<T>> finder, Function<String, T> creator) {
100         String name = replaceIfEmpty(rawName);
101 
102         return finder.apply(name).orElseGet(() -> {
103             try {
104                 return creator.apply(name);
105             } catch (DataIntegrityViolationException e) {
106                 // someone else inserted it concurrently
107                 return finder.apply(name).orElseThrow();
108             }
109         });
110     }
111 
112     @Transactional
113     ShopEntity getOrCreateShop(String name) {
114         return getOrCreate(name, shopRepository::findByName, n -> {
115             ShopEntity e = new ShopEntity();
116             e.setName(n);
117             return shopRepository.save(e);
118         });
119     }
120 
121     @Transactional
122     PaymentEntity getOrCreatePayment(String name) {
123         return getOrCreate(name, paymentRepository::findByName, n -> {
124             PaymentEntity e = new PaymentEntity();
125             e.setName(n);
126             return paymentRepository.save(e);
127         });
128     }
129 
130     @Transactional
131     CategoryEntity getOrCreateCategory(String name) {
132         return getOrCreate(name, categoryRepository::findByName, n -> {
133             CategoryEntity e = new CategoryEntity();
134             e.setName(n);
135             return categoryRepository.save(e);
136         });
137     }
138 
139     @Transactional
140     SourceEntity getOrCreateSource(String name) {
141         return getOrCreate(name, sourceRepository::findByName, n -> {
142             SourceEntity e = new SourceEntity();
143             e.setName(n); // no replaceIfEmpty here before, but now consistent
144             return sourceRepository.save(e);
145         });
146     }
147 
148     String replaceIfEmpty(String value) {
149         return value == null || value.isBlank() ? "<empty>" : value.trim();
150     }
151 
152 }