From d4fef7590388676f68f07d52b78939bf4bb67931 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sat, 14 Mar 2020 23:44:43 +0100 Subject: [PATCH] Add FQL and rework /transaction endpoint --- financer-common/pom.xml | 4 + .../main/java/de/financer/ResponseReason.java | 15 +- .../src/main/java/de/financer/dto/Order.java | 30 +++ .../dto/SaveTransactionRequestDto.java | 65 +++++ .../dto/SearchTransactionsRequestDto.java | 65 +++++ .../dto/SearchTransactionsResponseDto.java | 102 ++++++++ .../TransactionsByDateByIdDescComparator.java | 14 ++ .../financer/dto/comparator/package-info.java | 4 + .../main/java/de/financer/model/Account.java | 1 + financer-server/pom.xml | 27 ++ .../src/main/antlr4/de/financer/fql/FQL.g4 | 103 ++++++++ .../V22_0_1__calculateAccountStatistics.java | 8 +- .../V26_0_0__createGrandTotalPeriod.java | 8 +- ...V26_0_1__createExpenseYearPeriod_2019.java | 8 +- ...V26_0_2__createExpenseYearPeriod_2020.java | 9 +- .../main/java/de/financer/ResponseReason.java | 45 ---- .../de/financer/config/FinancerConfig.java | 7 +- .../financer/controller/ControllerUtil.java | 2 +- .../RecurringTransactionController.java | 2 - .../controller/TransactionController.java | 131 ++++++---- .../de/financer/dba/PeriodRepository.java | 1 - .../dba/RecurringTransactionRepository.java | 1 - .../financer/dba/TransactionRepository.java | 3 +- .../dba/TransactionRepositoryCustom.java | 18 ++ .../impl/TransactionRepositoryCustomImpl.java | 117 +++++++++ .../java/de/financer/fql/FQLVisitorImpl.java | 230 ++++++++++++++++++ .../java/de/financer/fql/FieldMapping.java | 119 +++++++++ .../fql/field_handler/BetweenIntHandler.java | 32 +++ .../field_handler/BetweenStringHandler.java | 38 +++ .../fql/field_handler/BooleanHandler.java | 16 ++ .../fql/field_handler/DateHandler.java | 21 ++ .../fql/field_handler/FieldHandler.java | 13 + .../fql/field_handler/IntHandler.java | 55 +++++ .../NotNullSyntheticHandler.java | 20 ++ .../fql/field_handler/PeriodConstHandler.java | 73 ++++++ .../fql/field_handler/StringHandler.java | 21 ++ .../join_handler/AccountGroupJoinHandler.java | 32 +++ .../join_handler/FromAccountJoinHandler.java | 26 ++ .../fql/join_handler/JoinHandler.java | 10 + .../de/financer/fql/join_handler/JoinKey.java | 36 +++ .../fql/join_handler/NoopJoinHandler.java | 13 + .../fql/join_handler/PeriodJoinHandler.java | 28 +++ .../join_handler/ToAccountJoinHandler.java | 26 ++ .../de/financer/service/AccountService.java | 3 +- .../de/financer/service/PeriodService.java | 7 +- .../financer/service/TransactionService.java | 117 +++++++-- .../exception/FinancerServiceException.java | 15 ++ .../RestControllerExceptionAdvisor.java | 17 ++ .../SearchTransactionsParameter.java | 67 +++++ .../SendRecurringTransactionReminderTask.java | 29 +-- .../AccountService_createAccountTest.java | 1 - ...ervice_createRecurringTransactionTest.java | 30 +-- ...nsactionService_createTransactionTest.java | 32 +-- ...nsactionService_deleteTransactionTest.java | 7 +- ...dRecurringTransactionReminderTaskTest.java | 9 +- .../java/de/financer/FinancerApplication.java | 2 - .../java/de/financer/chart/ChartType.java | 6 +- .../expense/AccountExpensesGenerator.java | 4 +- .../AccountGroupExpensesGenerator.java | 2 +- .../inline/ExpensesAllPeriodsGenerator.java | 2 +- .../impl/total/PeriodTotalGenerator.java | 2 +- .../controller/AccountController.java | 77 +++--- .../java/de/financer/controller/Function.java | 10 +- .../controller/TransactionController.java | 158 ++++++------ .../NoExceptionResponseErrorHandler.java | 18 -- .../financer/decorator/AccountDecorator.java | 6 +- .../financer/form/SearchTransactionsForm.java | 13 + .../template/FinancerRestTemplate.java | 142 +++++++++-- .../template/GetAccountExpensesTemplate.java | 1 - .../GetAccountGroupExpensesTemplate.java | 2 - .../GetAllTransactionsForAccountTemplate.java | 21 -- .../GetExpensesAllPeriodsTemplate.java | 1 - .../template/SearchTransactionsTemplate.java | 52 ++++ .../exception/FinancerRestException.java | 15 ++ .../de/financer/util/ControllerUtils.java | 4 + .../de/financer/util/TransactionUtils.java | 4 +- .../main/resources/i18n/message.properties | 36 +-- .../resources/i18n/message_de_DE.properties | 48 ++-- .../src/main/resources/static/changelog.txt | 5 +- .../src/main/resources/static/css/main.css | 26 +- .../src/main/resources/static/readme.txt | 14 +- .../templates/account/accountDetails.html | 31 +-- .../templates/account/accountOverview.html | 1 + .../transaction/searchTransactions.html | 61 +++++ .../transaction/transactionList.html | 32 +++ 85 files changed, 2257 insertions(+), 472 deletions(-) rename {financer-web-client => financer-common}/src/main/java/de/financer/ResponseReason.java (81%) create mode 100644 financer-common/src/main/java/de/financer/dto/Order.java create mode 100644 financer-common/src/main/java/de/financer/dto/SaveTransactionRequestDto.java create mode 100644 financer-common/src/main/java/de/financer/dto/SearchTransactionsRequestDto.java create mode 100644 financer-common/src/main/java/de/financer/dto/SearchTransactionsResponseDto.java create mode 100644 financer-common/src/main/java/de/financer/dto/comparator/TransactionsByDateByIdDescComparator.java create mode 100644 financer-common/src/main/java/de/financer/dto/comparator/package-info.java create mode 100644 financer-server/src/main/antlr4/de/financer/fql/FQL.g4 delete mode 100644 financer-server/src/main/java/de/financer/ResponseReason.java create mode 100644 financer-server/src/main/java/de/financer/dba/TransactionRepositoryCustom.java create mode 100644 financer-server/src/main/java/de/financer/dba/impl/TransactionRepositoryCustomImpl.java create mode 100644 financer-server/src/main/java/de/financer/fql/FQLVisitorImpl.java create mode 100644 financer-server/src/main/java/de/financer/fql/FieldMapping.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/BetweenIntHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/BetweenStringHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/BooleanHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/DateHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/FieldHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/IntHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/NotNullSyntheticHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/PeriodConstHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/StringHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/join_handler/AccountGroupJoinHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/join_handler/FromAccountJoinHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/join_handler/JoinHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/join_handler/JoinKey.java create mode 100644 financer-server/src/main/java/de/financer/fql/join_handler/NoopJoinHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/join_handler/PeriodJoinHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/join_handler/ToAccountJoinHandler.java create mode 100644 financer-server/src/main/java/de/financer/service/exception/FinancerServiceException.java create mode 100644 financer-server/src/main/java/de/financer/service/exception/RestControllerExceptionAdvisor.java create mode 100644 financer-server/src/main/java/de/financer/service/parameter/SearchTransactionsParameter.java delete mode 100644 financer-web-client/src/main/java/de/financer/controller/handler/NoExceptionResponseErrorHandler.java create mode 100644 financer-web-client/src/main/java/de/financer/form/SearchTransactionsForm.java delete mode 100644 financer-web-client/src/main/java/de/financer/template/GetAllTransactionsForAccountTemplate.java create mode 100644 financer-web-client/src/main/java/de/financer/template/SearchTransactionsTemplate.java create mode 100644 financer-web-client/src/main/java/de/financer/template/exception/FinancerRestException.java create mode 100644 financer-web-client/src/main/resources/templates/transaction/searchTransactions.html create mode 100644 financer-web-client/src/main/resources/templates/transaction/transactionList.html 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 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 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 fieldHandlerClass; + + FieldMapping(String fieldName, String attributeName, Class entityClass, + Class joinHandlerClass, JoinKey joinKey, + FieldMapping dependingJoin, Class 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>, FieldMapping, Void> { +} diff --git a/financer-server/src/main/java/de/financer/fql/join_handler/JoinKey.java b/financer-server/src/main/java/de/financer/fql/join_handler/JoinKey.java new file mode 100644 index 0000000..b2c4fd0 --- /dev/null +++ b/financer-server/src/main/java/de/financer/fql/join_handler/JoinKey.java @@ -0,0 +1,36 @@ +package de.financer.fql.join_handler; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class JoinKey { + private Class clazz; + private String key; + + public static JoinKey of(Class clazz, String key) { + JoinKey jk = new JoinKey(); + + jk.clazz = clazz; + jk.key = key; + + return jk; + } + + public static JoinKey of(Class clazz) { + JoinKey jk = new JoinKey(); + + jk.clazz = clazz; + + return jk; + } + + @Override + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(this, obj, false); + } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this, false); + } +} diff --git a/financer-server/src/main/java/de/financer/fql/join_handler/NoopJoinHandler.java b/financer-server/src/main/java/de/financer/fql/join_handler/NoopJoinHandler.java new file mode 100644 index 0000000..dac0d22 --- /dev/null +++ b/financer-server/src/main/java/de/financer/fql/join_handler/NoopJoinHandler.java @@ -0,0 +1,13 @@ +package de.financer.fql.join_handler; + +import de.financer.fql.FieldMapping; + +import javax.persistence.criteria.From; +import java.util.Map; + +public class NoopJoinHandler implements JoinHandler { + @Override + public Void apply(Map> joinKeyFromMap, FieldMapping fieldMapping) { + return null; + } +} diff --git a/financer-server/src/main/java/de/financer/fql/join_handler/PeriodJoinHandler.java b/financer-server/src/main/java/de/financer/fql/join_handler/PeriodJoinHandler.java new file mode 100644 index 0000000..46e5f26 --- /dev/null +++ b/financer-server/src/main/java/de/financer/fql/join_handler/PeriodJoinHandler.java @@ -0,0 +1,28 @@ +package de.financer.fql.join_handler; + + +import de.financer.fql.FieldMapping; +import de.financer.model.Period; +import de.financer.model.Transaction; +import de.financer.model.Transaction_; + +import javax.persistence.criteria.From; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.SetJoin; +import java.util.Map; + +public class PeriodJoinHandler implements JoinHandler { + + @Override + public Void apply(Map> joinKeyFromMap, FieldMapping fieldMapping) { + if (joinKeyFromMap.get(fieldMapping.getJoinKey()) == null) { + final SetJoin periodJoin = ((Root) joinKeyFromMap + .get(JoinKey.of(Transaction.class))) + .join(Transaction_.periods); + + joinKeyFromMap.put(fieldMapping.getJoinKey(), periodJoin); + } + + return null; + } +} diff --git a/financer-server/src/main/java/de/financer/fql/join_handler/ToAccountJoinHandler.java b/financer-server/src/main/java/de/financer/fql/join_handler/ToAccountJoinHandler.java new file mode 100644 index 0000000..9ff708e --- /dev/null +++ b/financer-server/src/main/java/de/financer/fql/join_handler/ToAccountJoinHandler.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 ToAccountJoinHandler 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_.toAccount); + + joinKeyFromMap.put(fieldMapping.getJoinKey(), accountJoin); + } + + return null; + } +} diff --git a/financer-server/src/main/java/de/financer/service/AccountService.java b/financer-server/src/main/java/de/financer/service/AccountService.java index 1b6422a..27122b2 100644 --- a/financer-server/src/main/java/de/financer/service/AccountService.java +++ b/financer-server/src/main/java/de/financer/service/AccountService.java @@ -43,6 +43,7 @@ public class AccountService { * @return the account or null if no account with the given key can be found */ public Account getAccountByKey(String key) { + // TODO change return type of repository to Optional return this.accountRepository.findByKey(key); } @@ -102,7 +103,7 @@ public class AccountService { // If we create an account it's implicitly open account.setStatus(AccountStatus.OPEN); // and has a current balance of 0 - account.setCurrentBalance(Long.valueOf(0L)); + account.setCurrentBalance(0L); try { this.accountRepository.save(account); diff --git a/financer-server/src/main/java/de/financer/service/PeriodService.java b/financer-server/src/main/java/de/financer/service/PeriodService.java index e94818f..a0a9619 100644 --- a/financer-server/src/main/java/de/financer/service/PeriodService.java +++ b/financer-server/src/main/java/de/financer/service/PeriodService.java @@ -12,10 +12,9 @@ import org.slf4j.LoggerFactory; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.Month; +import java.util.Optional; @Service public class PeriodService { @@ -132,4 +131,8 @@ public class PeriodService { return period; } + + public Optional getPeriodById(Long id) { + return this.periodRepository.findById(id); + } } diff --git a/financer-server/src/main/java/de/financer/service/TransactionService.java b/financer-server/src/main/java/de/financer/service/TransactionService.java index 6e23563..c49e976 100644 --- a/financer-server/src/main/java/de/financer/service/TransactionService.java +++ b/financer-server/src/main/java/de/financer/service/TransactionService.java @@ -4,7 +4,12 @@ import de.financer.ResponseReason; import de.financer.config.FinancerConfig; import de.financer.dba.TransactionRepository; import de.financer.dto.ExpensePeriodTotal; +import de.financer.dto.Order; +import de.financer.dto.SearchTransactionsResponseDto; import de.financer.model.*; +import de.financer.service.exception.FinancerServiceException; +import de.financer.service.parameter.SearchTransactionsParameter; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; @@ -48,25 +53,6 @@ public class TransactionService { return this.transactionRepository.findAll(); } - /** - * @param accountKey the key of the account to get the transactions for - * - * @return all transactions for the given account, for all time. Returns an empty list if the given key does not - * match any account. - */ - public Iterable getAllForAccount(String accountKey) { - final Account account = this.accountService.getAccountByKey(accountKey); - - if (account == null) { - LOGGER.warn(String.format("Account with key %s not found!", accountKey)); - - return Collections.emptyList(); - } - - // As we want all transactions of the given account use it as from and to account - return this.transactionRepository.findTransactionsByFromAccountOrToAccount(account, account); - } - @Transactional(propagation = Propagation.REQUIRED) public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, String description, Boolean taxRelevant @@ -114,7 +100,7 @@ public class TransactionService { this.accountService.saveAccount(fromAccount); this.accountService.saveAccount(toAccount); - response = ResponseReason.OK; + response = ResponseReason.CREATED; } catch (Exception e) { LOGGER.error("Could not create transaction!", e); @@ -261,7 +247,7 @@ public class TransactionService { final Long expensesCurrentPeriod = this.transactionRepository .getExpensesForPeriod(expensePeriod, AccountType.EXPENSE, AccountType.LIABILITY); - return Optional.ofNullable(expensesCurrentPeriod).orElse(Long.valueOf(0l)); + return Optional.ofNullable(expensesCurrentPeriod).orElse(0L); } /** @@ -290,4 +276,93 @@ public class TransactionService { return this.transactionRepository .getAccountExpenseTotals(periods, AccountType.INCOME, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY); } + + /** + * This method searches for transactions as specified with the given search parameters. If no parameters at all are + * given the search returns the top 500 transactions for all accounts ordered by the transaction date descending. + * + * @param parameter the encapsulated search parameters + * @return the list of transactions matching the given parameters or the top 500 transactions. Never null + */ + public Iterable searchTransactions(SearchTransactionsParameter parameter) { + // Check that at least one bounding parameter is set so the resulting list won't be too large + if (StringUtils.isEmpty(parameter.getPeriodId()) && StringUtils.isEmpty(parameter.getLimit())) { + // 500 is pretty conservative, could probably be increased if required to at least 1000 + parameter.setLimit("500"); + } + + final Order order; + Period period = null; + Integer limit = null; + Account fromAccount = null; + Account toAccount = null; + Boolean taxRelevant = null; + Boolean accountsAnd = Boolean.FALSE; // account values are OR conjunct by default + + if (StringUtils.isNotEmpty(parameter.getPeriodId())) { + if (!NumberUtils.isCreatable(parameter.getPeriodId())) { + throw new FinancerServiceException(ResponseReason.PERIOD_ID_NOT_NUMERIC); + } + + final Optional optionalPeriod = this.periodService.getPeriodById(Long.valueOf(parameter.getPeriodId())); + + period = optionalPeriod.orElseThrow(() -> new FinancerServiceException(ResponseReason.PERIOD_NOT_FOUND)); + } + + if (StringUtils.isNotEmpty(parameter.getLimit())) { + if (!NumberUtils.isCreatable(parameter.getLimit())) { + throw new FinancerServiceException(ResponseReason.LIMIT_NOT_NUMERIC); + } + + limit = Integer.valueOf(parameter.getLimit()); + } + + order = Order.getByName(parameter.getOrder()).orElse(Order.TRANSACTIONS_BY_DATE_DESC); + + if (StringUtils.isNotEmpty(parameter.getFromAccountKey())) { + fromAccount = this.accountService.getAccountByKey(parameter.getFromAccountKey()); + + if (fromAccount == null) { + throw new FinancerServiceException(ResponseReason.FROM_ACCOUNT_NOT_FOUND); + } + } + + if (StringUtils.isNotEmpty(parameter.getToAccountKey())) { + toAccount = this.accountService.getAccountByKey(parameter.getToAccountKey()); + + if (toAccount == null) { + throw new FinancerServiceException(ResponseReason.TO_ACCOUNT_NOT_FOUND); + } + } + + if (StringUtils.isNotEmpty(parameter.getTaxRelevant())) { + taxRelevant = BooleanUtils.toBooleanObject(parameter.getTaxRelevant()); + + if (taxRelevant == null) { + throw new FinancerServiceException(ResponseReason.INVALID_TAX_RELEVANT_VALUE); + } + } + + if (StringUtils.isNotEmpty(parameter.getAccountsAnd())) { + accountsAnd = BooleanUtils.toBooleanObject(parameter.getAccountsAnd()); + + if (accountsAnd == null) { + throw new FinancerServiceException(ResponseReason.INVALID_ACCOUNTS_AND_VALUE); + } + } + + return this.transactionRepository.searchTransactions(period, fromAccount, toAccount, limit, order, taxRelevant, + accountsAnd); + } + + public Iterable searchTransactions(String fql) { + try { + return this.transactionRepository.searchTransactions(fql); + } + catch(IllegalArgumentException iae) { + LOGGER.error("Error while parsing FQL", iae); + + throw new FinancerServiceException(ResponseReason.FQL_MALFORMED); + } + } } diff --git a/financer-server/src/main/java/de/financer/service/exception/FinancerServiceException.java b/financer-server/src/main/java/de/financer/service/exception/FinancerServiceException.java new file mode 100644 index 0000000..bc92cf1 --- /dev/null +++ b/financer-server/src/main/java/de/financer/service/exception/FinancerServiceException.java @@ -0,0 +1,15 @@ +package de.financer.service.exception; + +import de.financer.ResponseReason; + +public class FinancerServiceException extends RuntimeException { + private final ResponseReason responseReason; + + public FinancerServiceException(ResponseReason responseReason) { + this.responseReason = responseReason; + } + + public ResponseReason getResponseReason() { + return this.responseReason; + } +} diff --git a/financer-server/src/main/java/de/financer/service/exception/RestControllerExceptionAdvisor.java b/financer-server/src/main/java/de/financer/service/exception/RestControllerExceptionAdvisor.java new file mode 100644 index 0000000..7be8356 --- /dev/null +++ b/financer-server/src/main/java/de/financer/service/exception/RestControllerExceptionAdvisor.java @@ -0,0 +1,17 @@ +package de.financer.service.exception; + + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class RestControllerExceptionAdvisor extends ResponseEntityExceptionHandler { + + @ExceptionHandler(value = {FinancerServiceException.class}) + public ResponseEntity handleFinancerServiceException(FinancerServiceException ex, WebRequest request) { + return ex.getResponseReason().toResponseEntity(); + } +} diff --git a/financer-server/src/main/java/de/financer/service/parameter/SearchTransactionsParameter.java b/financer-server/src/main/java/de/financer/service/parameter/SearchTransactionsParameter.java new file mode 100644 index 0000000..4a79598 --- /dev/null +++ b/financer-server/src/main/java/de/financer/service/parameter/SearchTransactionsParameter.java @@ -0,0 +1,67 @@ +package de.financer.service.parameter; + +public class SearchTransactionsParameter { + private String fromAccountKey; + private String toAccountKey; + private String periodId; + private String limit; + private String order; + private String taxRelevant; + private String accountsAnd; + + 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; + } + + public String getAccountsAnd() { + return accountsAnd; + } + + public void setAccountsAnd(String accountsAnd) { + this.accountsAnd = accountsAnd; + } +} diff --git a/financer-server/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java b/financer-server/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java index d3e5c0e..b4ff26e 100644 --- a/financer-server/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java +++ b/financer-server/src/main/java/de/financer/task/SendRecurringTransactionReminderTask.java @@ -14,7 +14,6 @@ import org.springframework.mail.javamail.JavaMailSender; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import java.util.Locale; import java.util.stream.Collectors; @Component @@ -52,7 +51,7 @@ public class SendRecurringTransactionReminderTask { // TODO Filtering currently happens in memory but should be done via SQL recurringTransactions = IterableUtils.toList(recurringTransactions) .stream() - .filter((rt) -> rt.isRemind()) + .filter(RecurringTransaction::isRemind) .collect(Collectors.toList()); LOGGER.info(String @@ -65,20 +64,18 @@ public class SendRecurringTransactionReminderTask { .append(System.lineSeparator()) .append(System.lineSeparator()); - IterableUtils.toList(recurringTransactions).forEach((rt) -> { - reminderBuilder.append(rt.getId()) - .append("|") - .append(rt.getDescription()) - .append(System.lineSeparator()) - .append(this.getMessage("financer.recurring-transaction.email.reminder.from")) - .append(rt.getFromAccount().getKey()) - .append(this.getMessage("financer.recurring-transaction.email.reminder.to")) - .append(rt.getToAccount().getKey()) - .append(": ") - .append(rt.getAmount().toString()) - .append(System.lineSeparator()) - .append(System.lineSeparator()); - }); + IterableUtils.toList(recurringTransactions).forEach((rt) -> reminderBuilder.append(rt.getId()) + .append("|") + .append(rt.getDescription()) + .append(System.lineSeparator()) + .append(this.getMessage("financer.recurring-transaction.email.reminder.from")) + .append(rt.getFromAccount().getKey()) + .append(this.getMessage("financer.recurring-transaction.email.reminder.to")) + .append(rt.getToAccount().getKey()) + .append(": ") + .append(rt.getAmount().toString()) + .append(System.lineSeparator()) + .append(System.lineSeparator())); final SimpleMailMessage msg = new SimpleMailMessage(); diff --git a/financer-server/src/test/java/de/financer/service/AccountService_createAccountTest.java b/financer-server/src/test/java/de/financer/service/AccountService_createAccountTest.java index 400b77a..5d5d965 100644 --- a/financer-server/src/test/java/de/financer/service/AccountService_createAccountTest.java +++ b/financer-server/src/test/java/de/financer/service/AccountService_createAccountTest.java @@ -3,7 +3,6 @@ package de.financer.service; import de.financer.ResponseReason; import de.financer.dba.AccountRepository; import de.financer.model.Account; -import de.financer.model.AccountGroup; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/financer-server/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java index 8848616..b2617aa 100644 --- a/financer-server/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java +++ b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_createRecurringTransactionTest.java @@ -46,7 +46,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.invalid", "account.invalid", - Long.valueOf(150l), + 150L, "DESCRIPTION", "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", @@ -67,7 +67,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.invalid", - Long.valueOf(150l), + 150L, "DESCRIPTION", "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", @@ -88,7 +88,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.invalid", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", @@ -110,7 +110,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", @@ -154,7 +154,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(0l), + 0L, "DESCRIPTION", "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", @@ -176,7 +176,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", null, "INTERVAL_TYPE", @@ -198,7 +198,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", "HOLIDAY_WEEKEND_TYPE", "INTERVAL_TYPE", @@ -220,7 +220,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", HolidayWeekendType.SAME_DAY.name(), null, @@ -242,7 +242,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", HolidayWeekendType.SAME_DAY.name(), "INTERVAL_TYPE", @@ -264,7 +264,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", HolidayWeekendType.SAME_DAY.name(), IntervalType.DAILY.name(), @@ -286,7 +286,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", HolidayWeekendType.SAME_DAY.name(), IntervalType.DAILY.name(), @@ -308,7 +308,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", HolidayWeekendType.SAME_DAY.name(), IntervalType.DAILY.name(), @@ -331,7 +331,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", HolidayWeekendType.SAME_DAY.name(), IntervalType.DAILY.name(), @@ -353,7 +353,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { // Act final ResponseReason response = this.classUnderTest.createRecurringTransaction("account.from", "account.to", - Long.valueOf(150l), + 150L, "DESCRIPTION", HolidayWeekendType.SAME_DAY.name(), IntervalType.DAILY.name(), @@ -369,7 +369,7 @@ public class RecurringTransactionService_createRecurringTransactionTest { private Account createAccount() { final Account account = new Account(); - account.setCurrentBalance(Long.valueOf(0l)); + account.setCurrentBalance(0L); return account; } diff --git a/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java b/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java index cee6568..a9eae84 100644 --- a/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java +++ b/financer-server/src/test/java/de/financer/service/TransactionService_createTransactionTest.java @@ -48,7 +48,7 @@ public class TransactionService_createTransactionTest { // will not be found. // Act - final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX", false); + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", 150L, "24.02.2019", "XXX", false); // Assert Assert.assertEquals(ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND, response); @@ -60,7 +60,7 @@ public class TransactionService_createTransactionTest { Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), null); // Act - final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.invalid", Long.valueOf(150l), "24.02.2019", "XXX", false); + final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.invalid", 150L, "24.02.2019", "XXX", false); // Assert Assert.assertEquals(ResponseReason.TO_ACCOUNT_NOT_FOUND, response); @@ -72,7 +72,7 @@ public class TransactionService_createTransactionTest { Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(null, createAccount()); // Act - final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.to", Long.valueOf(150l), "24.02.2019", "XXX", false); + final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.to", 150L, "24.02.2019", "XXX", false); // Assert Assert.assertEquals(ResponseReason.FROM_ACCOUNT_NOT_FOUND, response); @@ -85,7 +85,7 @@ public class TransactionService_createTransactionTest { Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.FALSE); // Act - final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(150l), "24.02.2019", "XXX", false); + final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 150L, "24.02.2019", "XXX", false); // Assert Assert.assertEquals(ResponseReason.INVALID_BOOKING_ACCOUNTS, response); @@ -111,7 +111,7 @@ public class TransactionService_createTransactionTest { Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); // Act - final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(0l), "24.02.2019", "XXX", false); + final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 0L, "24.02.2019", "XXX", false); // Assert Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response); @@ -124,7 +124,7 @@ public class TransactionService_createTransactionTest { Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); // Act - final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(125l), null, "XXX", false); + final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, null, "XXX", false); // Assert Assert.assertEquals(ResponseReason.MISSING_DATE, response); @@ -137,7 +137,7 @@ public class TransactionService_createTransactionTest { Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); // Act - final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(125l), "2019-01-01", "XXX", false); + final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, "2019-01-01", "XXX", false); // Assert Assert.assertEquals(ResponseReason.INVALID_DATE_FORMAT, response); @@ -150,24 +150,24 @@ public class TransactionService_createTransactionTest { final Account toAccount = Mockito.mock(Account.class); Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(fromAccount, toAccount); Mockito.when(this.ruleService.isValidBooking(Mockito.any(), Mockito.any())).thenReturn(Boolean.TRUE); - Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenReturn(Long.valueOf(-1l)); - Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenReturn(Long.valueOf(1l)); - Mockito.when(fromAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l)); - Mockito.when(toAccount.getCurrentBalance()).thenReturn(Long.valueOf(0l)); + Mockito.when(this.ruleService.getMultiplierFromAccount(Mockito.any())).thenReturn(-1L); + Mockito.when(this.ruleService.getMultiplierToAccount(Mockito.any())).thenReturn(1L); + Mockito.when(fromAccount.getCurrentBalance()).thenReturn(0L); + Mockito.when(toAccount.getCurrentBalance()).thenReturn(0L); // Act - final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", Long.valueOf(125l), "24.02.2019", "XXX", false); + final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, "24.02.2019", "XXX", false); // Assert - Assert.assertEquals(ResponseReason.OK, response); - Mockito.verify(fromAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(-125)); - Mockito.verify(toAccount, Mockito.times(1)).setCurrentBalance(Long.valueOf(125)); + Assert.assertEquals(ResponseReason.CREATED, response); + Mockito.verify(fromAccount, Mockito.times(1)).setCurrentBalance((long) -125); + Mockito.verify(toAccount, Mockito.times(1)).setCurrentBalance(125L); } private Account createAccount() { final Account account = new Account(); - account.setCurrentBalance(Long.valueOf(0l)); + account.setCurrentBalance(0L); return account; } diff --git a/financer-server/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java b/financer-server/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java index a45c96f..379db61 100644 --- a/financer-server/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java +++ b/financer-server/src/test/java/de/financer/service/TransactionService_deleteTransactionTest.java @@ -13,7 +13,6 @@ import org.junit.runner.RunWith; import org.mockito.*; import org.mockito.junit.MockitoJUnitRunner; -import java.util.List; import java.util.Optional; @RunWith(MockitoJUnitRunner.class) @@ -122,10 +121,10 @@ public class TransactionService_deleteTransactionTest { transaction.setFromAccount(fromAccount); transaction.setToAccount(toAccount); - transaction.setAmount(Long.valueOf(10000L)); + transaction.setAmount(10000L); - fromAccount.setCurrentBalance(Long.valueOf(40000L)); - toAccount.setCurrentBalance(Long.valueOf(15000L)); + fromAccount.setCurrentBalance(40000L); + toAccount.setCurrentBalance(15000L); fromAccount.setType(fromType); toAccount.setType(toType); diff --git a/financer-server/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java b/financer-server/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java index 4db96de..ddf8d3b 100644 --- a/financer-server/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java +++ b/financer-server/src/test/java/de/financer/task/SendRecurringTransactionReminderTaskTest.java @@ -4,7 +4,6 @@ import de.financer.config.FinancerConfig; import de.financer.model.Account; import de.financer.model.RecurringTransaction; import de.financer.service.RecurringTransactionService; -import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -40,10 +39,10 @@ public class SendRecurringTransactionReminderTaskTest { public void test_sendReminder() { // Arrange final Collection recurringTransactions = Arrays.asList( - createRecurringTransaction("Test booking 1", "Income", "accounts.bank", Long.valueOf(250000), true), - createRecurringTransaction("Test booking 2", "Bank", "accounts.rent", Long.valueOf(41500), true), - createRecurringTransaction("Test booking 3", "Bank", "accounts.cash", Long.valueOf(5000), true), - createRecurringTransaction("Test booking 4", "Car", "accounts.car", Long.valueOf(1234), false) + createRecurringTransaction("Test booking 1", "Income", "accounts.bank", 250000L, true), + createRecurringTransaction("Test booking 2", "Bank", "accounts.rent", 41500L, true), + createRecurringTransaction("Test booking 3", "Bank", "accounts.cash", 5000L, true), + createRecurringTransaction("Test booking 4", "Car", "accounts.car", 1234L, false) ); Mockito.when(this.recurringTransactionService.getAllDueToday()).thenReturn(recurringTransactions); diff --git a/financer-web-client/src/main/java/de/financer/FinancerApplication.java b/financer-web-client/src/main/java/de/financer/FinancerApplication.java index 8737a0b..14eda14 100644 --- a/financer-web-client/src/main/java/de/financer/FinancerApplication.java +++ b/financer-web-client/src/main/java/de/financer/FinancerApplication.java @@ -4,8 +4,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; @SpringBootApplication public class FinancerApplication extends SpringBootServletInitializer { diff --git a/financer-web-client/src/main/java/de/financer/chart/ChartType.java b/financer-web-client/src/main/java/de/financer/chart/ChartType.java index 75bd4fc..fa6d580 100644 --- a/financer-web-client/src/main/java/de/financer/chart/ChartType.java +++ b/financer-web-client/src/main/java/de/financer/chart/ChartType.java @@ -12,18 +12,18 @@ public enum ChartType { EXPENSE_PERIOD_TOTALS_CURRENT_YEAR(false), EXPENSES_ALL_PERIODS_INLINE(true); - private boolean inline; + private final boolean inline; ChartType(boolean inline) { this.inline = inline; } public static List valueList(boolean filterInline) { - return Arrays.stream(ChartType.values()).filter((ct) -> filterInline ? !ct.inline : true).map(ChartType::name) + return Arrays.stream(ChartType.values()).filter((ct) -> !filterInline || !ct.inline).map(ChartType::name) .collect(Collectors.toList()); } public static ChartType getByValue(String value) { - return Arrays.asList(values()).stream().filter((ct) -> ct.name().equals(value)).findFirst().get(); + return Arrays.stream(values()).filter((ct) -> ct.name().equals(value)).findFirst().get(); } } diff --git a/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountExpensesGenerator.java b/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountExpensesGenerator.java index f1f581f..7dddcc5 100644 --- a/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountExpensesGenerator.java +++ b/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountExpensesGenerator.java @@ -2,10 +2,8 @@ package de.financer.chart.impl.expense; import de.financer.config.FinancerConfig; import de.financer.dto.AccountExpense; -import de.financer.dto.AccountGroupExpense; import de.financer.template.GetAccountExpensesCurrentExpensePeriodTemplate; import de.financer.template.GetAccountExpensesTemplate; -import de.financer.template.GetAccountGroupExpensesTemplate; import org.apache.commons.collections4.IterableUtils; import org.jfree.data.general.DefaultPieDataset; import org.jfree.data.general.PieDataset; @@ -24,7 +22,7 @@ public class AccountExpensesGenerator extends AbstractExpensesGenerator { final DefaultPieDataset dataSet = new DefaultPieDataset(); - IterableUtils.toList(expenses).stream() + IterableUtils.toList(expenses) .forEach((ex) -> dataSet.setValue(ex.getAccount().getKey(), (ex.getExpense() / 100D))); return dataSet; diff --git a/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountGroupExpensesGenerator.java b/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountGroupExpensesGenerator.java index 03101a1..0992a12 100644 --- a/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountGroupExpensesGenerator.java +++ b/financer-web-client/src/main/java/de/financer/chart/impl/expense/AccountGroupExpensesGenerator.java @@ -22,7 +22,7 @@ public class AccountGroupExpensesGenerator extends AbstractExpensesGenerator { final DefaultPieDataset dataSet = new DefaultPieDataset(); - IterableUtils.toList(expenses).stream() + IterableUtils.toList(expenses) .forEach((ex) -> dataSet.setValue(ex.getAccountGroup().getName(), (ex.getExpense() / 100D))); return dataSet; diff --git a/financer-web-client/src/main/java/de/financer/chart/impl/inline/ExpensesAllPeriodsGenerator.java b/financer-web-client/src/main/java/de/financer/chart/impl/inline/ExpensesAllPeriodsGenerator.java index b60c423..9708c35 100644 --- a/financer-web-client/src/main/java/de/financer/chart/impl/inline/ExpensesAllPeriodsGenerator.java +++ b/financer-web-client/src/main/java/de/financer/chart/impl/inline/ExpensesAllPeriodsGenerator.java @@ -39,7 +39,7 @@ public class ExpensesAllPeriodsGenerator extends AbstractChartGenerator totalData = new GetExpensesAllPeriodsTemplate().exchange(this.getFinancerConfig()).getBody(); final AtomicInteger counter = new AtomicInteger(); - totalData.stream().forEach((l) -> result.addValue(l, "c", "r" + counter.incrementAndGet())); + totalData.forEach((l) -> result.addValue(l, "c", "r" + counter.incrementAndGet())); return result; } diff --git a/financer-web-client/src/main/java/de/financer/chart/impl/total/PeriodTotalGenerator.java b/financer-web-client/src/main/java/de/financer/chart/impl/total/PeriodTotalGenerator.java index 7164ce8..7eb541d 100644 --- a/financer-web-client/src/main/java/de/financer/chart/impl/total/PeriodTotalGenerator.java +++ b/financer-web-client/src/main/java/de/financer/chart/impl/total/PeriodTotalGenerator.java @@ -56,7 +56,7 @@ public class PeriodTotalGenerator extends AbstractChartGenerator ept.getPeriod().getStart()) - .thenComparing((ExpensePeriodTotal ept) -> ept.getType())) + .thenComparing(ExpensePeriodTotal::getType)) .forEach((ept) -> result.addValue((ept.getTotal() / 100D), this.getMessage("financer.account-type." + ept.getType()), formatDateY(ept.getPeriod()))); diff --git a/financer-web-client/src/main/java/de/financer/controller/AccountController.java b/financer-web-client/src/main/java/de/financer/controller/AccountController.java index ab3cf89..4341eff 100644 --- a/financer-web-client/src/main/java/de/financer/controller/AccountController.java +++ b/financer-web-client/src/main/java/de/financer/controller/AccountController.java @@ -3,12 +3,14 @@ package de.financer.controller; import de.financer.ResponseReason; import de.financer.config.FinancerConfig; import de.financer.decorator.AccountDecorator; -import de.financer.template.*; +import de.financer.dto.Order; +import de.financer.dto.SearchTransactionsResponseDto; import de.financer.form.NewAccountForm; import de.financer.model.*; +import de.financer.template.*; +import de.financer.template.exception.FinancerRestException; import de.financer.util.ControllerUtils; import de.financer.util.TransactionUtils; -import de.financer.util.comparator.TransactionByDateByIdDescComparator; import org.apache.commons.collections4.IterableUtils; import org.apache.commons.lang3.BooleanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -57,7 +59,7 @@ public class AccountController { } private Iterable decorateAccounts(List accounts) { - return accounts.stream().map((a) -> new AccountDecorator(a)).collect(Collectors.toList()); + return accounts.stream().map(AccountDecorator::new).collect(Collectors.toList()); } @GetMapping("/newAccount") @@ -102,21 +104,40 @@ public class AccountController { return "redirect:/accountOverview"; } + public static void _accountDetails(String key, Model model, FinancerConfig financerConfig) { + try { + final Iterable response = + new SearchTransactionsTemplate() + .exchangeGet(financerConfig, key, key, null, null, + Order.TRANSACTIONS_BY_DATE_DESC, null, false); + + final List transactions = IterableUtils.toList(response); + final Account account = IterableUtils.toList(response).stream() + .filter(t -> t.getFromAccount().getKey().equals(key)) + .findFirst() + .map(SearchTransactionsResponseDto::getFromAccount) + .orElseGet(() -> transactions + .stream() + .filter(t -> t.getToAccount().getKey().equals(key)) + .findFirst() + .map(SearchTransactionsResponseDto::getToAccount).get()); + + transactions.forEach((t) -> TransactionUtils.adjustAmount(t, account)); + + model.addAttribute("account", account); + model.addAttribute("transactions", transactions); + model.addAttribute("showActions", true); + model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus())); + } + catch(FinancerRestException e) { + // TODO probably leads to follow-up exceptions during thymeleaf parsing as relevant information are missing + } + } + @GetMapping("/accountDetails") public String accountDetails(String key, Model model) { - final ResponseEntity response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key); - final Account account = response.getBody(); - final ResponseEntity> transactionResponse = new GetAllTransactionsForAccountTemplate() - .exchange(this.financerConfig, account.getKey()); + _accountDetails(key, model, this.financerConfig); - List transactions = IterableUtils.toList(transactionResponse.getBody()); - - transactions.sort(new TransactionByDateByIdDescComparator()); - transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account)); - - model.addAttribute("account", account); - model.addAttribute("transactions", transactions); - model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus())); ControllerUtils.addVersionAttribute(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig); @@ -134,19 +155,8 @@ public class AccountController { final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse); if (!ResponseReason.OK.equals(responseReason)) { - final ResponseEntity response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key); - final Account account = response.getBody(); - final ResponseEntity> transactionResponse = new GetAllTransactionsForAccountTemplate() - .exchange(this.financerConfig, account.getKey()); + _accountDetails(key, model, this.financerConfig); - List transactions = IterableUtils.toList(transactionResponse.getBody()); - - transactions.sort(new TransactionByDateByIdDescComparator()); - transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account)); - - model.addAttribute("account", account); - model.addAttribute("transactions", transactions); - model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus())); model.addAttribute("errorMessage", responseReason.name()); ControllerUtils.addVersionAttribute(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig); @@ -168,19 +178,8 @@ public class AccountController { final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse); if (!ResponseReason.OK.equals(responseReason)) { - final ResponseEntity response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key); - final Account account = response.getBody(); - final ResponseEntity> transactionResponse = new GetAllTransactionsForAccountTemplate() - .exchange(this.financerConfig, account.getKey()); + _accountDetails(key, model, this.financerConfig); - List transactions = IterableUtils.toList(transactionResponse.getBody()); - - transactions.sort(new TransactionByDateByIdDescComparator()); - transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account)); - - model.addAttribute("account", account); - model.addAttribute("transactions", transactions); - model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus())); model.addAttribute("errorMessage", responseReason.name()); ControllerUtils.addVersionAttribute(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig); diff --git a/financer-web-client/src/main/java/de/financer/controller/Function.java b/financer-web-client/src/main/java/de/financer/controller/Function.java index 10fb1e5..2f4d7e9 100644 --- a/financer-web-client/src/main/java/de/financer/controller/Function.java +++ b/financer-web-client/src/main/java/de/financer/controller/Function.java @@ -15,10 +15,10 @@ public enum Function { ACC_GP_GET_ACC_GP_EXPENSES("accountGroups/getAccountGroupExpenses"), ACC_GP_GET_ACC_GP_EXPENSES_CURRENT_EXPENSE_PERIOD("accountGroups/getAccountGroupExpensesCurrentExpensePeriod"), - TR_GET_ALL("transactions/getAll"), - TR_GET_ALL_FOR_ACCOUNT("transactions/getAllForAccount"), - TR_CREATE_TRANSACTION("transactions/createTransaction"), - TR_DELETE_TRANSACTION("transactions/deleteTransaction"), + TR_SEARCH("transactions"), + TR_SEARCH_BY_FQL("transactionsByFql"), + TR_CREATE_TRANSACTION("transactions"), + TR_DELETE_TRANSACTION("transactions/"), TR_EXPENSES_CURRENT_PERIOD("transactions/getExpensesCurrentPeriod"), TR_EXPENSE_PERIOD_TOTALS("transactions/getExpensePeriodTotals"), TR_EXPENSES_ALL_PERIODS("transactions/getExpensesAllPeriods"), @@ -34,7 +34,7 @@ public enum Function { P_GET_CURRENT_EXPENSE_PERIOD("periods/getCurrentExpensePeriod"), P_CLOSE_CURRENT_EXPENSE_PERIOD("periods/closeCurrentExpensePeriod"); - private String path; + private final String path; Function(String path) { this.path = path; diff --git a/financer-web-client/src/main/java/de/financer/controller/TransactionController.java b/financer-web-client/src/main/java/de/financer/controller/TransactionController.java index 9ff43f9..1ca6444 100644 --- a/financer-web-client/src/main/java/de/financer/controller/TransactionController.java +++ b/financer-web-client/src/main/java/de/financer/controller/TransactionController.java @@ -2,21 +2,17 @@ package de.financer.controller; import de.financer.ResponseReason; import de.financer.config.FinancerConfig; +import de.financer.dto.SaveTransactionRequestDto; +import de.financer.dto.SearchTransactionsResponseDto; +import de.financer.form.SearchTransactionsForm; import de.financer.model.AccountType; -import de.financer.template.GetAccountByKeyTemplate; -import de.financer.template.GetAllAccountsTemplate; -import de.financer.template.GetAllTransactionsForAccountTemplate; -import de.financer.template.StringTemplate; +import de.financer.template.*; import de.financer.form.NewTransactionForm; import de.financer.model.Account; -import de.financer.model.AccountStatus; -import de.financer.model.Transaction; +import de.financer.template.exception.FinancerRestException; import de.financer.util.ControllerUtils; -import de.financer.util.TransactionUtils; -import de.financer.util.comparator.TransactionByDateByIdDescComparator; -import org.apache.commons.collections4.IterableUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -24,6 +20,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.util.UriComponentsBuilder; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Controller @@ -32,20 +29,77 @@ public class TransactionController { @Autowired private FinancerConfig financerConfig; + @GetMapping("/searchTransactions") + public String searchTransaction(Model model) { + model.addAttribute("form", new SearchTransactionsForm()); + model.addAttribute("showActions", false); + ControllerUtils.addVersionAttribute(model, this.financerConfig); + ControllerUtils.addCurrencySymbol(model, this.financerConfig); + + return "transaction/searchTransactions"; + } + + @PostMapping("/searchTransactions") + public String searchTransactions(SearchTransactionsForm form, Model model) { + try { + final UriComponentsBuilder transactionBuilder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_SEARCH_BY_FQL)); + + transactionBuilder.queryParam("fql", form.getFql()); + + final Iterable trxs = FinancerRestTemplate.exchangeGet(transactionBuilder, + new ParameterizedTypeReference>() { + }); + + + model.addAttribute("transactions", trxs); + } + catch(FinancerRestException e) { + // TODO + model.addAttribute("errorMessage", e.getResponseReason().name()); + } + + model.addAttribute("form", form); + model.addAttribute("showActions", false); + ControllerUtils.addVersionAttribute(model, this.financerConfig); + ControllerUtils.addCurrencySymbol(model, this.financerConfig); + + return "transaction/searchTransactions"; + } + @GetMapping("/newTransaction") public String newTransaction(Model model) { - final ResponseEntity> response = new GetAllAccountsTemplate().exchange(this.financerConfig); - final List fromAccounts = ControllerUtils.filterAndSortAccounts(response.getBody()).stream() - .filter((a) -> a.getType() != AccountType.EXPENSE) - .collect(Collectors.toList()); - final List toAccounts = ControllerUtils.filterAndSortAccounts(response.getBody()).stream() - .filter((a) -> a.getType() != AccountType.INCOME && a - .getType() != AccountType.START) - .collect(Collectors.toList()); + return _newTransaction(model, new NewTransactionForm(), Optional.empty()); + } + + private String _newTransaction(Model model, NewTransactionForm form, Optional responseReason) { + try { + final Iterable allAccounts = + FinancerRestTemplate.exchangeGet(this.financerConfig, + Function.ACC_GET_ALL, + new ParameterizedTypeReference>() {}); + + final List fromAccounts = ControllerUtils.filterAndSortAccounts(allAccounts).stream() + .filter((a) -> a.getType() != AccountType.EXPENSE) + .collect(Collectors.toList()); + final List toAccounts = ControllerUtils.filterAndSortAccounts(allAccounts).stream() + .filter((a) -> a.getType() != AccountType.INCOME && a + .getType() != AccountType.START) + .collect(Collectors.toList()); + + model.addAttribute("fromAccounts", fromAccounts); + model.addAttribute("toAccounts", toAccounts); + } + catch(FinancerRestException e) { + // Nothing to do + // This is very unlikely to happen and if it happens the account selection stays empty, so the user + // cannot create a transaction anyway and is forced to reload the page or navigate back + } + + model.addAttribute("form", form); + + responseReason.ifPresent(rr -> model.addAttribute("errorMessage", rr.name())); - model.addAttribute("fromAccounts", fromAccounts); - model.addAttribute("toAccounts", toAccounts); - model.addAttribute("form", new NewTransactionForm()); ControllerUtils.addVersionAttribute(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig); @@ -54,36 +108,20 @@ public class TransactionController { @PostMapping("/saveTransaction") public String saveTransaction(NewTransactionForm form, Model model) { - final UriComponentsBuilder builder = UriComponentsBuilder - .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_CREATE_TRANSACTION)) - .queryParam("fromAccountKey", form.getFromAccountKey()) - .queryParam("toAccountKey", form.getToAccountKey()) - .queryParam("amount", form.getAmount()) - .queryParam("date", ControllerUtils.formatDate(this.financerConfig, form.getDate())) - .queryParam("description", form.getDescription()) - .queryParam("taxRelevant", form.getTaxRelevant()); + final SaveTransactionRequestDto requestDto = new SaveTransactionRequestDto(); - final ResponseEntity response = new StringTemplate().exchange(builder); - final ResponseReason responseReason = ResponseReason.fromResponseEntity(response); + requestDto.setFromAccountKey(form.getFromAccountKey()); + requestDto.setToAccountKey(form.getToAccountKey()); + requestDto.setAmount(form.getAmount()); + requestDto.setDate(ControllerUtils.formatDate(this.financerConfig, form.getDate())); + requestDto.setDescription(form.getDescription()); + requestDto.setTaxRelevant(form.getTaxRelevant()); - if (!ResponseReason.OK.equals(responseReason)) { - final ResponseEntity> accRes = new GetAllAccountsTemplate().exchange(this.financerConfig); - final List fromAccounts = ControllerUtils.filterAndSortAccounts(accRes.getBody()).stream() - .filter((a) -> a.getType() != AccountType.EXPENSE) - .collect(Collectors.toList()); - final List toAccounts = ControllerUtils.filterAndSortAccounts(accRes.getBody()).stream() - .filter((a) -> a.getType() != AccountType.INCOME && a - .getType() != AccountType.START) - .collect(Collectors.toList()); + final ResponseReason responseReason = FinancerRestTemplate + .exchangePost(this.financerConfig, Function.TR_CREATE_TRANSACTION, requestDto); - model.addAttribute("fromAccounts", fromAccounts); - model.addAttribute("toAccounts", toAccounts); - model.addAttribute("form", form); - model.addAttribute("errorMessage", responseReason.name()); - ControllerUtils.addVersionAttribute(model, this.financerConfig); - ControllerUtils.addCurrencySymbol(model, this.financerConfig); - - return "transaction/newTransaction"; + if (!ResponseReason.CREATED.equals(responseReason)) { + return _newTransaction(model, form, Optional.of(responseReason)); } return "redirect:/accountOverview"; @@ -91,34 +129,16 @@ public class TransactionController { @GetMapping("/deleteTransaction") public String deleteTransaction(String transactionId, String accountKey, Model model) { - final UriComponentsBuilder builder = UriComponentsBuilder - .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_DELETE_TRANSACTION)) - .queryParam("transactionId", transactionId); + final ResponseReason responseReason = FinancerRestTemplate + .exchangeDelete(this.financerConfig, Function.TR_DELETE_TRANSACTION, transactionId); - final ResponseEntity response = new StringTemplate().exchange(builder); - final ResponseReason responseReason = ResponseReason.fromResponseEntity(response); + AccountController._accountDetails(accountKey, model, this.financerConfig); - final ResponseEntity accountResponse = new GetAccountByKeyTemplate() - .exchange(this.financerConfig, accountKey); - final Account account = accountResponse.getBody(); - final ResponseEntity> transactionResponse = new GetAllTransactionsForAccountTemplate() - .exchange(this.financerConfig, account.getKey()); - - List transactions = IterableUtils.toList(transactionResponse.getBody()); - - transactions.sort(new TransactionByDateByIdDescComparator()); - transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account)); - - model.addAttribute("account", account); - model.addAttribute("transactions", transactions); - model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus())); ControllerUtils.addVersionAttribute(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig); if (!ResponseReason.OK.equals(responseReason)) { model.addAttribute("errorMessage", responseReason.name()); - - return "account/accountDetails"; } return "account/accountDetails"; diff --git a/financer-web-client/src/main/java/de/financer/controller/handler/NoExceptionResponseErrorHandler.java b/financer-web-client/src/main/java/de/financer/controller/handler/NoExceptionResponseErrorHandler.java deleted file mode 100644 index 16c6eaf..0000000 --- a/financer-web-client/src/main/java/de/financer/controller/handler/NoExceptionResponseErrorHandler.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.financer.controller.handler; - -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.web.client.ResponseErrorHandler; - -import java.io.IOException; - -public class NoExceptionResponseErrorHandler implements ResponseErrorHandler { - @Override - public boolean hasError(ClientHttpResponse response) throws IOException { - return false; - } - - @Override - public void handleError(ClientHttpResponse response) throws IOException { - - } -} diff --git a/financer-web-client/src/main/java/de/financer/decorator/AccountDecorator.java b/financer-web-client/src/main/java/de/financer/decorator/AccountDecorator.java index 6563d53..c472399 100644 --- a/financer-web-client/src/main/java/de/financer/decorator/AccountDecorator.java +++ b/financer-web-client/src/main/java/de/financer/decorator/AccountDecorator.java @@ -41,7 +41,7 @@ public class AccountDecorator { .findFirst() .orElseGet(() -> { AccountStatistic as = new AccountStatistic(); - as.setTransactionCountTo(0l); + as.setTransactionCountTo(0L); return as; }) .getSpendingTotalTo(); @@ -55,9 +55,9 @@ public class AccountDecorator { return Math.round(this.account.getAccountStatistics().stream() .filter((as) -> as.getPeriod().getType().equals(PeriodType.EXPENSE) && as.getPeriod() .getEnd() != null) - .mapToLong((as) -> as.getSpendingTotalTo()) + .mapToLong(AccountStatistic::getSpendingTotalTo) .average() - .orElseGet(() -> Double.valueOf(0d))); + .orElseGet(() -> 0d)); } return null; diff --git a/financer-web-client/src/main/java/de/financer/form/SearchTransactionsForm.java b/financer-web-client/src/main/java/de/financer/form/SearchTransactionsForm.java new file mode 100644 index 0000000..775058b --- /dev/null +++ b/financer-web-client/src/main/java/de/financer/form/SearchTransactionsForm.java @@ -0,0 +1,13 @@ +package de.financer.form; + +public class SearchTransactionsForm { + private String fql; + + public String getFql() { + return fql; + } + + public void setFql(String fql) { + this.fql = fql; + } +} diff --git a/financer-web-client/src/main/java/de/financer/template/FinancerRestTemplate.java b/financer-web-client/src/main/java/de/financer/template/FinancerRestTemplate.java index 4009ce9..ba6deb8 100644 --- a/financer-web-client/src/main/java/de/financer/template/FinancerRestTemplate.java +++ b/financer-web-client/src/main/java/de/financer/template/FinancerRestTemplate.java @@ -1,19 +1,133 @@ package de.financer.template; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.financer.controller.handler.NoExceptionResponseErrorHandler; +import de.financer.ResponseReason; +import de.financer.config.FinancerConfig; +import de.financer.controller.Function; +import de.financer.template.exception.FinancerRestException; +import de.financer.util.ControllerUtils; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; public class FinancerRestTemplate { - public ResponseEntity exchange(String url, ParameterizedTypeReference type) { + // Requests that result in the return of a String allow simplified error handling + + private static ResponseEntity _exchangePost(FinancerConfig financerConfig, + Function endpoint, + B body, + Class type) throws FinancerRestException { + final UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint)); final RestTemplate restTemplate = new RestTemplate(); - restTemplate.setErrorHandler(new NoExceptionResponseErrorHandler()); + try { + return restTemplate.exchange(builder.toUriString(), HttpMethod.POST, new HttpEntity<>(body), type); + } catch (HttpStatusCodeException e) { + final ResponseEntity exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e + .getStatusCode()); + + throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse)); + } + } + + public static R exchangePost(FinancerConfig financerConfig, + Function endpoint, + B body, + Class type) throws FinancerRestException { + return FinancerRestTemplate._exchangePost(financerConfig, endpoint, body, type).getBody(); + } + + public static ResponseReason exchangePost(FinancerConfig financerConfig, + Function endpoint, + B body) { + ResponseReason response; + + try { + final ResponseEntity responseEntity = FinancerRestTemplate + ._exchangePost(financerConfig, endpoint, body, String.class); + + response = ResponseReason.fromResponseEntity(responseEntity); + } catch (FinancerRestException e) { + response = e.getResponseReason(); + } + + return response; + } + + public static ResponseReason exchangeDelete(FinancerConfig financerConfig, + Function endpoint, + String id + ) { + final UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint, id)); + final RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response; + + try { + response = restTemplate.exchange(builder.toUriString(), HttpMethod.DELETE, null, String.class); + } catch (HttpStatusCodeException e) { + response = new ResponseEntity<>(e.getResponseBodyAsString(), e.getStatusCode()); + } + + return ResponseReason.fromResponseEntity(response); + } + + public static R exchangeGet(FinancerConfig financerConfig, + Function endpoint, + Class type) throws FinancerRestException { + final UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint)); + final RestTemplate restTemplate = new RestTemplate(); + + try { + return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, null, type).getBody(); + } catch (HttpStatusCodeException e) { + final ResponseEntity exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e + .getStatusCode()); + + throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse)); + } + } + + public static R exchangeGet(FinancerConfig financerConfig, + Function endpoint, + ParameterizedTypeReference type) throws FinancerRestException { + final UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint)); + final RestTemplate restTemplate = new RestTemplate(); + + try { + return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, null, type).getBody(); + } catch (HttpStatusCodeException e) { + final ResponseEntity exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e + .getStatusCode()); + + throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse)); + } + } + + public static R exchangeGet(UriComponentsBuilder builder, + ParameterizedTypeReference type) throws FinancerRestException { + final RestTemplate restTemplate = new RestTemplate(); + + try { + return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, null, type).getBody(); + } catch (HttpStatusCodeException e) { + final ResponseEntity exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e + .getStatusCode()); + + throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse)); + } + } + + // ---------------------------------- LEGACY + + public ResponseEntity exchange(String url, ParameterizedTypeReference type) { + final RestTemplate restTemplate = new RestTemplate(); return restTemplate.exchange(url, HttpMethod.GET, null, type); } @@ -21,22 +135,6 @@ public class FinancerRestTemplate { public ResponseEntity exchange(String url, Class type) { final RestTemplate restTemplate = new RestTemplate(); - restTemplate.setErrorHandler(new NoExceptionResponseErrorHandler()); - return restTemplate.exchange(url, HttpMethod.GET, null, type); } - - // The spring.jackson. properties are not picked up by the RestTemplate and its converter, - // so we need to overwrite it here - private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { - MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter(); - ObjectMapper objectMapper = new ObjectMapper(); - - objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - jsonConverter.setObjectMapper(objectMapper); - - return jsonConverter; - } } diff --git a/financer-web-client/src/main/java/de/financer/template/GetAccountExpensesTemplate.java b/financer-web-client/src/main/java/de/financer/template/GetAccountExpensesTemplate.java index e74c4e0..6801daf 100644 --- a/financer-web-client/src/main/java/de/financer/template/GetAccountExpensesTemplate.java +++ b/financer-web-client/src/main/java/de/financer/template/GetAccountExpensesTemplate.java @@ -3,7 +3,6 @@ package de.financer.template; import de.financer.config.FinancerConfig; import de.financer.controller.Function; import de.financer.dto.AccountExpense; -import de.financer.dto.AccountGroupExpense; import de.financer.util.ControllerUtils; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.ResponseEntity; diff --git a/financer-web-client/src/main/java/de/financer/template/GetAccountGroupExpensesTemplate.java b/financer-web-client/src/main/java/de/financer/template/GetAccountGroupExpensesTemplate.java index 64e28cc..310c41b 100644 --- a/financer-web-client/src/main/java/de/financer/template/GetAccountGroupExpensesTemplate.java +++ b/financer-web-client/src/main/java/de/financer/template/GetAccountGroupExpensesTemplate.java @@ -3,8 +3,6 @@ package de.financer.template; import de.financer.config.FinancerConfig; import de.financer.controller.Function; import de.financer.dto.AccountGroupExpense; -import de.financer.model.Account; -import de.financer.model.Transaction; import de.financer.util.ControllerUtils; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.ResponseEntity; diff --git a/financer-web-client/src/main/java/de/financer/template/GetAllTransactionsForAccountTemplate.java b/financer-web-client/src/main/java/de/financer/template/GetAllTransactionsForAccountTemplate.java deleted file mode 100644 index d7811d0..0000000 --- a/financer-web-client/src/main/java/de/financer/template/GetAllTransactionsForAccountTemplate.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.financer.template; - -import de.financer.config.FinancerConfig; -import de.financer.controller.Function; -import de.financer.model.Transaction; -import de.financer.util.ControllerUtils; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.ResponseEntity; -import org.springframework.web.util.UriComponentsBuilder; - -public class GetAllTransactionsForAccountTemplate { - public ResponseEntity> exchange(FinancerConfig financerConfig, String accountKey) { - final UriComponentsBuilder transactionBuilder = UriComponentsBuilder - .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_GET_ALL_FOR_ACCOUNT)) - .queryParam("accountKey", accountKey); - - return new FinancerRestTemplate>() - .exchange(transactionBuilder.toUriString(), new ParameterizedTypeReference>() { - }); - } -} diff --git a/financer-web-client/src/main/java/de/financer/template/GetExpensesAllPeriodsTemplate.java b/financer-web-client/src/main/java/de/financer/template/GetExpensesAllPeriodsTemplate.java index 45cf7c0..15edbf9 100644 --- a/financer-web-client/src/main/java/de/financer/template/GetExpensesAllPeriodsTemplate.java +++ b/financer-web-client/src/main/java/de/financer/template/GetExpensesAllPeriodsTemplate.java @@ -2,7 +2,6 @@ package de.financer.template; import de.financer.config.FinancerConfig; import de.financer.controller.Function; -import de.financer.dto.ExpensePeriodTotal; import de.financer.util.ControllerUtils; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.ResponseEntity; diff --git a/financer-web-client/src/main/java/de/financer/template/SearchTransactionsTemplate.java b/financer-web-client/src/main/java/de/financer/template/SearchTransactionsTemplate.java new file mode 100644 index 0000000..e4b9fe1 --- /dev/null +++ b/financer-web-client/src/main/java/de/financer/template/SearchTransactionsTemplate.java @@ -0,0 +1,52 @@ +package de.financer.template; + +import de.financer.config.FinancerConfig; +import de.financer.controller.Function; +import de.financer.dto.Order; +import de.financer.dto.SearchTransactionsResponseDto; +import de.financer.template.exception.FinancerRestException; +import de.financer.util.ControllerUtils; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.web.util.UriComponentsBuilder; + +public class SearchTransactionsTemplate { + public Iterable exchangeGet(FinancerConfig financerConfig, + String fromAccountKey, + String toAccountKey, + Long periodId, + Integer limit, + Order order, + Boolean taxRelevant, + boolean accountsAnd) throws FinancerRestException { + final UriComponentsBuilder transactionBuilder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_SEARCH)); + + if (fromAccountKey != null) { + transactionBuilder.queryParam("fromAccountKey", fromAccountKey); + } + + if (fromAccountKey != null) { + transactionBuilder.queryParam("toAccountKey", toAccountKey); + } + + if (periodId != null) { + transactionBuilder.queryParam("periodId", periodId); + } + + if (limit != null) { + transactionBuilder.queryParam("limit", limit); + } + + if (order != null) { + transactionBuilder.queryParam("order", order.name()); + } + + if (taxRelevant != null) { + transactionBuilder.queryParam("taxRelevant", taxRelevant); + } + + transactionBuilder.queryParam("accountsAnd", accountsAnd); + + return FinancerRestTemplate.exchangeGet(transactionBuilder, new ParameterizedTypeReference>() {}); + } +} diff --git a/financer-web-client/src/main/java/de/financer/template/exception/FinancerRestException.java b/financer-web-client/src/main/java/de/financer/template/exception/FinancerRestException.java new file mode 100644 index 0000000..88d1b1a --- /dev/null +++ b/financer-web-client/src/main/java/de/financer/template/exception/FinancerRestException.java @@ -0,0 +1,15 @@ +package de.financer.template.exception; + +import de.financer.ResponseReason; + +public class FinancerRestException extends Exception { + private final ResponseReason responseReason; + + public FinancerRestException(ResponseReason responseReason) { + this.responseReason = responseReason; + } + + public ResponseReason getResponseReason() { + return this.responseReason; + } +} diff --git a/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java b/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java index fd23bab..7da2eb1 100644 --- a/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java +++ b/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java @@ -24,6 +24,10 @@ public class ControllerUtils { return String.format("%s%s", financerConfig.getServerUrl(), function.getPath()); } + public static String buildUrl(FinancerConfig financerConfig, Function function, String id) { + return String.format("%s%s%s", financerConfig.getServerUrl(), function.getPath(), id); + } + public static List filterAndSortAccounts(Iterable accounts) { return filterAndSortAccounts(accounts, false); } diff --git a/financer-web-client/src/main/java/de/financer/util/TransactionUtils.java b/financer-web-client/src/main/java/de/financer/util/TransactionUtils.java index 444d3d0..67b7f54 100644 --- a/financer-web-client/src/main/java/de/financer/util/TransactionUtils.java +++ b/financer-web-client/src/main/java/de/financer/util/TransactionUtils.java @@ -1,13 +1,13 @@ package de.financer.util; +import de.financer.dto.SearchTransactionsResponseDto; import de.financer.model.Account; import de.financer.model.AccountType; -import de.financer.model.Transaction; import static de.financer.model.AccountType.*; public class TransactionUtils { - public static final void adjustAmount(Transaction t, Account account) { + public static void adjustAmount(SearchTransactionsResponseDto t, Account account) { boolean isFrom = t.getFromAccount().getKey().equals(account.getKey()); if (AccountType.START.equals(t.getFromAccount().getType()) && AccountType.LIABILITY.equals(t.getToAccount().getType())) { diff --git a/financer-web-client/src/main/resources/i18n/message.properties b/financer-web-client/src/main/resources/i18n/message.properties index 9a1dc57..84c1a5f 100644 --- a/financer-web-client/src/main/resources/i18n/message.properties +++ b/financer-web-client/src/main/resources/i18n/message.properties @@ -4,6 +4,7 @@ financer.account-overview.available-actions.show-closed=Show closed accounts financer.account-overview.available-actions.hide-closed=Hide closed accounts financer.account-overview.available-actions.create-account=Create new account financer.account-overview.available-actions.create-transaction=Create new transaction +financer.account-overview.available-actions.search-transactions=Search transactions financer.account-overview.available-actions.create-recurring-transaction=Create new recurring transaction financer.account-overview.available-actions.recurring-transaction-all=Show all recurring transactions financer.account-overview.available-actions.create-account-group=Create new account group @@ -98,28 +99,34 @@ financer.account-details.available-actions.close-account=Close account financer.account-details.available-actions.open-account=Open account financer.account-details.available-actions.back-to-overview=Back to overview financer.account-details.available-actions.create-transaction=Create new transaction -financer.account-details.table-header.id=ID -financer.account-details.table-header.fromAccount=From account -financer.account-details.table-header.toAccount=To account -financer.account-details.table-header.date=Date -financer.account-details.table-header.amount=Amount -financer.account-details.table-header.description=Description -financer.account-details.table-header.byRecurring=Recurring -financer.account-details.table-header.taxRelevant=Tax relevant +financer.transaction-list.table-header.id=ID +financer.transaction-list.table-header.fromAccount=From account +financer.transaction-list.table-header.toAccount=To account +financer.transaction-list.table-header.date=Date +financer.transaction-list.table-header.amount=Amount +financer.transaction-list.table-header.description=Description +financer.transaction-list.table-header.byRecurring=Recurring +financer.transaction-list.table-header.taxRelevant=Tax relevant financer.account-details.details.type=Type\: financer.account-details.details.balance=Current balance\: financer.account-details.details.group=Group\: -financer.account-details.table-header.actions=Actions -financer.account-details.table.actions.deleteTransaction=Delete -financer.account-details.table.recurring.yes=Yes -financer.account-details.table.recurring.no=No -financer.account-details.table.taxRelevant.true=Yes -financer.account-details.table.taxRelevant.false=No +financer.transaction-list.table-header.actions=Actions +financer.transaction-list.table.actions.deleteTransaction=Delete +financer.transaction-list.table.recurring.yes=Yes +financer.transaction-list.table.recurring.no=No +financer.transaction-list.table.taxRelevant.true=Yes +financer.transaction-list.table.taxRelevant.false=No financer.recurring-to-transaction-with-amount.title=financer\: create transaction from recurring with amount financer.recurring-to-transaction-with-amount.label.amount=Amount\: financer.recurring-to-transaction-with-amount.submit=Create +financer.search-transactions.title=financer\: search transactions +financer.search-transactions.back-to-overview=Back to overview +financer.search-transactions.label.fql=FQL expression\: +financer.search-transactions.submit=Search +financer.search-transactions.show-query-options=Show query options + financer.chart-select.title=Select a chart to generate financer.chart-select.submit=Select @@ -166,6 +173,7 @@ financer.heading.recurring-to-transaction-with-amount=financer\: create transact financer.heading.chart-select=financer\: select a chart to generate financer.heading.chart-config-account-group-expenses-for-period=financer\: configure account group expenses for period chart financer.heading.chart-config-account-expenses-for-period=financer\: configure account expenses for period chart +financer.heading.search-transactions=financer\: search transactions financer.cancel-back-to-overview=Cancel and back to overview diff --git a/financer-web-client/src/main/resources/i18n/message_de_DE.properties b/financer-web-client/src/main/resources/i18n/message_de_DE.properties index 16ebf59..a4f6a44 100644 --- a/financer-web-client/src/main/resources/i18n/message_de_DE.properties +++ b/financer-web-client/src/main/resources/i18n/message_de_DE.properties @@ -4,6 +4,7 @@ financer.account-overview.available-actions.show-closed=Zeige auch geschlossene financer.account-overview.available-actions.hide-closed=Verstecke geschlossene Konten financer.account-overview.available-actions.create-account=Neues Konto erstellen financer.account-overview.available-actions.create-transaction=Neue Buchung erstellen +financer.account-overview.available-actions.search-transactions=Buchungen suchen financer.account-overview.available-actions.create-recurring-transaction=Neue wiederkehrende Buchung erstellen financer.account-overview.available-actions.recurring-transaction-all=Zeige alle wiederkehrende Buchungen financer.account-overview.available-actions.create-account-group=Neue Konto-Gruppe erstellen @@ -18,6 +19,8 @@ financer.account-overview.table-header.id=ID financer.account-overview.table-header.key=Konto financer.account-overview.table-header.group=Gruppe financer.account-overview.table-header.balance=Kontostand +financer.account-overview.table-header.spending-current-period=Ausgaben aktuelle Periode +financer.account-overview.table-header.average-spending-period=Durchschnittliche Ausgaben financer.account-overview.table-header.type=Typ financer.account-overview.table-header.status=Status financer.account-overview.tooltip.status.current-expenses=Periode ab {0}. Durch Klicken des Betrags kann eine grafische \u00DCbersicht über die Ausgaben gruppiert nach Konto-Gruppe angezeigt werden @@ -96,28 +99,34 @@ financer.account-details.available-actions.close-account=Konto schlie\u00DFen financer.account-details.available-actions.open-account=Konto \u00F6ffnen financer.account-details.available-actions.back-to-overview=Zur\u00FCck zur \u00DCbersicht financer.account-details.available-actions.create-transaction=Neue Buchung erstellen -financer.account-details.table-header.id=ID -financer.account-details.table-header.fromAccount=Von Konto -financer.account-details.table-header.toAccount=An Konto -financer.account-details.table-header.date=Datum -financer.account-details.table-header.amount=Betrag -financer.account-details.table-header.description=Beschreibung -financer.account-details.table-header.byRecurring=Wiederkehrend -financer.account-details.table-header.taxRelevant=Relevant f\u00FCr Steuererkl\u00E4rung +financer.transaction-list.table-header.id=ID +financer.transaction-list.table-header.fromAccount=Von Konto +financer.transaction-list.table-header.toAccount=An Konto +financer.transaction-list.table-header.date=Datum +financer.transaction-list.table-header.amount=Betrag +financer.transaction-list.table-header.description=Beschreibung +financer.transaction-list.table-header.byRecurring=Wiederkehrend +financer.transaction-list.table-header.taxRelevant=Relevant f\u00FCr Steuererkl\u00E4rung financer.account-details.details.type=Typ\: financer.account-details.details.balance=Kontostand\: financer.account-details.details.group=Gruppe\: -financer.account-details.table-header.actions=Aktionen -financer.account-details.table.actions.deleteTransaction=L\u00F6schen -financer.account-details.table.recurring.yes=Ja -financer.account-details.table.recurring.no=Nein -financer.account-details.table.taxRelevant.true=Ja -financer.account-details.table.taxRelevant.false=Nein +financer.transaction-list.table-header.actions=Aktionen +financer.transaction-list.table.actions.deleteTransaction=L\u00F6schen +financer.transaction-list.table.recurring.yes=Ja +financer.transaction-list.table.recurring.no=Nein +financer.transaction-list.table.taxRelevant.true=Ja +financer.transaction-list.table.taxRelevant.false=Nein financer.recurring-to-transaction-with-amount.title=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen financer.recurring-to-transaction-with-amount.label.amount=Betrag\: financer.recurring-to-transaction-with-amount.submit=Erstellen +financer.search-transactions.title=financer\: Buchungen suchen +financer.search-transactions.back-to-overview=Zur\u00FCck zur \u00DCbersicht +financer.search-transactions.label.fql=FQL Ausdruck\: +financer.search-transactions.submit=Suchen +financer.search-transactions.show-query-options=Suchoptionen anzeigen + financer.chart-select.title=Ein Diagramm zum Erzeugen ausw\u00E4hlen financer.chart-select.submit=Ausw\u00E4hlen @@ -157,12 +166,13 @@ financer.heading.account-new=financer\: Neues Konto erstellen financer.heading.account-group-new=financer\: Neue Konto-Gruppe erstellen financer.heading.account-overview=financer\: \u00DCbersicht financer.heading.account-details=financer\: Kontodetails f\u00FCr {0} -financer.heading.recurring-transaction-list.dueToday=financer\: wiederkehrende Buchungen heute f\u00E4llig -financer.heading.recurring-transaction-list.active=financer\: aktive wiederkehrende Buchungen -financer.heading.recurring-transaction-list.all=financer\: alle wiederkehrende Buchungen +financer.heading.recurring-transaction-list.dueToday=financer\: Wiederkehrende Buchungen heute f\u00E4llig +financer.heading.recurring-transaction-list.active=financer\: Aktive wiederkehrende Buchungen +financer.heading.recurring-transaction-list.all=financer\: Alle wiederkehrende Buchungen financer.heading.recurring-to-transaction-with-amount=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen -financer.heading.chart-select=financer\: ein Diagramm zum Erzeugen ausw\u00E4hlen -financer.heading.chart-config-account-group-expenses-for-period=financer\: konfigurieren von Ausgaben f\u00FCr Periode gruppiert nach Konto-Gruppe Diagramm +financer.heading.chart-select=financer\: Ein Diagramm zum Erzeugen ausw\u00E4hlen +financer.heading.chart-config-account-group-expenses-for-period=financer\: Konfigurieren von Ausgaben f\u00FCr Periode gruppiert nach Konto-Gruppe Diagramm +financer.heading.search-transactions=financer\: Buchungen suchen financer.cancel-back-to-overview=Abbrechen und zur \u00DCbersicht diff --git a/financer-web-client/src/main/resources/static/changelog.txt b/financer-web-client/src/main/resources/static/changelog.txt index a799e85..4930007 100644 --- a/financer-web-client/src/main/resources/static/changelog.txt +++ b/financer-web-client/src/main/resources/static/changelog.txt @@ -2,7 +2,10 @@ v26 -> v27: - Changed sort order of accounts in overview page. The accounts are now sorted by the account type first (BCILES), then by the account group name and then by the account ID, leading to an overall more organic order of accounts - Add tax relevance flag to transaction and recurring transaction creation. This flag denotes whether a transaction or - the instances of a recurring transaction are relevant for a tax declaration. This is preparation for extended reports. + the instances of a recurring transaction are relevant for a tax declaration. This is preparation for extended reports +- Add searching of transactions via FQL (Financer Query Language) +- Rework /transaction end point to better adhere to REST API requirements (proper HTTP return codes and HTTP method + usage) v25 -> v26: - Close of the current expense period now creates null statistic entries for accounts that have not been used in diff --git a/financer-web-client/src/main/resources/static/css/main.css b/financer-web-client/src/main/resources/static/css/main.css index 3965647..70c5f4f 100644 --- a/financer-web-client/src/main/resources/static/css/main.css +++ b/financer-web-client/src/main/resources/static/css/main.css @@ -12,7 +12,7 @@ /* --------------------- */ #account-overview-table, -#account-transaction-table, +#transaction-table, #recurring-transaction-list-table { width: 100%; border-collapse: collapse; @@ -23,8 +23,8 @@ #account-overview-table th, #account-overview-table td, -#account-transaction-table th, -#account-transaction-table td, +#transaction-table th, +#transaction-table td, #recurring-transaction-list-table th, #recurring-transaction-list-table td { border-bottom: 1px solid #ddd; @@ -33,7 +33,7 @@ } #account-overview-table th, -#account-transaction-table th, +#transaction-table th, #recurring-transaction-list-table th { position: sticky; top: 0px; @@ -107,13 +107,18 @@ tr:hover { #recurring-to-transaction-with-amount-form *, #new-account-group-form *, #chart-config-account-group-expenses-for-period-form *, -#chart-config-account-expenses-for-period-form * { +#chart-config-account-expenses-for-period-form *, +#search-transactions-form * { display: block; margin-top: 1em; width: 20em; box-sizing: border-box; } +#search-transactions-form > input[type=text] { + width: 100% !important; +} + #chart-select-form div { width: 20em; margin-top: 1em; @@ -187,4 +192,15 @@ input[type=submit] { #type-row { width: var(--type-row-width); padding: 0px !important +} + +#search-transactions-fql-detail { + border: 1px solid grey; + border-radius: 0.5em; + padding-inline: 0.3em; + background-color: lightgrey; +} + +#search-transactions-fql-detail > * { + font-size: 0.7em; } \ No newline at end of file diff --git a/financer-web-client/src/main/resources/static/readme.txt b/financer-web-client/src/main/resources/static/readme.txt index f109b6e..f3dfec8 100644 --- a/financer-web-client/src/main/resources/static/readme.txt +++ b/financer-web-client/src/main/resources/static/readme.txt @@ -14,8 +14,9 @@ 7. Transactions 8. Recurring transactions 9. Reporting - 10. Setup - 11. Planned features + 10. FQL + 11. Setup + 12. Planned features 1. About ======== @@ -170,12 +171,15 @@ 9. Reporting ============ - 10. Setup + 10. FQL + ======= + + 11. Setup ========= This chapter explains how to setup a financer instance. It requires PostgreSQL as a database backend and a Java Servlet Container (e.g. Apache Tomcat) as a runtime environment. - 10.1 Database setup + 11.1 Database setup ------------------- First install PostgreSQL. Then create a user for financer: sudo -iu postgres @@ -191,7 +195,7 @@ \q exit - 11. Planned features + 12. Planned features ==================== This chapter lists planned features. The list is in no particular order: - Transaction import from online banking (file based) diff --git a/financer-web-client/src/main/resources/templates/account/accountDetails.html b/financer-web-client/src/main/resources/templates/account/accountDetails.html index 9d3b33a..c510f1b 100644 --- a/financer-web-client/src/main/resources/templates/account/accountDetails.html +++ b/financer-web-client/src/main/resources/templates/account/accountDetails.html @@ -35,36 +35,7 @@ - - - - - - -
- - - - - - - - -
- - - - - - - - - -
- -
-
+
\ No newline at end of file diff --git a/financer-web-client/src/main/resources/templates/account/accountOverview.html b/financer-web-client/src/main/resources/templates/account/accountOverview.html index 1a929db..83a39eb 100644 --- a/financer-web-client/src/main/resources/templates/account/accountOverview.html +++ b/financer-web-client/src/main/resources/templates/account/accountOverview.html @@ -49,6 +49,7 @@
+ + + + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" th:href="@{/css/main.css}"> + <link rel="shortcut icon" th:href="@{/favicon.ico}" /> +</head> +<body> +<h1 th:text="#{financer.heading.search-transactions}" /> +<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/> +<a th:href="@{/accountOverview}" th:text="#{financer.search-transactions.back-to-overview}"/> +<form id="search-transactions-form" action="#" th:action="@{/searchTransactions}" th:object="${form}" + method="post"> + <label for="inputFql" th:text="#{financer.search-transactions.label.fql}"/> + <input type="text" id="inputFql" th:field="*{fql}"/> + <input type="submit" th:value="#{financer.search-transactions.submit}"/> +</form> +<details> + <summary th:text="#{financer.search-transactions.show-query-options}"/> + <div id="search-transactions-fql-detail"> + <p> + <div>Available fields:</div> + <ul> + <li>amount: the amount of a transaction</li> + <li>fromAccount: the key of the from account</li> + <li>toAccount: the key of the to account</li> + <li>fromAccountGroup: the name of the account group of the from account</li> + <li>toAccountGroup: the name of the account group of the to account</li> + <li>date: the date of the transaction in the format yyyy-mm-dd</li> + <li>recurring: whether the transaction has been created from a recurring transaction</li> + <li>taxRelevant: whether the transaction is relevant for tax declaration</li> + <li>period: the period the transaction is assigned to + <ul> + <li>CURRENT: denotes the current expense period</li> + <li>LAST: denotes the last expense period</li> + <li>CURRENT_YEAR: denotes the current year period</li> + <li>LAST_YEAR: denotes the last year period</li> + <li>GRAND_TOTAL: denotes the grand total period</li> + </ul> + </li> + </ul> + </p> + <p> + <div>General</div> + <ul> + <li>Conjunctions with OR, AND</li> + <li>Grouping of expressions with parenthesis</li> + <li>Strings enclosed in single quotes</li> + <li>Case-insensitive</li> + <li>Possible operators =, >, >=, <, <=, !=</li> + <li>Ordering for a single field ASC, DESC via ORDER BY</li> + </ul> + </p> + </div> +</details> +<div th:replace="transaction/transactionList :: transaction-list"/> +<div th:replace="includes/footer :: footer"/> +</body> +</html> \ No newline at end of file diff --git a/financer-web-client/src/main/resources/templates/transaction/transactionList.html b/financer-web-client/src/main/resources/templates/transaction/transactionList.html new file mode 100644 index 0000000..3786861 --- /dev/null +++ b/financer-web-client/src/main/resources/templates/transaction/transactionList.html @@ -0,0 +1,32 @@ +<div id="transaction-list-container" th:fragment="transaction-list"> + <table id="transaction-table"> + <tr> + <th class="hideable-column" th:text="#{financer.transaction-list.table-header.id}"/> + <th th:text="#{financer.transaction-list.table-header.fromAccount}"/> + <th th:text="#{financer.transaction-list.table-header.toAccount}"/> + <th th:text="#{financer.transaction-list.table-header.date}"/> + <th th:text="#{financer.transaction-list.table-header.amount}"/> + <th th:text="#{financer.transaction-list.table-header.description}"/> + <th th:text="#{financer.transaction-list.table-header.byRecurring}"/> + <th th:text="#{financer.transaction-list.table-header.taxRelevant}"/> + <th th:if="${showActions}" th:text="#{financer.transaction-list.table-header.actions}"/> + </tr> + <tr th:each="transaction : ${transactions}"> + <td class="hideable-column" th:text="${transaction.id}"/> + <td th:text="${transaction.fromAccount.key}" /> + <td th:text="${transaction.toAccount.key}" /> + <td th:text="${#temporals.format(transaction.date)}" /> + <td th:text="${#numbers.formatDecimal(transaction.amount/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/> + <td th:text="${transaction.description}" /> + <td th:if="${transaction.recurring}" th:text="#{financer.transaction-list.table.recurring.yes}" /> + <td th:if="${!transaction.recurring}" th:text="#{financer.transaction-list.table.recurring.no}" /> + <td th:text="#{'financer.transaction-list.table.taxRelevant.' + ${transaction.taxRelevant}}" /> + <td th:if="${showActions}"> + <div id="account-transaction-table-actions-container"> + <a th:href="@{/deleteTransaction(transactionId=${transaction.id}, accountKey=${account.key})}" + th:text="#{financer.transaction-list.table.actions.deleteTransaction}"/> + </div> + </td> + </tr> + </table> +</div> \ No newline at end of file