diff --git a/financer-common/pom.xml b/financer-common/pom.xml
index ec47540..e714769 100644
--- a/financer-common/pom.xml
+++ b/financer-common/pom.xml
@@ -56,6 +56,10 @@
com.fasterxml.jackson.core
jackson-annotations
+
+ org.springframework
+ spring-web
+
diff --git a/financer-web-client/src/main/java/de/financer/ResponseReason.java b/financer-common/src/main/java/de/financer/ResponseReason.java
similarity index 81%
rename from financer-web-client/src/main/java/de/financer/ResponseReason.java
rename to financer-common/src/main/java/de/financer/ResponseReason.java
index f73464b..30ef79c 100644
--- a/financer-web-client/src/main/java/de/financer/ResponseReason.java
+++ b/financer-common/src/main/java/de/financer/ResponseReason.java
@@ -5,6 +5,7 @@ import org.springframework.http.ResponseEntity;
public enum ResponseReason {
OK(HttpStatus.OK),
+ CREATED(HttpStatus.CREATED),
UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_ACCOUNT_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
FROM_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
@@ -31,9 +32,15 @@ public enum ResponseReason {
ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
DUPLICATE_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR),
DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.INTERNAL_SERVER_ERROR),
- ACCOUNT_GROUP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR);
+ ACCOUNT_GROUP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
+ PERIOD_ID_NOT_NUMERIC(HttpStatus.BAD_REQUEST),
+ PERIOD_NOT_FOUND(HttpStatus.BAD_REQUEST),
+ LIMIT_NOT_NUMERIC(HttpStatus.BAD_REQUEST),
+ INVALID_TAX_RELEVANT_VALUE(HttpStatus.BAD_REQUEST),
+ INVALID_ACCOUNTS_AND_VALUE(HttpStatus.BAD_REQUEST),
+ FQL_MALFORMED(HttpStatus.BAD_REQUEST);
- private HttpStatus httpStatus;
+ private final HttpStatus httpStatus;
ResponseReason(HttpStatus httpStatus) {
this.httpStatus = httpStatus;
@@ -43,6 +50,10 @@ public enum ResponseReason {
return new ResponseEntity<>(this.name(), this.httpStatus);
}
+ public HttpStatus getHttpStatus() {
+ return this.httpStatus;
+ }
+
public static ResponseReason fromResponseEntity(ResponseEntity entity) {
for (ResponseReason reason : values()) {
if (reason.name().equals(entity.getBody())) {
diff --git a/financer-common/src/main/java/de/financer/dto/Order.java b/financer-common/src/main/java/de/financer/dto/Order.java
new file mode 100644
index 0000000..b776537
--- /dev/null
+++ b/financer-common/src/main/java/de/financer/dto/Order.java
@@ -0,0 +1,30 @@
+package de.financer.dto;
+
+import de.financer.dto.comparator.TransactionsByDateByIdDescComparator;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Optional;
+
+public enum Order {
+ TRANSACTIONS_BY_DATE_DESC(TransactionsByDateByIdDescComparator.class);
+
+ private Comparator comparator;
+
+ Order(Class extends Comparator> comparatorClass) {
+ try {
+ this.comparator = comparatorClass.getDeclaredConstructor().newInstance();
+ }
+ catch (ReflectiveOperationException e) {
+ // Comparators are required by contract to have a default constructor
+ }
+ }
+
+ public Comparator getComparator() {
+ return this.comparator;
+ }
+
+ public static Optional getByName(String name) {
+ return Arrays.stream(values()).filter(o -> o.name().equals(name)).findFirst();
+ }
+}
diff --git a/financer-common/src/main/java/de/financer/dto/SaveTransactionRequestDto.java b/financer-common/src/main/java/de/financer/dto/SaveTransactionRequestDto.java
new file mode 100644
index 0000000..3615afa
--- /dev/null
+++ b/financer-common/src/main/java/de/financer/dto/SaveTransactionRequestDto.java
@@ -0,0 +1,65 @@
+package de.financer.dto;
+
+import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
+
+public class SaveTransactionRequestDto {
+ private String fromAccountKey;
+ private String toAccountKey;
+ private String amount;
+ private String date;
+ private String description;
+ private Boolean taxRelevant;
+
+ public String getFromAccountKey() {
+ return fromAccountKey;
+ }
+
+ public void setFromAccountKey(String fromAccountKey) {
+ this.fromAccountKey = fromAccountKey;
+ }
+
+ public String getToAccountKey() {
+ return toAccountKey;
+ }
+
+ public void setToAccountKey(String toAccountKey) {
+ this.toAccountKey = toAccountKey;
+ }
+
+ public String getAmount() {
+ return amount;
+ }
+
+ public void setAmount(String amount) {
+ this.amount = amount;
+ }
+
+ public String getDate() {
+ return date;
+ }
+
+ public void setDate(String date) {
+ this.date = date;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Boolean getTaxRelevant() {
+ return taxRelevant;
+ }
+
+ public void setTaxRelevant(Boolean taxRelevant) {
+ this.taxRelevant = taxRelevant;
+ }
+
+ @Override
+ public String toString() {
+ return ReflectionToStringBuilder.toString(this);
+ }
+}
diff --git a/financer-common/src/main/java/de/financer/dto/SearchTransactionsRequestDto.java b/financer-common/src/main/java/de/financer/dto/SearchTransactionsRequestDto.java
new file mode 100644
index 0000000..c7a0ff5
--- /dev/null
+++ b/financer-common/src/main/java/de/financer/dto/SearchTransactionsRequestDto.java
@@ -0,0 +1,65 @@
+package de.financer.dto;
+
+import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
+
+public class SearchTransactionsRequestDto {
+ private String fromAccountKey;
+ private String toAccountKey;
+ private String periodId;
+ private String limit;
+ private String order;
+ private String taxRelevant;
+
+ public String getFromAccountKey() {
+ return fromAccountKey;
+ }
+
+ public void setFromAccountKey(String fromAccountKey) {
+ this.fromAccountKey = fromAccountKey;
+ }
+
+ public String getToAccountKey() {
+ return toAccountKey;
+ }
+
+ public void setToAccountKey(String toAccountKey) {
+ this.toAccountKey = toAccountKey;
+ }
+
+ public String getPeriodId() {
+ return periodId;
+ }
+
+ public void setPeriodId(String periodId) {
+ this.periodId = periodId;
+ }
+
+ public String getLimit() {
+ return limit;
+ }
+
+ public void setLimit(String limit) {
+ this.limit = limit;
+ }
+
+ public String getOrder() {
+ return order;
+ }
+
+ public void setOrder(String order) {
+ this.order = order;
+ }
+
+ public String getTaxRelevant() {
+ return taxRelevant;
+ }
+
+ public void setTaxRelevant(String taxRelevant) {
+ this.taxRelevant = taxRelevant;
+ }
+
+ @Override
+ public String toString() {
+ return ReflectionToStringBuilder.toString(this);
+ }
+}
diff --git a/financer-common/src/main/java/de/financer/dto/SearchTransactionsResponseDto.java b/financer-common/src/main/java/de/financer/dto/SearchTransactionsResponseDto.java
new file mode 100644
index 0000000..e6f83a4
--- /dev/null
+++ b/financer-common/src/main/java/de/financer/dto/SearchTransactionsResponseDto.java
@@ -0,0 +1,102 @@
+package de.financer.dto;
+
+import de.financer.model.Account;
+
+import java.time.LocalDate;
+
+public class SearchTransactionsResponseDto {
+ private Long id;
+ private Account fromAccount;
+ private Account toAccount;
+ private LocalDate date;
+ private String description;
+ private long amount;
+ private boolean taxRelevant;
+ private boolean recurring;
+
+ public SearchTransactionsResponseDto() {
+ // No-arg constructor for Jackson
+ }
+
+ public SearchTransactionsResponseDto(Long id,
+ Account fromAccount,
+ Account toAccount,
+ LocalDate date,
+ String description,
+ long amount,
+ boolean taxRelevant,
+ boolean recurring) {
+ this.id = id;
+ this.fromAccount = fromAccount;
+ this.toAccount = toAccount;
+ this.date = date;
+ this.description = description;
+ this.amount = amount;
+ this.taxRelevant = taxRelevant;
+ this.recurring = recurring;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Account getFromAccount() {
+ return fromAccount;
+ }
+
+ public void setFromAccount(Account fromAccount) {
+ this.fromAccount = fromAccount;
+ }
+
+ public Account getToAccount() {
+ return toAccount;
+ }
+
+ public void setToAccount(Account toAccount) {
+ this.toAccount = toAccount;
+ }
+
+ public LocalDate getDate() {
+ return date;
+ }
+
+ public void setDate(LocalDate date) {
+ this.date = date;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public long getAmount() {
+ return amount;
+ }
+
+ public void setAmount(long amount) {
+ this.amount = amount;
+ }
+
+ public boolean isTaxRelevant() {
+ return taxRelevant;
+ }
+
+ public void setTaxRelevant(boolean taxRelevant) {
+ this.taxRelevant = taxRelevant;
+ }
+
+ public boolean isRecurring() {
+ return recurring;
+ }
+
+ public void setRecurring(boolean recurring) {
+ this.recurring = recurring;
+ }
+}
diff --git a/financer-common/src/main/java/de/financer/dto/comparator/TransactionsByDateByIdDescComparator.java b/financer-common/src/main/java/de/financer/dto/comparator/TransactionsByDateByIdDescComparator.java
new file mode 100644
index 0000000..0e4a419
--- /dev/null
+++ b/financer-common/src/main/java/de/financer/dto/comparator/TransactionsByDateByIdDescComparator.java
@@ -0,0 +1,14 @@
+package de.financer.dto.comparator;
+
+import de.financer.model.Transaction;
+
+import java.util.Comparator;
+
+public class TransactionsByDateByIdDescComparator implements Comparator {
+ @Override
+ public int compare(Transaction o1, Transaction o2) {
+ int dateOrder = o1.getDate().compareTo(o2.getDate()) * -1;
+
+ return dateOrder != 0 ? dateOrder : o1.getId().compareTo(o2.getId()) * -1;
+ }
+}
diff --git a/financer-common/src/main/java/de/financer/dto/comparator/package-info.java b/financer-common/src/main/java/de/financer/dto/comparator/package-info.java
new file mode 100644
index 0000000..26d5b2c
--- /dev/null
+++ b/financer-common/src/main/java/de/financer/dto/comparator/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * All implementations need to declare an (implicit) parameterless default constructor
+ */
+package de.financer.dto.comparator;
\ No newline at end of file
diff --git a/financer-common/src/main/java/de/financer/model/Account.java b/financer-common/src/main/java/de/financer/model/Account.java
index a1d8bd0..88d0dcb 100644
--- a/financer-common/src/main/java/de/financer/model/Account.java
+++ b/financer-common/src/main/java/de/financer/model/Account.java
@@ -17,6 +17,7 @@ public class Account {
private Long currentBalance;
@ManyToOne
private AccountGroup accountGroup;
+ // TODO need to be lazy and disabled via Jackson View
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "account_id")
private Set accountStatistics;
diff --git a/financer-server/pom.xml b/financer-server/pom.xml
index e003c71..07da2f9 100644
--- a/financer-server/pom.xml
+++ b/financer-server/pom.xml
@@ -57,6 +57,11 @@
org.glassfish.jaxb
jaxb-runtime
+
+ org.antlr
+ antlr4-runtime
+ 4.8-1
+
de.77zzcx7.financer
@@ -93,6 +98,28 @@
+
+
+
+ org.antlr
+ antlr4-maven-plugin
+ 4.8-1
+
+
+ antlr
+
+ antlr4
+
+
+ true
+ false
+
+
+
+
+
+
+
diff --git a/financer-server/src/main/antlr4/de/financer/fql/FQL.g4 b/financer-server/src/main/antlr4/de/financer/fql/FQL.g4
new file mode 100644
index 0000000..7e7000d
--- /dev/null
+++ b/financer-server/src/main/antlr4/de/financer/fql/FQL.g4
@@ -0,0 +1,103 @@
+grammar FQL;
+
+/*
+ * Parser rules
+ */
+
+query
+ : (orExpression | orderByExpression EOF)* ;
+orExpression
+ : andExpression (OR andExpression)* ;
+andExpression
+ : expression (AND expression)* ;
+expression
+ : (regularExpression | betweenExpression | periodExpression)
+ | parenthesisExpression ;
+parenthesisExpression
+ : L_PAREN expr=orExpression R_PAREN ;
+
+orderByExpression
+ : ORDER_BY field=IDENTIFIER order=(DESC | ASC ) ;
+
+// regular expressions - NOT regex
+regularExpression
+ : (stringExpression | intExpression | booleanExpression | dateExpression) ;
+stringExpression
+ : field=IDENTIFIER operator=STRING_OPERATOR value=STRING_VALUE ;
+intExpression
+ : field=IDENTIFIER operator=(INT_OPERATOR | STRING_OPERATOR) value=INT_VALUE ;
+
+booleanExpression
+ : field=IDENTIFIER operator=STRING_OPERATOR value=(TRUE | FALSE) ;
+dateExpression
+ : field=IDENTIFIER operator=STRING_OPERATOR value=DATE_VALUE ;
+
+// BETWEEN expressions
+betweenExpression
+ : (betweenStringExpression | betweenIntExpression ) ;
+betweenStringExpression
+ : field=IDENTIFIER BETWEEN left=STRING_VALUE AND right=STRING_VALUE ;
+betweenIntExpression
+ : field=IDENTIFIER BETWEEN left=INT_VALUE AND right=INT_VALUE ;
+
+// period expressions
+periodExpression
+ : periodConstExpression ;
+periodConstExpression
+ : field=IDENTIFIER operator=STRING_OPERATOR value=(CURRENT |
+ LAST |
+ CURRENT_YEAR |
+ LAST_YEAR |
+ GRAND_TOTAL) ;
+
+/*
+ * Lexer rules
+ */
+
+fragment B : ('B' | 'b') ;
+fragment E : ('E' | 'e') ;
+fragment T : ('T' | 't') ;
+fragment W : ('W' | 'w') ;
+fragment N : ('N' | 'n') ;
+fragment A : ('A' | 'a') ;
+fragment D : ('D' | 'd') ;
+fragment C : ('C' | 'c') ;
+fragment U : ('U' | 'u') ;
+fragment R : ('R' | 'r') ;
+fragment L : ('L' | 'l') ;
+fragment S : ('S' | 's') ;
+fragment Y : ('Y' | 'y') ;
+fragment G : ('G' | 'g') ;
+fragment O : ('O' | 'o') ;
+fragment F : ('F' | 'f') ;
+fragment SPACE : ' ' ;
+
+// Keywords
+BETWEEN : B E T W E E N ;
+AND : A N D ;
+OR : O R ;
+ORDER_BY : O R D E R SPACE B Y ;
+DESC : D E S C ;
+ASC : A S C ;
+TRUE : T R U E ;
+FALSE : F A L S E ;
+
+// Constant values
+CURRENT : C U R R E N T ;
+LAST : L A S T ;
+YEAR : Y E A R ;
+GRAND_TOTAL : G R A N D '_' T O T A L ;
+CURRENT_YEAR : CURRENT '_' YEAR ;
+LAST_YEAR : LAST '_' YEAR ;
+
+STRING_OPERATOR : '=' ;
+INT_OPERATOR : '>' | '<' | '>=' | '<=' | '!=' ;
+L_PAREN : '(' ;
+R_PAREN : ')' ;
+IDENTIFIER : [a-zA-Z]+ ;
+INT_VALUE : [0-9]+ ;
+STRING_VALUE : '\'' [a-zA-Z0-9- ]+ '\'' ;
+DATE_VALUE : [0-9-]+ ;
+
+NEWLINE : ('\r'? '\n' | '\r')+ ;
+WHITESPACE : ' ' -> skip;
\ No newline at end of file
diff --git a/financer-server/src/main/java/database/common/V22_0_1__calculateAccountStatistics.java b/financer-server/src/main/java/database/common/V22_0_1__calculateAccountStatistics.java
index 80ff288..b26d69a 100644
--- a/financer-server/src/main/java/database/common/V22_0_1__calculateAccountStatistics.java
+++ b/financer-server/src/main/java/database/common/V22_0_1__calculateAccountStatistics.java
@@ -11,10 +11,10 @@ import java.util.List;
public class V22_0_1__calculateAccountStatistics extends BaseJavaMigration {
private static class TransactionPeriodAccountsContainer {
- public Long transactionAmount;
- public Long expensePeriodId;
- public Long fromAccountId;
- public Long toAccountId;
+ public final Long transactionAmount;
+ public final Long expensePeriodId;
+ public final Long fromAccountId;
+ public final Long toAccountId;
public TransactionPeriodAccountsContainer(Long transactionAmount, Long expensePeriodId, Long fromAccountId, Long toAccountId) {
this.transactionAmount = transactionAmount;
diff --git a/financer-server/src/main/java/database/common/V26_0_0__createGrandTotalPeriod.java b/financer-server/src/main/java/database/common/V26_0_0__createGrandTotalPeriod.java
index dfeab37..2dcb5d0 100644
--- a/financer-server/src/main/java/database/common/V26_0_0__createGrandTotalPeriod.java
+++ b/financer-server/src/main/java/database/common/V26_0_0__createGrandTotalPeriod.java
@@ -12,10 +12,10 @@ import java.util.List;
public class V26_0_0__createGrandTotalPeriod extends BaseJavaMigration {
private static class TransactionAccountsContainer {
- public Long transactionAmount;
- public Long fromAccountId;
- public Long toAccountId;
- public Long id;
+ public final Long transactionAmount;
+ public final Long fromAccountId;
+ public final Long toAccountId;
+ public final Long id;
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
this.transactionAmount = transactionAmount;
diff --git a/financer-server/src/main/java/database/common/V26_0_1__createExpenseYearPeriod_2019.java b/financer-server/src/main/java/database/common/V26_0_1__createExpenseYearPeriod_2019.java
index 3f8eaab..6854fd6 100644
--- a/financer-server/src/main/java/database/common/V26_0_1__createExpenseYearPeriod_2019.java
+++ b/financer-server/src/main/java/database/common/V26_0_1__createExpenseYearPeriod_2019.java
@@ -14,10 +14,10 @@ public class V26_0_1__createExpenseYearPeriod_2019 extends BaseJavaMigration {
private static class TransactionAccountsContainer {
- public Long transactionAmount;
- public Long fromAccountId;
- public Long toAccountId;
- public Long id;
+ public final Long transactionAmount;
+ public final Long fromAccountId;
+ public final Long toAccountId;
+ public final Long id;
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
this.transactionAmount = transactionAmount;
this.fromAccountId = fromAccountId;
diff --git a/financer-server/src/main/java/database/common/V26_0_2__createExpenseYearPeriod_2020.java b/financer-server/src/main/java/database/common/V26_0_2__createExpenseYearPeriod_2020.java
index ea641e7..e6f1b38 100644
--- a/financer-server/src/main/java/database/common/V26_0_2__createExpenseYearPeriod_2020.java
+++ b/financer-server/src/main/java/database/common/V26_0_2__createExpenseYearPeriod_2020.java
@@ -6,7 +6,6 @@ import org.flywaydb.core.api.migration.Context;
import java.sql.*;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
-import java.time.temporal.TemporalField;
import java.util.ArrayList;
import java.util.List;
@@ -14,10 +13,10 @@ public class V26_0_2__createExpenseYearPeriod_2020 extends BaseJavaMigration {
private static class TransactionAccountsContainer {
- public Long transactionAmount;
- public Long fromAccountId;
- public Long toAccountId;
- public Long id;
+ public final Long transactionAmount;
+ public final Long fromAccountId;
+ public final Long toAccountId;
+ public final Long id;
public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) {
this.transactionAmount = transactionAmount;
this.fromAccountId = fromAccountId;
diff --git a/financer-server/src/main/java/de/financer/ResponseReason.java b/financer-server/src/main/java/de/financer/ResponseReason.java
deleted file mode 100644
index 264d41f..0000000
--- a/financer-server/src/main/java/de/financer/ResponseReason.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package de.financer;
-
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-
-public enum ResponseReason {
- OK(HttpStatus.OK),
- UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
- INVALID_ACCOUNT_TYPE(HttpStatus.BAD_REQUEST),
- FROM_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
- TO_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
- FROM_AND_TO_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
- INVALID_DATE_FORMAT(HttpStatus.BAD_REQUEST),
- MISSING_DATE(HttpStatus.BAD_REQUEST),
- AMOUNT_ZERO(HttpStatus.BAD_REQUEST),
- MISSING_AMOUNT(HttpStatus.BAD_REQUEST),
- INVALID_BOOKING_ACCOUNTS(HttpStatus.BAD_REQUEST),
- MISSING_HOLIDAY_WEEKEND_TYPE(HttpStatus.BAD_REQUEST),
- INVALID_HOLIDAY_WEEKEND_TYPE(HttpStatus.BAD_REQUEST),
- MISSING_INTERVAL_TYPE(HttpStatus.BAD_REQUEST),
- INVALID_INTERVAL_TYPE(HttpStatus.BAD_REQUEST),
- MISSING_FIRST_OCCURRENCE(HttpStatus.BAD_REQUEST),
- INVALID_FIRST_OCCURRENCE_FORMAT(HttpStatus.BAD_REQUEST),
- INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.BAD_REQUEST),
- MISSING_RECURRING_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
- INVALID_RECURRING_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
- RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.BAD_REQUEST),
- MISSING_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
- INVALID_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
- TRANSACTION_NOT_FOUND(HttpStatus.BAD_REQUEST),
- ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
- DUPLICATE_ACCOUNT_KEY(HttpStatus.BAD_REQUEST),
- DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.BAD_REQUEST),
- ACCOUNT_GROUP_NOT_FOUND(HttpStatus.BAD_REQUEST);
-
- private HttpStatus httpStatus;
-
- ResponseReason(HttpStatus httpStatus) {
- this.httpStatus = httpStatus;
- }
-
- public ResponseEntity toResponseEntity() {
- return new ResponseEntity<>(this.name(), this.httpStatus);
- }
-}
diff --git a/financer-server/src/main/java/de/financer/config/FinancerConfig.java b/financer-server/src/main/java/de/financer/config/FinancerConfig.java
index d998e0c..6c830e3 100644
--- a/financer-server/src/main/java/de/financer/config/FinancerConfig.java
+++ b/financer-server/src/main/java/de/financer/config/FinancerConfig.java
@@ -6,7 +6,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
-import javax.management.ReflectionException;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collection;
@@ -68,10 +67,10 @@ public class FinancerConfig {
/**
* @return the date format used in e.g. the {@link de.financer.service.TransactionService#createTransaction(String,
- * String, Long, String, String) TransactionService#createTransaction} or {@link
+ * String, Long, String, String, Boolean)} TransactionService#createTransaction} or {@link
* de.financer.service.RecurringTransactionService#createRecurringTransaction(String, String, Long, String, String,
- * String, String, String) RecurringTransactionService#createRecurringTransaction} methods. Used to parse the
- * client-supplied date string to proper {@link java.time.LocalDate LocalDate} objects
+ * String, String, String, Boolean, Boolean)} RecurringTransactionService#createRecurringTransaction} methods. Used
+ * to parse the client-supplied date string to proper {@link java.time.LocalDate LocalDate} objects
*/
public String getDateFormat() {
return dateFormat;
diff --git a/financer-server/src/main/java/de/financer/controller/ControllerUtil.java b/financer-server/src/main/java/de/financer/controller/ControllerUtil.java
index bf5711e..dcaa841 100644
--- a/financer-server/src/main/java/de/financer/controller/ControllerUtil.java
+++ b/financer-server/src/main/java/de/financer/controller/ControllerUtil.java
@@ -11,7 +11,7 @@ public class ControllerUtil {
* @param toDecode the string to decode
* @return the decoded string in UTF-8 or, if UTF-8 is not available for whatever reason, the encoded string
*/
- public static final String urlDecode(String toDecode) {
+ public static String urlDecode(String toDecode) {
try {
return URLDecoder.decode(toDecode, StandardCharsets.UTF_8.name());
}
diff --git a/financer-server/src/main/java/de/financer/controller/RecurringTransactionController.java b/financer-server/src/main/java/de/financer/controller/RecurringTransactionController.java
index cc4684b..120bf14 100644
--- a/financer-server/src/main/java/de/financer/controller/RecurringTransactionController.java
+++ b/financer-server/src/main/java/de/financer/controller/RecurringTransactionController.java
@@ -10,8 +10,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
import java.util.Optional;
@RestController
diff --git a/financer-server/src/main/java/de/financer/controller/TransactionController.java b/financer-server/src/main/java/de/financer/controller/TransactionController.java
index 2f89fe6..b6b580d 100644
--- a/financer-server/src/main/java/de/financer/controller/TransactionController.java
+++ b/financer-server/src/main/java/de/financer/controller/TransactionController.java
@@ -2,125 +2,160 @@ package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.dto.ExpensePeriodTotal;
-import de.financer.model.Transaction;
+import de.financer.dto.SaveTransactionRequestDto;
+import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.service.TransactionService;
+import de.financer.service.parameter.SearchTransactionsParameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
-@RequestMapping("transactions")
public class TransactionController {
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionController.class);
@Autowired
private TransactionService transactionService;
- @RequestMapping("getAll")
- public Iterable getAll() {
- return this.transactionService.getAll();
- }
-
- @RequestMapping("getAllForAccount")
- public Iterable getAllForAccount(String accountKey) {
- final String decoded = ControllerUtil.urlDecode(accountKey);
+ @GetMapping("/transactionsByFql")
+ public Iterable searchTransactions(String fql) {
+ final String fqlDecoded = ControllerUtil.urlDecode(fql);
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String.format("/transactions/getAllForAccount got parameter: %s", decoded));
+ LOGGER.debug(String.format("GET /transactionsByFql/ got parameter: %s", fqlDecoded));
}
- return this.transactionService.getAllForAccount(decoded);
- }
-
- @RequestMapping(value = "createTransaction")
- public ResponseEntity createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
- String description, Boolean taxRelevant
- ) {
- final String decodedFrom = ControllerUtil.urlDecode(fromAccountKey);
- final String decodedTo = ControllerUtil.urlDecode(toAccountKey);
- final String decodedDesc = ControllerUtil.urlDecode(description);
+ final Iterable transactionSearchResponse =
+ this.transactionService.searchTransactions(fqlDecoded);
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String
- .format("/transactions/createTransaction got parameters: %s, %s, %s, %s, %s, %s",
- decodedFrom, decodedTo, amount, date, decodedDesc, taxRelevant));
+ LOGGER.debug(String.format("GET /transactionsByFql returns with %s", transactionSearchResponse));
+ }
+
+ return transactionSearchResponse;
+ }
+
+ @GetMapping("/transactions")
+ public Iterable searchTransactions(String fromAccountKey,
+ String toAccountKey,
+ String periodId,
+ String limit,
+ String order,
+ String taxRelevant,
+ String accountsAnd) {
+ final String fromDecoded = ControllerUtil.urlDecode(fromAccountKey);
+ final String toDecoded = ControllerUtil.urlDecode(toAccountKey);
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(String.format("GET /transactions/ got parameter: %s, %s, %s, %s, %s, %s - with order %s",
+ fromDecoded, toDecoded, periodId, limit, taxRelevant, accountsAnd, order));
+ }
+
+ // Wrap the url parameters into a proper POJO so the service parameter list wont get polluted
+ // There is no validation taking place here, only the wrapping. Validation happens in the service.
+ final SearchTransactionsParameter parameter = new SearchTransactionsParameter();
+
+ parameter.setFromAccountKey(fromDecoded);
+ parameter.setToAccountKey(toDecoded);
+ parameter.setLimit(limit);
+ parameter.setPeriodId(periodId);
+ parameter.setOrder(order);
+ parameter.setTaxRelevant(taxRelevant);
+ parameter.setAccountsAnd(accountsAnd);
+
+ final Iterable transactionSearchResponse =
+ this.transactionService.searchTransactions(parameter);
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(String.format("GET /transactions returns with %s", transactionSearchResponse));
+ }
+
+ return transactionSearchResponse;
+ }
+
+ @PostMapping(value = "/transactions")
+ public ResponseEntity createTransaction(@RequestBody SaveTransactionRequestDto requestDto) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(String.format("POST /transactions got parameters: %s", requestDto));
}
final ResponseReason responseReason = this.transactionService
- .createTransaction(decodedFrom, decodedTo, amount, date, decodedDesc, taxRelevant);
+ .createTransaction(requestDto.getFromAccountKey(),
+ requestDto.getToAccountKey(),
+ //TODO Conversion should be moved to
+ //TODO service and cause a proper response
+ //TODO if the value is not numeric
+ Long.valueOf(requestDto.getAmount()),
+ requestDto.getDate(),
+ requestDto.getDescription(),
+ requestDto.getTaxRelevant());
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String.format("/transactions/createTransaction returns with %s", responseReason.name()));
+ LOGGER.debug(String.format("POST /transactions returns with %s", responseReason.name()));
}
return responseReason.toResponseEntity();
}
- @RequestMapping("deleteTransaction")
- public ResponseEntity deleteTransaction(String transactionId) {
+ @DeleteMapping("/transactions/{transactionId}")
+ public ResponseEntity deleteTransaction(@PathVariable String transactionId) {
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String
- .format("/transactions/deleteTransaction got parameters: %s",
- transactionId));
+ LOGGER.debug(String.format("DELETE /transactions/{transactionId} got parameters: %s", transactionId));
}
- final ResponseReason responseReason = this.transactionService
- .deleteTransaction(transactionId);
+ final ResponseReason responseReason = this.transactionService.deleteTransaction(transactionId);
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String
- .format("/transactions/deleteTransaction returns with %s", responseReason.name()));
+ LOGGER.debug(String.format("DELETE /transactions/{transactionId} returns with %s", responseReason.name()));
}
return responseReason.toResponseEntity();
}
- @RequestMapping("getExpensesCurrentPeriod")
+ @GetMapping("/transactions/getExpensesCurrentPeriod")
public Long getExpensesCurrentPeriod() {
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod called"));
+ LOGGER.debug(String.format("GET /transactions/getExpensesCurrentPeriod called"));
}
final Long response = this.transactionService.getExpensesCurrentPeriod();
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod returns with %s", response));
+ LOGGER.debug(String.format("GET /transactions/getExpensesCurrentPeriod returns with %s", response));
}
return response;
}
- @RequestMapping("getExpensePeriodTotals")
+ @GetMapping("/transactions/getExpensePeriodTotals")
public Iterable getExpensePeriodTotals(Integer year) {
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String.format("/transactions/getExpensePeriodTotals got parameters: %s", year));
+ LOGGER.debug(String.format("GET /transactions/getExpensePeriodTotals got parameters: %s", year));
}
- final Iterable expensePeriodTotals = this.transactionService
- .getExpensePeriodTotals(year);
+ final Iterable expensePeriodTotals = this.transactionService.getExpensePeriodTotals(year);
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String.format("/transactions/getExpensePeriodTotals returns with %s", expensePeriodTotals));
+ LOGGER.debug(String.format("GET /transactions/getExpensePeriodTotals returns with %s", expensePeriodTotals));
}
return expensePeriodTotals;
}
- @RequestMapping("getExpensesAllPeriods")
+ @GetMapping("/transactions/getExpensesAllPeriods")
public List getExpensesAllPeriods() {
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String.format("/transactions/getExpensesAllPeriods called"));
+ LOGGER.debug(String.format("GET /transactions/getExpensesAllPeriods called"));
}
final List response = this.transactionService.getExpensesAllPeriods();
if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(String.format("/transactions/getExpensesAllPeriods returns with %s", response));
+ LOGGER.debug(String.format("GET /transactions/getExpensesAllPeriods returns with %s", response));
}
return response;
diff --git a/financer-server/src/main/java/de/financer/dba/PeriodRepository.java b/financer-server/src/main/java/de/financer/dba/PeriodRepository.java
index 27deeef..db8a872 100644
--- a/financer-server/src/main/java/de/financer/dba/PeriodRepository.java
+++ b/financer-server/src/main/java/de/financer/dba/PeriodRepository.java
@@ -7,7 +7,6 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
-import java.time.LocalDate;
import java.time.LocalDateTime;
@Transactional(propagation = Propagation.REQUIRED)
diff --git a/financer-server/src/main/java/de/financer/dba/RecurringTransactionRepository.java b/financer-server/src/main/java/de/financer/dba/RecurringTransactionRepository.java
index 07683be..2405893 100644
--- a/financer-server/src/main/java/de/financer/dba/RecurringTransactionRepository.java
+++ b/financer-server/src/main/java/de/financer/dba/RecurringTransactionRepository.java
@@ -4,7 +4,6 @@ import de.financer.model.Account;
import de.financer.model.RecurringTransaction;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
-import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
diff --git a/financer-server/src/main/java/de/financer/dba/TransactionRepository.java b/financer-server/src/main/java/de/financer/dba/TransactionRepository.java
index 5097b2c..16a85a5 100644
--- a/financer-server/src/main/java/de/financer/dba/TransactionRepository.java
+++ b/financer-server/src/main/java/de/financer/dba/TransactionRepository.java
@@ -7,11 +7,10 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
-import java.time.LocalDate;
import java.util.List;
@Transactional(propagation = Propagation.REQUIRED)
-public interface TransactionRepository extends CrudRepository {
+public interface TransactionRepository extends CrudRepository, TransactionRepositoryCustom {
@Query("SELECT t FROM Transaction t WHERE t.toAccount = :toAccount OR t.fromAccount = :fromAccount")
Iterable findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
diff --git a/financer-server/src/main/java/de/financer/dba/TransactionRepositoryCustom.java b/financer-server/src/main/java/de/financer/dba/TransactionRepositoryCustom.java
new file mode 100644
index 0000000..cd9461f
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/dba/TransactionRepositoryCustom.java
@@ -0,0 +1,18 @@
+package de.financer.dba;
+
+import de.financer.dto.Order;
+import de.financer.dto.SearchTransactionsResponseDto;
+import de.financer.model.Account;
+import de.financer.model.Period;
+
+public interface TransactionRepositoryCustom {
+ Iterable searchTransactions(Period period,
+ Account fromAccount,
+ Account toAccount,
+ Integer limit,
+ Order order,
+ Boolean taxRelevant,
+ boolean accountsAnd);
+
+ Iterable searchTransactions(String fql);
+}
diff --git a/financer-server/src/main/java/de/financer/dba/impl/TransactionRepositoryCustomImpl.java b/financer-server/src/main/java/de/financer/dba/impl/TransactionRepositoryCustomImpl.java
new file mode 100644
index 0000000..fb32d1a
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/dba/impl/TransactionRepositoryCustomImpl.java
@@ -0,0 +1,117 @@
+package de.financer.dba.impl;
+
+import de.financer.dba.TransactionRepositoryCustom;
+import de.financer.dto.Order;
+import de.financer.dto.SearchTransactionsResponseDto;
+import de.financer.fql.FQLLexer;
+import de.financer.fql.FQLParser;
+import de.financer.fql.FQLVisitorImpl;
+import de.financer.model.*;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.springframework.stereotype.Repository;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.persistence.TypedQuery;
+import javax.persistence.criteria.*;
+import java.util.ArrayList;
+import java.util.List;
+
+@Repository
+public class TransactionRepositoryCustomImpl implements TransactionRepositoryCustom {
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Override
+ public Iterable searchTransactions(Period period,
+ Account fromAccount,
+ Account toAccount,
+ Integer limit,
+ Order order,
+ Boolean taxRelevant,
+ boolean accountsAnd) {
+ final CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
+ final CriteriaQuery criteriaQuery = criteriaBuilder
+ .createQuery(SearchTransactionsResponseDto.class);
+ final Root fromTransaction = criteriaQuery.from(Transaction.class);
+
+ final List predicates = new ArrayList<>();
+ Predicate fromAccountPredicate = null;
+ Predicate toAccountPredicate = null;
+
+ if (taxRelevant != null) {
+ predicates.add(criteriaBuilder.equal(fromTransaction.get(Transaction_.taxRelevant), taxRelevant));
+ }
+
+ if (fromAccount != null) {
+ fromAccountPredicate = criteriaBuilder.equal(fromTransaction.get(Transaction_.fromAccount), fromAccount);
+ }
+
+ if (toAccount != null) {
+ toAccountPredicate = criteriaBuilder.equal(fromTransaction.get(Transaction_.toAccount), toAccount);
+ }
+
+ if (period != null) {
+ final SetJoin periodJoin = fromTransaction.join(Transaction_.periods);
+
+ predicates.add(criteriaBuilder.equal(periodJoin.get(Period_.id), period.getId()));
+ }
+
+ if (fromAccountPredicate != null && toAccountPredicate != null) {
+ if (accountsAnd) {
+ predicates.add(criteriaBuilder.and(fromAccountPredicate, toAccountPredicate));
+ }
+ else {
+ predicates.add(criteriaBuilder.or(fromAccountPredicate, toAccountPredicate));
+ }
+ }
+ else if (fromAccountPredicate != null) {
+ predicates.add(fromAccountPredicate);
+ }
+ else if (toAccountPredicate != null) {
+ predicates.add(toAccountPredicate);
+ }
+ // else: both null, nothing to do
+
+ criteriaQuery.where(predicates.toArray(new Predicate[]{}));
+
+ switch(order) {
+ case TRANSACTIONS_BY_DATE_DESC:
+ // either leave case as last before default if new cases arrive
+ // or copy the expression
+ default:
+ criteriaQuery.orderBy(criteriaBuilder.desc(fromTransaction.get(Transaction_.date)));
+ }
+
+ criteriaQuery.select(criteriaBuilder.construct(SearchTransactionsResponseDto.class,
+ fromTransaction.get(Transaction_.id),
+ fromTransaction.get(Transaction_.fromAccount),
+ fromTransaction.get(Transaction_.toAccount),
+ fromTransaction.get(Transaction_.date),
+ fromTransaction.get(Transaction_.description),
+ fromTransaction.get(Transaction_.amount),
+ fromTransaction.get(Transaction_.taxRelevant),
+ criteriaBuilder.isNotNull(fromTransaction.get(Transaction_.recurringTransaction))));
+
+ final TypedQuery query = this.entityManager.createQuery(criteriaQuery);
+
+ if (limit != null) {
+ query.setMaxResults(limit);
+ }
+
+ return query.getResultList();
+ }
+
+ @Override
+ public Iterable searchTransactions(String fql) {
+ final FQLLexer lexer = new FQLLexer(CharStreams.fromString(fql));
+ final FQLParser parser = new FQLParser(new CommonTokenStream(lexer));
+ final CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
+ final FQLVisitorImpl visitor = new FQLVisitorImpl(criteriaBuilder);
+
+ visitor.visitQuery(parser.query());
+
+ return this.entityManager.createQuery(visitor.getCriteriaQuery()).getResultList();
+ }
+}
diff --git a/financer-server/src/main/java/de/financer/fql/FQLVisitorImpl.java b/financer-server/src/main/java/de/financer/fql/FQLVisitorImpl.java
new file mode 100644
index 0000000..74ccc44
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/FQLVisitorImpl.java
@@ -0,0 +1,230 @@
+package de.financer.fql;
+
+import de.financer.dto.SearchTransactionsResponseDto;
+import de.financer.fql.field_handler.BetweenIntHandler;
+import de.financer.fql.field_handler.BetweenStringHandler;
+import de.financer.fql.field_handler.PeriodConstHandler;
+import de.financer.fql.join_handler.JoinKey;
+import de.financer.fql.field_handler.IntHandler;
+import de.financer.model.*;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.persistence.criteria.*;
+import java.util.*;
+
+public class FQLVisitorImpl extends FQLBaseVisitor {
+
+ private final CriteriaBuilder criteriaBuilder;
+ private final CriteriaQuery criteriaQuery;
+ private final Root criteriaRoot;
+ private final Map> froms;
+
+ public FQLVisitorImpl(CriteriaBuilder criteriaBuilder) {
+ this.criteriaBuilder = criteriaBuilder;
+ this.criteriaQuery = criteriaBuilder.createQuery(SearchTransactionsResponseDto.class);
+ this.criteriaRoot = criteriaQuery.from(Transaction.class);
+
+ this.froms = new HashMap<>();
+ this.froms.put(JoinKey.of(Transaction.class), this.criteriaRoot);
+ }
+
+ public CriteriaQuery getCriteriaQuery() {
+ return this.criteriaQuery;
+ }
+
+ @Override
+ public Predicate visitQuery(FQLParser.QueryContext ctx) {
+ buildSelect();
+
+ final Predicate predicateTree = ctx.getChild(0).accept(this);
+
+ // ORDER BY
+ if (ctx.getChildCount() > 1) {
+ ctx.getChild(1).accept(this);
+ }
+
+ this.criteriaQuery.where(predicateTree);
+
+ return predicateTree;
+ }
+
+ private void buildSelect() {
+ criteriaQuery.select(criteriaBuilder.construct(SearchTransactionsResponseDto.class,
+ this.criteriaRoot.get(Transaction_.id),
+ this.criteriaRoot.get(Transaction_.fromAccount),
+ this.criteriaRoot.get(Transaction_.toAccount),
+ this.criteriaRoot.get(Transaction_.date),
+ this.criteriaRoot.get(Transaction_.description),
+ this.criteriaRoot.get(Transaction_.amount),
+ this.criteriaRoot.get(Transaction_.taxRelevant),
+ criteriaBuilder.isNotNull(this.criteriaRoot.get(Transaction_.recurringTransaction))));
+ }
+
+ @Override
+ public Predicate visitOrExpression(FQLParser.OrExpressionContext ctx) {
+ if (ctx.getChildCount() == 1) {
+ return ctx.getChild(0).accept(this);
+ } else {
+ final List predicates = new ArrayList<>();
+
+ for (int i = 0; i < ctx.getChildCount(); i++) {
+ ParseTree c = ctx.getChild(i);
+ Predicate p = c.accept(this);
+
+ if (p != null) {
+ predicates.add(p);
+ }
+ }
+
+ return this.criteriaBuilder.or(predicates.toArray(new Predicate[]{}));
+ }
+ }
+
+ @Override
+ public Predicate visitAndExpression(FQLParser.AndExpressionContext ctx) {
+ if (ctx.getChildCount() == 1) {
+ return ctx.getChild(0).accept(this);
+ } else {
+ final List predicates = new ArrayList<>();
+
+ for (int i = 0; i < ctx.getChildCount(); i++) {
+ ParseTree c = ctx.getChild(i);
+ Predicate p = c.accept(this);
+
+ if (p != null) {
+ predicates.add(p);
+ }
+ }
+
+ return this.criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
+ }
+ }
+
+ @Override
+ public Predicate visitExpression(FQLParser.ExpressionContext ctx) {
+ return ctx.getChild(0).accept(this);
+ }
+
+ @Override
+ public Predicate visitParenthesisExpression(FQLParser.ParenthesisExpressionContext ctx) {
+ return ctx.expr.accept(this);
+ }
+
+ @Override
+ public Predicate visitRegularExpression(FQLParser.RegularExpressionContext ctx) {
+ return ctx.getChild(0).accept(this);
+ }
+
+ @Override
+ public Predicate visitBooleanExpression(FQLParser.BooleanExpressionContext ctx) {
+ final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
+ final Boolean value = Boolean.parseBoolean(ctx.value.getText());
+
+ fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
+
+ return fieldMapping.getFieldHandler().apply(fieldMapping, this.froms, this.criteriaBuilder, value);
+ }
+
+ @Override
+ public Predicate visitStringExpression(FQLParser.StringExpressionContext ctx) {
+ final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
+
+ fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
+
+ return fieldMapping.getFieldHandler()
+ .apply(fieldMapping, this.froms, this.criteriaBuilder, ctx.value.getText());
+ }
+
+ @Override
+ public Predicate visitIntExpression(FQLParser.IntExpressionContext ctx) {
+ final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
+ final IntHandler.IntHandlerParameterContainer con = IntHandler.IntHandlerParameterContainer
+ .of(ctx.operator.getText(), ctx.value.getText());
+
+ fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
+
+ return fieldMapping.getFieldHandler().apply(fieldMapping, this.froms, this.criteriaBuilder, con);
+ }
+
+ @Override
+ public Predicate visitDateExpression(FQLParser.DateExpressionContext ctx) {
+ final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
+
+ fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
+
+ return fieldMapping.getFieldHandler()
+ .apply(fieldMapping, this.froms, this.criteriaBuilder, ctx.value.getText());
+ }
+
+ @Override
+ public Predicate visitBetweenExpression(FQLParser.BetweenExpressionContext ctx) {
+ return ctx.getChild(0).accept(this);
+ }
+
+ @Override
+ public Predicate visitBetweenStringExpression(FQLParser.BetweenStringExpressionContext ctx) {
+ final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
+ final BetweenStringHandler.BetweenStringHandlerParameterContainer con = BetweenStringHandler.BetweenStringHandlerParameterContainer
+ .of(ctx.left.getText(), ctx.right.getText());
+
+ fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
+
+ // Overwrite the actual field handler of the field
+ return new BetweenStringHandler().apply(fieldMapping, this.froms, this.criteriaBuilder, con);
+ }
+
+ @Override
+ public Predicate visitBetweenIntExpression(FQLParser.BetweenIntExpressionContext ctx) {
+ final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
+ final BetweenIntHandler.BetweenIntHandlerParameterContainer con = BetweenIntHandler.BetweenIntHandlerParameterContainer
+ .of(ctx.left.getText(), ctx.right.getText());
+
+ fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
+
+ // Overwrite the actual field handler of the field
+ return new BetweenIntHandler().apply(fieldMapping, this.froms, this.criteriaBuilder, con);
+ }
+
+ @Override
+ public Predicate visitPeriodExpression(FQLParser.PeriodExpressionContext ctx) {
+ return ctx.getChild(0).accept(this);
+ }
+
+ @Override
+ public Predicate visitPeriodConstExpression(FQLParser.PeriodConstExpressionContext ctx) {
+ final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
+ final PeriodConstHandler.PeriodConstHandlerParameterContainer con = PeriodConstHandler.PeriodConstHandlerParameterContainer
+ .of(ctx.value.getText(), this.criteriaQuery);
+
+ fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
+
+ return fieldMapping.getFieldHandler().apply(fieldMapping, this.froms, this.criteriaBuilder, con);
+ }
+
+ @Override
+ public Predicate visitOrderByExpression(FQLParser.OrderByExpressionContext ctx) {
+ final FieldMapping fieldMapping = FieldMapping.findByFieldName(ctx.field.getText());
+ final String orderValue = StringUtils.upperCase(ctx.order.getText());
+ Order order;
+
+ fieldMapping.getJoinHandler().apply(this.froms, fieldMapping);
+
+ switch (orderValue) {
+ case "DESC":
+ order = this.criteriaBuilder
+ .desc(this.froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()));
+ break;
+ case "ASC":
+ order = this.criteriaBuilder
+ .asc(this.froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()));
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown order value: %s", ctx.order.getText()));
+ }
+
+ this.criteriaQuery.orderBy(order);
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/financer-server/src/main/java/de/financer/fql/FieldMapping.java b/financer-server/src/main/java/de/financer/fql/FieldMapping.java
new file mode 100644
index 0000000..4ae4d32
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/FieldMapping.java
@@ -0,0 +1,119 @@
+package de.financer.fql;
+
+import de.financer.fql.field_handler.*;
+import de.financer.fql.join_handler.*;
+import de.financer.model.*;
+
+import java.util.Arrays;
+
+public enum FieldMapping {
+ AMOUNT("amount", Transaction_.AMOUNT, Transaction.class, NoopJoinHandler.class, JoinKey
+ .of(Transaction.class), null, IntHandler.class),
+
+ PERIOD("period", Period_.ID, Period.class, PeriodJoinHandler.class, JoinKey.of(Period.class), null, PeriodConstHandler.class),
+
+ FROM_ACCOUNT("fromAccount", Account_.KEY, Account.class, FromAccountJoinHandler.class, JoinKey
+ .of(Account.class, "FROM"), null, StringHandler.class),
+
+ TO_ACCOUNT("toAccount", Account_.KEY, Account.class, ToAccountJoinHandler.class, JoinKey
+ .of(Account.class, "TO"), null, StringHandler.class),
+
+ FROM_ACCOUNT_GROUP("fromAccountGroup", AccountGroup_.NAME, AccountGroup.class,
+ AccountGroupJoinHandler.class, JoinKey.of(AccountGroup.class, "FROM"), FROM_ACCOUNT, StringHandler.class),
+
+ TO_ACCOUNT_GROUP("toAccountGroup", AccountGroup_.NAME, AccountGroup.class,
+ AccountGroupJoinHandler.class, JoinKey.of(AccountGroup.class, "TO"), TO_ACCOUNT, StringHandler.class),
+
+ DATE("date", Transaction_.DATE, Transaction.class, NoopJoinHandler.class, JoinKey.of(Transaction.class), null, DateHandler.class),
+
+ RECURRING("recurring", Transaction_.RECURRING_TRANSACTION, Transaction.class, NoopJoinHandler.class,
+ JoinKey.of(Transaction.class), null, NotNullSyntheticHandler.class),
+
+ TAX_RELEVANT("taxRelevant", Transaction_.TAX_RELEVANT, Transaction.class, NoopJoinHandler.class,
+ JoinKey.of(Transaction.class), null, BooleanHandler.class);
+
+ /**
+ * The name of the field as used in FQL
+ */
+ private final String fieldName;
+
+ /**
+ * The name of the ORM field
+ */
+ private final String attributeName;
+
+ /**
+ * The entity this field belongs to
+ */
+ private final Class> entityClass;
+
+ /**
+ * The join handler responsible for constructing the join if the field is not part of the root entity
+ */
+ private final Class extends JoinHandler> joinHandlerClass;
+
+ /**
+ * A synthetic key to uniquely identify the join required for this field
+ */
+ private final JoinKey joinKey;
+
+ /**
+ * The field mapping this field depends on if the entity is not directly reachable from the root entity
+ */
+ private final FieldMapping dependingJoin;
+
+ /**
+ * The handler for this FQL field handling the translation to a JPA criteria
+ */
+ private final Class extends FieldHandler> fieldHandlerClass;
+
+ FieldMapping(String fieldName, String attributeName, Class> entityClass,
+ Class extends JoinHandler> joinHandlerClass, JoinKey joinKey,
+ FieldMapping dependingJoin, Class extends FieldHandler> fieldHandlerClass
+ ) {
+ this.fieldName = fieldName;
+ this.attributeName = attributeName;
+ this.entityClass = entityClass;
+ this.joinHandlerClass = joinHandlerClass;
+ this.joinKey = joinKey;
+ this.dependingJoin = dependingJoin;
+ this.fieldHandlerClass = fieldHandlerClass;
+ }
+
+ public static FieldMapping findByFieldName(String fqlFieldName) {
+ return Arrays.stream(values()).filter(fm -> fm.fieldName.equalsIgnoreCase(fqlFieldName)).findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("Unknown field: " + fqlFieldName));
+ }
+
+ public String getAttributeName() {
+ return attributeName;
+ }
+
+ public Class> getEntityClass() {
+ return entityClass;
+ }
+
+ public JoinHandler getJoinHandler() {
+ try {
+ return this.joinHandlerClass.getDeclaredConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("No public, parameterless constructor found!");
+ }
+ }
+
+ public FieldHandler getFieldHandler() {
+ try {
+ return this.fieldHandlerClass.getDeclaredConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("No public, parameterless constructor found!");
+ }
+ }
+
+ public JoinKey getJoinKey() {
+ return this.joinKey;
+ }
+
+ public FieldMapping getDependingJoin() {
+ return this.dependingJoin;
+ }
+}
diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/BetweenIntHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/BetweenIntHandler.java
new file mode 100644
index 0000000..f3a914f
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/field_handler/BetweenIntHandler.java
@@ -0,0 +1,32 @@
+package de.financer.fql.field_handler;
+
+import de.financer.fql.FieldMapping;
+import de.financer.fql.join_handler.JoinKey;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Predicate;
+import java.util.Map;
+
+public class BetweenIntHandler implements FieldHandler {
+ public static class BetweenIntHandlerParameterContainer {
+ private Long left;
+ private Long right;
+
+ public static BetweenIntHandlerParameterContainer of(String left, String right) {
+ final BetweenIntHandlerParameterContainer con = new BetweenIntHandlerParameterContainer();
+
+ con.left = Long.valueOf(left);
+ con.right = Long.valueOf(right);
+
+ return con;
+ }
+ }
+
+ @Override
+ public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, BetweenIntHandlerParameterContainer con) {
+ return criteriaBuilder
+ .between(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), con.left, con.right);
+ }
+
+}
diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/BetweenStringHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/BetweenStringHandler.java
new file mode 100644
index 0000000..f6cd60b
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/field_handler/BetweenStringHandler.java
@@ -0,0 +1,38 @@
+package de.financer.fql.field_handler;
+
+import de.financer.fql.FieldMapping;
+import de.financer.fql.join_handler.JoinKey;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Predicate;
+import java.util.Map;
+
+public class BetweenStringHandler implements FieldHandler {
+ public static class BetweenStringHandlerParameterContainer {
+ private String left;
+ private String right;
+
+ public static BetweenStringHandlerParameterContainer of(String left, String right) {
+ final BetweenStringHandlerParameterContainer con = new BetweenStringHandlerParameterContainer();
+
+ con.left = left;
+ con.right = right;
+
+ return con;
+ }
+ }
+
+ @Override
+ public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, BetweenStringHandlerParameterContainer con) {
+ return criteriaBuilder
+ .between(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()),
+ removeQuotes(con.left),
+ removeQuotes(con.right));
+ }
+
+ private String removeQuotes(String value) {
+ return value.replaceAll("'", "");
+ }
+
+}
diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/BooleanHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/BooleanHandler.java
new file mode 100644
index 0000000..645704b
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/field_handler/BooleanHandler.java
@@ -0,0 +1,16 @@
+package de.financer.fql.field_handler;
+
+import de.financer.fql.FieldMapping;
+import de.financer.fql.join_handler.JoinKey;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Predicate;
+import java.util.Map;
+
+public class BooleanHandler implements FieldHandler {
+ @Override
+ public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, Boolean value) {
+ return criteriaBuilder.equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), value);
+ }
+}
diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/DateHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/DateHandler.java
new file mode 100644
index 0000000..4fceef2
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/field_handler/DateHandler.java
@@ -0,0 +1,21 @@
+package de.financer.fql.field_handler;
+
+import de.financer.fql.FieldMapping;
+import de.financer.fql.join_handler.JoinKey;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Predicate;
+import java.time.LocalDate;
+import java.util.Map;
+
+public class DateHandler implements FieldHandler {
+ @Override
+ public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, String value) {
+ return criteriaBuilder.equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), parseDate(value));
+ }
+
+ private LocalDate parseDate(String value) {
+ return LocalDate.parse(value);
+ }
+}
diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/FieldHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/FieldHandler.java
new file mode 100644
index 0000000..99fff15
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/field_handler/FieldHandler.java
@@ -0,0 +1,13 @@
+package de.financer.fql.field_handler;
+
+import de.financer.fql.FieldMapping;
+import de.financer.fql.join_handler.JoinKey;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Predicate;
+import java.util.Map;
+
+public interface FieldHandler {
+ Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, T value);
+}
diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/IntHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/IntHandler.java
new file mode 100644
index 0000000..fdd671d
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/field_handler/IntHandler.java
@@ -0,0 +1,55 @@
+package de.financer.fql.field_handler;
+
+import de.financer.fql.FieldMapping;
+import de.financer.fql.join_handler.JoinKey;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Path;
+import javax.persistence.criteria.Predicate;
+import java.util.Map;
+
+public class IntHandler implements FieldHandler {
+ public static class IntHandlerParameterContainer {
+ private String operator;
+ private Long value;
+
+ public static IntHandlerParameterContainer of(String operator, String value) {
+ IntHandlerParameterContainer con = new IntHandlerParameterContainer();
+
+ con.operator = operator;
+ con.value = Long.valueOf(value);
+
+ return con;
+ }
+ }
+
+ @Override
+ public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, IntHandlerParameterContainer con) {
+ final Path path = froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName());
+ Predicate retVal = null;
+
+ switch (con.operator) {
+ case "=":
+ retVal = criteriaBuilder.equal(path, con.value);
+ break;
+ case ">":
+ retVal = criteriaBuilder.gt(path, con.value);
+ break;
+ case "<":
+ retVal = criteriaBuilder.lt(path, con.value);
+ break;
+ case ">=":
+ retVal = criteriaBuilder.greaterThanOrEqualTo(path, con.value);
+ break;
+ case "<=":
+ retVal = criteriaBuilder.lessThanOrEqualTo(path, con.value);
+ break;
+ case "!=":
+ retVal = criteriaBuilder.notEqual(path, con.value);
+ break;
+ }
+
+ return retVal;
+ }
+}
diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/NotNullSyntheticHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/NotNullSyntheticHandler.java
new file mode 100644
index 0000000..33299fe
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/field_handler/NotNullSyntheticHandler.java
@@ -0,0 +1,20 @@
+package de.financer.fql.field_handler;
+
+import de.financer.fql.FieldMapping;
+import de.financer.fql.join_handler.JoinKey;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Predicate;
+import java.util.Map;
+
+public class NotNullSyntheticHandler implements FieldHandler {
+ @Override
+ public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, Boolean value) {
+ if (value) {
+ return criteriaBuilder.isNotNull(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()));
+ } else {
+ return criteriaBuilder.isNull(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()));
+ }
+ }
+}
diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/PeriodConstHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/PeriodConstHandler.java
new file mode 100644
index 0000000..47db03c
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/field_handler/PeriodConstHandler.java
@@ -0,0 +1,73 @@
+package de.financer.fql.field_handler;
+
+import de.financer.dto.SearchTransactionsResponseDto;
+import de.financer.fql.FieldMapping;
+import de.financer.fql.join_handler.JoinKey;
+import de.financer.model.Period;
+import de.financer.model.PeriodType;
+import de.financer.model.Period_;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.persistence.criteria.*;
+import java.util.Map;
+
+public class PeriodConstHandler implements FieldHandler {
+ public static class PeriodConstHandlerParameterContainer {
+ private String value;
+ private CriteriaQuery criteriaQuery;
+
+ public static PeriodConstHandlerParameterContainer of(String value, CriteriaQuery criteriaQuery) {
+ final PeriodConstHandlerParameterContainer con = new PeriodConstHandlerParameterContainer();
+
+ con.value = value;
+ con.criteriaQuery = criteriaQuery;
+
+ return con;
+ }
+ }
+
+ @Override
+ public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, PeriodConstHandlerParameterContainer con) {
+ final Subquery subquery = con.criteriaQuery.subquery(Long.class);
+ final Root subFrom = subquery.from(Period.class);
+ final String upperValue = StringUtils.upperCase(con.value);
+
+ subquery.select(subFrom.get(fieldMapping.getAttributeName()));
+
+ // Generate subselect for each period value
+ switch (upperValue) {
+ case "CURRENT":
+ subquery.where(criteriaBuilder.and(criteriaBuilder
+ .equal(subFrom.get(Period_.TYPE), PeriodType.EXPENSE), criteriaBuilder
+ .isNull(subFrom.get(Period_.END))));
+ break;
+ case "LAST":
+ subquery.where(criteriaBuilder.and(criteriaBuilder
+ .equal(subFrom.get(Period_.TYPE), PeriodType.EXPENSE), criteriaBuilder
+ .isNotNull(subFrom.get(Period_.END))));
+
+ // subquery.orderBy -> orderBy not supported by JPA criteria
+ // So we need to workaround this by misusing the autoincremented ID
+ subquery.select(criteriaBuilder.max(subFrom.get(fieldMapping.getAttributeName())));
+ break;
+ case "CURRENT_YEAR":
+ subquery.where(criteriaBuilder.and(criteriaBuilder
+ .equal(subFrom.get(Period_.TYPE), PeriodType.EXPENSE_YEAR), criteriaBuilder
+ .isNull(subFrom.get(Period_.END))));
+ break;
+ case "LAST_YEAR":
+ subquery.where(criteriaBuilder.and(criteriaBuilder
+ .equal(subFrom.get(Period_.TYPE), PeriodType.EXPENSE_YEAR), criteriaBuilder
+ .isNotNull(subFrom.get(Period_.END))));
+
+ subquery.select(criteriaBuilder.max(subFrom.get(fieldMapping.getAttributeName())));
+ break;
+ case "GRAND_TOTAL":
+ subquery.where(criteriaBuilder.equal(subFrom.get(Period_.TYPE), PeriodType.GRAND_TOTAL));
+ break;
+ }
+
+ return criteriaBuilder.equal(froms.get(fieldMapping.getJoinKey())
+ .get(fieldMapping.getAttributeName()), subquery);
+ }
+}
diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/StringHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/StringHandler.java
new file mode 100644
index 0000000..8cb389d
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/field_handler/StringHandler.java
@@ -0,0 +1,21 @@
+package de.financer.fql.field_handler;
+
+import de.financer.fql.FieldMapping;
+import de.financer.fql.join_handler.JoinKey;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Predicate;
+import java.util.Map;
+
+public class StringHandler implements FieldHandler {
+ @Override
+ public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, String value) {
+ return criteriaBuilder
+ .equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), removeQuotes(value));
+ }
+
+ private String removeQuotes(String value) {
+ return value.replaceAll("'", "");
+ }
+}
diff --git a/financer-server/src/main/java/de/financer/fql/join_handler/AccountGroupJoinHandler.java b/financer-server/src/main/java/de/financer/fql/join_handler/AccountGroupJoinHandler.java
new file mode 100644
index 0000000..5e791d4
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/join_handler/AccountGroupJoinHandler.java
@@ -0,0 +1,32 @@
+package de.financer.fql.join_handler;
+
+import de.financer.fql.FieldMapping;
+import de.financer.model.Account;
+import de.financer.model.AccountGroup;
+import de.financer.model.Account_;
+import de.financer.model.Transaction;
+
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Join;
+import java.util.Map;
+
+public class AccountGroupJoinHandler implements JoinHandler {
+ @Override
+ public Void apply(Map> joinKeyFromMap, FieldMapping fieldMapping) {
+ final FieldMapping dependingJoin = fieldMapping.getDependingJoin();
+
+ if (dependingJoin != null) {
+ dependingJoin.getJoinHandler().apply(joinKeyFromMap, dependingJoin);
+ }
+
+ if (joinKeyFromMap.get(fieldMapping.getJoinKey()) == null) {
+ final Join accountGroupJoin = ((Join) joinKeyFromMap
+ .get(dependingJoin.getJoinKey()))
+ .join(Account_.accountGroup);
+
+ joinKeyFromMap.put(fieldMapping.getJoinKey(), accountGroupJoin);
+ }
+
+ return null;
+ }
+}
diff --git a/financer-server/src/main/java/de/financer/fql/join_handler/FromAccountJoinHandler.java b/financer-server/src/main/java/de/financer/fql/join_handler/FromAccountJoinHandler.java
new file mode 100644
index 0000000..65d2061
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/join_handler/FromAccountJoinHandler.java
@@ -0,0 +1,26 @@
+package de.financer.fql.join_handler;
+
+import de.financer.fql.FieldMapping;
+import de.financer.model.Account;
+import de.financer.model.Transaction;
+import de.financer.model.Transaction_;
+
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Join;
+import javax.persistence.criteria.Root;
+import java.util.Map;
+
+public class FromAccountJoinHandler implements JoinHandler {
+ @Override
+ public Void apply(Map> joinKeyFromMap, FieldMapping fieldMapping) {
+ if (joinKeyFromMap.get(fieldMapping.getJoinKey()) == null) {
+ final Join accountJoin = ((Root) joinKeyFromMap
+ .get(JoinKey.of(Transaction.class)))
+ .join(Transaction_.fromAccount);
+
+ joinKeyFromMap.put(fieldMapping.getJoinKey(), accountJoin);
+ }
+
+ return null;
+ }
+}
diff --git a/financer-server/src/main/java/de/financer/fql/join_handler/JoinHandler.java b/financer-server/src/main/java/de/financer/fql/join_handler/JoinHandler.java
new file mode 100644
index 0000000..ac19cb7
--- /dev/null
+++ b/financer-server/src/main/java/de/financer/fql/join_handler/JoinHandler.java
@@ -0,0 +1,10 @@
+package de.financer.fql.join_handler;
+
+import de.financer.fql.FieldMapping;
+
+import javax.persistence.criteria.From;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+public interface JoinHandler extends BiFunction