diff --git a/pom.xml b/pom.xml index fe759de..9a77fb0 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,12 @@ spring-security-web 6.1.5 + + + org.apache.commons + commons-math3 + 3.6.1 + package diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/ExpenseController.java b/src/main/java/com/faf223/expensetrackerfaf/controller/ExpenseController.java index 4e092d0..d4c667d 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/controller/ExpenseController.java +++ b/src/main/java/com/faf223/expensetrackerfaf/controller/ExpenseController.java @@ -9,11 +9,9 @@ import com.faf223.expensetrackerfaf.model.User; import com.faf223.expensetrackerfaf.service.ExpenseCategoryService; import com.faf223.expensetrackerfaf.service.ExpenseService; import com.faf223.expensetrackerfaf.service.UserService; +import com.faf223.expensetrackerfaf.util.DataExtender; import com.faf223.expensetrackerfaf.util.errors.ErrorResponse; -import com.faf223.expensetrackerfaf.util.exceptions.TransactionDoesNotBelongToTheUserException; -import com.faf223.expensetrackerfaf.util.exceptions.TransactionNotCreatedException; -import com.faf223.expensetrackerfaf.util.exceptions.TransactionNotUpdatedException; -import com.faf223.expensetrackerfaf.util.exceptions.TransactionsNotFoundException; +import com.faf223.expensetrackerfaf.util.exceptions.*; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -26,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.Month; import java.util.Collections; @@ -134,6 +133,36 @@ public class ExpenseController { throw new TransactionsNotFoundException("The expenses have not been found"); } + @GetMapping("/extend-data") + public ResponseEntity> extendData(@RequestParam Optional userUuid, + @RequestParam Optional extendValue, + @RequestParam Optional extrapolationCount) { + if(userUuid.isEmpty()) + throw new RequiredParamMissingException("User UUID has not been specified"); + + if(extendValue.isEmpty()) + throw new RequiredParamMissingException("Extend value has not been specified"); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + User user; + + if (authentication != null && authentication.getPrincipal() instanceof UserDetails userDetails) + user = userService.getUserByEmail(userDetails.getUsername()); + else + user = null; + + if(user == null) + throw new UserNotFoundException("User with the specified UUID has not been found"); + + if(extendValue.get().equalsIgnoreCase("i")) + return ResponseEntity.ok(DataExtender.interpolate(user.getExpenses())); + + if(extendValue.get().equalsIgnoreCase("e")) + return extrapolationCount.map(integer -> ResponseEntity.ok(DataExtender.extrapolate(user.getExpenses(), integer))).orElseGet(() -> ResponseEntity.ok(DataExtender.extrapolate(user.getExpenses(), 10))); + + throw new WrongParamValueException("Wrong extend value has been specified (use either \"i\" or \"e\")"); + } + @GetMapping("/categories") public ResponseEntity> getAllCategories() { List categories = expenseCategoryService.getAllCategories(); diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/GlobalExceptionHandler.java b/src/main/java/com/faf223/expensetrackerfaf/controller/GlobalExceptionHandler.java index 17a003d..ad4a234 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/controller/GlobalExceptionHandler.java +++ b/src/main/java/com/faf223/expensetrackerfaf/controller/GlobalExceptionHandler.java @@ -12,72 +12,47 @@ public class GlobalExceptionHandler { @ExceptionHandler private ResponseEntity handleDoesNotBelongException(TransactionDoesNotBelongToTheUserException e) { - ErrorResponse response = new ErrorResponse( - e.getMessage(), - System.currentTimeMillis() - ); - - return new ResponseEntity<>(response, HttpStatus.FORBIDDEN); + return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.FORBIDDEN); } @ExceptionHandler private ResponseEntity handleTransactionNotCreatedException(TransactionNotCreatedException e) { - ErrorResponse response = new ErrorResponse( - e.getMessage(), - System.currentTimeMillis() - ); - - return new ResponseEntity<>(response, HttpStatus.NOT_MODIFIED); + return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_MODIFIED); } @ExceptionHandler private ResponseEntity handleTransactionsNotFoundException(TransactionsNotFoundException e) { - ErrorResponse response = new ErrorResponse( - e.getMessage(), - System.currentTimeMillis() - ); - - return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_FOUND); } @ExceptionHandler private ResponseEntity handleUserNotFoundException(UserNotFoundException e) { - ErrorResponse response = new ErrorResponse( - e.getMessage(), - System.currentTimeMillis() - ); - - return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_FOUND); } @ExceptionHandler private ResponseEntity handleTransactionNotUpdatedException(TransactionNotUpdatedException e) { - ErrorResponse response = new ErrorResponse( - e.getMessage(), - System.currentTimeMillis() - ); - - return new ResponseEntity<>(response, HttpStatus.NOT_MODIFIED); + return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_MODIFIED); } @ExceptionHandler private ResponseEntity handleUserNotAuthenticatedException(UserNotAuthenticatedException e) { - ErrorResponse response = new ErrorResponse( - e.getMessage(), - System.currentTimeMillis() - ); - - return new ResponseEntity<>(response, HttpStatus.FORBIDDEN); + return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.FORBIDDEN); } @ExceptionHandler private ResponseEntity handleUserNotCreatedException(UserNotCreatedException e) { - ErrorResponse response = new ErrorResponse( - e.getMessage(), - System.currentTimeMillis() - ); + return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_MODIFIED); + } - return new ResponseEntity<>(response, HttpStatus.NOT_MODIFIED); + @ExceptionHandler + private ResponseEntity handleRequiredParamMissingException(RequiredParamMissingException e) { + return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler + private ResponseEntity handleWrongParamValueException(WrongParamValueException e) { + return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.BAD_REQUEST); } } diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/IncomeController.java b/src/main/java/com/faf223/expensetrackerfaf/controller/IncomeController.java index 1a9f179..eec468a 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/controller/IncomeController.java +++ b/src/main/java/com/faf223/expensetrackerfaf/controller/IncomeController.java @@ -9,11 +9,9 @@ import com.faf223.expensetrackerfaf.model.User; import com.faf223.expensetrackerfaf.service.IncomeCategoryService; import com.faf223.expensetrackerfaf.service.IncomeService; import com.faf223.expensetrackerfaf.service.UserService; +import com.faf223.expensetrackerfaf.util.DataExtender; import com.faf223.expensetrackerfaf.util.errors.ErrorResponse; -import com.faf223.expensetrackerfaf.util.exceptions.TransactionDoesNotBelongToTheUserException; -import com.faf223.expensetrackerfaf.util.exceptions.TransactionNotCreatedException; -import com.faf223.expensetrackerfaf.util.exceptions.TransactionNotUpdatedException; -import com.faf223.expensetrackerfaf.util.exceptions.TransactionsNotFoundException; +import com.faf223.expensetrackerfaf.util.exceptions.*; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -26,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.Month; import java.util.Collections; @@ -134,6 +133,36 @@ public class IncomeController { throw new TransactionsNotFoundException("The expenses have not been found"); } + @GetMapping("/extend-data") + public ResponseEntity> extendData(@RequestParam Optional userUuid, + @RequestParam Optional extendValue, + @RequestParam Optional extrapolationCount) { + if(userUuid.isEmpty()) + throw new RequiredParamMissingException("User UUID has not been specified"); + + if(extendValue.isEmpty()) + throw new RequiredParamMissingException("Extend value has not been specified"); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + User user; + + if (authentication != null && authentication.getPrincipal() instanceof UserDetails userDetails) + user = userService.getUserByEmail(userDetails.getUsername()); + else + user = null; + + if(user == null) + throw new UserNotFoundException("User with the specified UUID has not been found"); + + if(extendValue.get().equalsIgnoreCase("i")) + return ResponseEntity.ok(DataExtender.interpolate(user.getIncomes())); + + if(extendValue.get().equalsIgnoreCase("e")) + return extrapolationCount.map(integer -> ResponseEntity.ok(DataExtender.extrapolate(user.getIncomes(), integer))).orElseGet(() -> ResponseEntity.ok(DataExtender.extrapolate(user.getIncomes(), 10))); + + throw new WrongParamValueException("Wrong extend value has been specified (use either \"i\" or \"e\")"); + } + @GetMapping("/categories") public ResponseEntity> getAllCategories() { List categories = incomeCategoryService.getAllCategories(); diff --git a/src/main/java/com/faf223/expensetrackerfaf/util/DataExtender.java b/src/main/java/com/faf223/expensetrackerfaf/util/DataExtender.java new file mode 100644 index 0000000..6765886 --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/util/DataExtender.java @@ -0,0 +1,63 @@ +package com.faf223.expensetrackerfaf.util; + +import com.faf223.expensetrackerfaf.model.IMoneyTransaction; +import org.apache.commons.math3.analysis.interpolation.LinearInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +public class DataExtender { + + public static List interpolate(List transactions) { + List values = new ArrayList<>(); + + // Perform linear interpolation on the amount field + if (transactions.size() > 1) { + LinearInterpolator interpolator = new LinearInterpolator(); + + double[] xValues = new double[transactions.size()]; + double[] yValues = new double[transactions.size()]; + + for (int i = 0; i < transactions.size(); i++) { + xValues[i] = i; + yValues[i] = transactions.get(i).getAmount().doubleValue(); + } + + PolynomialSplineFunction splineFunction = interpolator.interpolate(xValues, yValues); + + // Interpolate values between the first and last data points + for (int i = 1; i < transactions.size() - 1; i++) + values.add(BigDecimal.valueOf(splineFunction.value(i + 0.5))); // Interpolate at the midpoint + } + + return values; + } + + public static List extrapolate(List transactions, int count) { + List values = new ArrayList<>(); + + // Perform linear extrapolation on the amount field + if (transactions.size() > 1) { + LinearInterpolator interpolator = new LinearInterpolator(); + + double[] xValues = new double[transactions.size()]; + double[] yValues = new double[transactions.size()]; + + for (int i = 0; i < transactions.size(); i++) { + xValues[i] = i; + yValues[i] = transactions.get(i).getAmount().doubleValue(); + } + + PolynomialSplineFunction splineFunction = interpolator.interpolate(xValues, yValues); + + // Extrapolate values beyond the last data point + for (int i = transactions.size(); i < transactions.size() + count; i++) + values.add(BigDecimal.valueOf(splineFunction.value(i))); // Extrapolate by extending the x-axis + } + + return values; + } + +} diff --git a/src/main/java/com/faf223/expensetrackerfaf/util/exceptions/RequiredParamMissingException.java b/src/main/java/com/faf223/expensetrackerfaf/util/exceptions/RequiredParamMissingException.java new file mode 100644 index 0000000..e99ed84 --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/util/exceptions/RequiredParamMissingException.java @@ -0,0 +1,7 @@ +package com.faf223.expensetrackerfaf.util.exceptions; + +public class RequiredParamMissingException extends RuntimeException { + public RequiredParamMissingException(String message) { + super(message); + } +} diff --git a/src/main/java/com/faf223/expensetrackerfaf/util/exceptions/WrongParamValueException.java b/src/main/java/com/faf223/expensetrackerfaf/util/exceptions/WrongParamValueException.java new file mode 100644 index 0000000..1cc7aa6 --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/util/exceptions/WrongParamValueException.java @@ -0,0 +1,7 @@ +package com.faf223.expensetrackerfaf.util.exceptions; + +public class WrongParamValueException extends RuntimeException { + public WrongParamValueException(String message) { + super(message); + } +}