diff --git a/.classpath b/.classpath
index 25127bf..1a4ae87 100644
--- a/.classpath
+++ b/.classpath
@@ -24,14 +24,6 @@
-
-
-
-
-
-
-
-
@@ -39,12 +31,21 @@
-
+
+
+
+
+
+
-
-
+
+
+
+
+
+
diff --git a/.factorypath b/.factorypath
index 38d6468..7c47bf9 100644
--- a/.factorypath
+++ b/.factorypath
@@ -1,74 +1,80 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
index abdea9a..29abf99 100644
--- a/.settings/org.eclipse.core.resources.prefs
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,6 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/resources=UTF-8
encoding/=UTF-8
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 47dcc0b..cd99dd0 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -10,5 +10,5 @@ org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.processAnnotations=enabled
-org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=17
diff --git a/pom.xml b/pom.xml
index 66c1755..85f9d64 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,6 +80,11 @@
jaxb-api
2.3.0
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.8.5
+
diff --git a/src/main/java/com/stephenschafer/budget/BudgetAmounts.java b/src/main/java/com/stephenschafer/budget/BudgetAmounts.java
new file mode 100644
index 0000000..d110a3f
--- /dev/null
+++ b/src/main/java/com/stephenschafer/budget/BudgetAmounts.java
@@ -0,0 +1,23 @@
+package com.stephenschafer.budget;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+@Getter
+@Setter
+@ToString
+public class BudgetAmounts {
+ private String year = null;
+ private Integer categoryId;
+ private BigDecimal yearBudget;
+ private Map monthBudgets = new HashMap<>();
+
+ public void setMonthBudget(final int monthNum, final BigDecimal amount) {
+ monthBudgets.put(Integer.valueOf(monthNum), amount);
+ }
+}
diff --git a/src/main/java/com/stephenschafer/budget/CatRegex.java b/src/main/java/com/stephenschafer/budget/CatRegex.java
new file mode 100644
index 0000000..98aa0b7
--- /dev/null
+++ b/src/main/java/com/stephenschafer/budget/CatRegex.java
@@ -0,0 +1,175 @@
+package com.stephenschafer.budget;
+
+import java.util.regex.Pattern;
+
+public class CatRegex {
+ private final Pattern pattern;
+ private final String category;
+ private final String source;
+ private final int priority;
+ private final String extraDescription;
+ private final Integer year;
+ private int id;
+
+ public CatRegex(final int id, final Pattern pattern, final String category, final String source,
+ final int priority, final String extraDescription, final Integer year) {
+ this.id = id;
+ this.pattern = pattern;
+ this.category = category;
+ this.source = source;
+ this.priority = priority;
+ this.extraDescription = extraDescription;
+ this.year = year;
+ }
+
+ public CatRegex(final String line) {
+ final String[] parts = line.split(",");
+ final String category = parts[0].trim();
+ final String regex = parts[1].trim();
+ String flagsString = "";
+ String source = "";
+ int priority = 0;
+ String extraDescription = "";
+ Integer year = null;
+ if (parts.length > 2) {
+ flagsString = parts[2].trim();
+ if (parts.length > 3) {
+ source = parts[3].trim();
+ if (parts.length > 4) {
+ final String priorityString = parts[4].trim();
+ if (priorityString.length() > 0) {
+ try {
+ priority = Integer.parseInt(priorityString);
+ }
+ catch (final NumberFormatException e) {
+ System.out.println(
+ "Invalid priority in reegex line: " + line + "
");
+ }
+ }
+ if (parts.length > 5) {
+ extraDescription = parts[5].trim();
+ if (parts.length > 6) {
+ final String yearString = parts[6].trim();
+ if (yearString.length() > 0) {
+ try {
+ year = Integer.valueOf(yearString);
+ }
+ catch (final NumberFormatException e) {
+ System.out.println(
+ "Invalid year in reegex line: " + line + "
");
+ }
+ }
+ if (parts.length > 7) {
+ System.out.println("Too many parts in " + line);
+ }
+ }
+ }
+ }
+ }
+ }
+ int flags = 0;
+ if (flagsString.indexOf("i") >= 0) {
+ flags |= Pattern.CASE_INSENSITIVE;
+ }
+ if (flagsString.indexOf("m") >= 0) {
+ flags |= Pattern.MULTILINE;
+ }
+ pattern = Pattern.compile(regex, flags);
+ this.category = category;
+ this.source = source.length() == 0 ? null : source;
+ this.priority = priority;
+ this.extraDescription = extraDescription;
+ this.year = year;
+ }
+
+ public String toLine() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(category);
+ sb.append(", ");
+ sb.append(pattern.pattern());
+ sb.append(", ");
+ String flags = "";
+ if ((pattern.flags() & Pattern.CASE_INSENSITIVE) != 0) {
+ flags += "i";
+ }
+ if ((pattern.flags() & Pattern.MULTILINE) != 0) {
+ flags += "m";
+ }
+ sb.append(flags);
+ sb.append(", ");
+ sb.append(source == null ? "" : source);
+ sb.append(", ");
+ sb.append(priority == 0 ? "" : String.valueOf(priority));
+ sb.append(", ");
+ sb.append(extraDescription);
+ sb.append(", ");
+ sb.append(year == null ? "" : year.toString());
+ return sb.toString();
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public Pattern getPattern() {
+ return pattern;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public int getPriority() {
+ return priority;
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 0;
+ if (source != null) {
+ hashCode += source.hashCode();
+ }
+ return hashCode + pattern.hashCode();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj instanceof CatRegex) {
+ final CatRegex that = (CatRegex) obj;
+ if (!source.equals(that.source) || !pattern.equals(that.pattern)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(category);
+ sb.append(": ");
+ sb.append(pattern.toString());
+ if (source != null && source.trim().length() > 0) {
+ sb.append(" (" + source + ")");
+ }
+ sb.append(", ");
+ sb.append(priority);
+ return sb.toString();
+ }
+
+ public String getExtraDescription() {
+ return extraDescription;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(final int id) {
+ this.id = id;
+ }
+
+ public Integer getYear() {
+ return year;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/stephenschafer/budget/CategorizationResponse.java b/src/main/java/com/stephenschafer/budget/CategorizationResponse.java
new file mode 100644
index 0000000..f4f05f2
--- /dev/null
+++ b/src/main/java/com/stephenschafer/budget/CategorizationResponse.java
@@ -0,0 +1,17 @@
+package com.stephenschafer.budget;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+@Getter
+@Setter
+@ToString
+public class CategorizationResponse {
+ String year = null;
+ List multiplyAssignedTransactions = new ArrayList<>();
+ List unassignedTransactions = new ArrayList<>();
+}
diff --git a/src/main/java/com/stephenschafer/budget/CategorizerController.java b/src/main/java/com/stephenschafer/budget/CategorizerController.java
new file mode 100644
index 0000000..be423d8
--- /dev/null
+++ b/src/main/java/com/stephenschafer/budget/CategorizerController.java
@@ -0,0 +1,362 @@
+package com.stephenschafer.budget;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.math.BigDecimal;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.StatementCallback;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@CrossOrigin(origins = "*", maxAge = 3600)
+@RestController
+public class CategorizerController {
+ @Autowired
+ TransactionDao transactionDao;
+ @Autowired
+ UserService userService;
+ @Autowired
+ JdbcTemplate jdbcTemplate;
+
+ @GetMapping("/problems")
+ @ResponseBody
+ private ApiResponse