diff --git a/pom.xml b/pom.xml index 527f6e4..d8499cb 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,18 @@ + + org.springframework + spring-context + 6.2.12 + + + + org.springframework + spring-webmvc + 6.2.12 + + org.apache.tomcat.embed tomcat-embed-core @@ -31,6 +43,27 @@ jackson-datatype-jsr310 2.20.0 + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + 2.20.0 + + + org.hibernate.validator + hibernate-validator + 9.0.1.Final + + + org.glassfish.expressly + expressly + 6.0.0 + + + + org.thymeleaf + thymeleaf-spring6 + 3.1.3.RELEASE + diff --git a/src/main/java/com/sergiubarsa/mybank/Application.java b/src/main/java/com/sergiubarsa/mybank/Application.java deleted file mode 100644 index ac2f7cf..0000000 --- a/src/main/java/com/sergiubarsa/mybank/Application.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sergiubarsa.mybank; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.sergiubarsa.mybank.services.TransactionService; - -import java.time.Clock; - -public class Application { - - public static TransactionService transactionService = new TransactionService(Clock.systemDefaultZone()); - public static ObjectMapper objectMapper = new ObjectMapper(); - - static { - objectMapper.registerModule(new JavaTimeModule()); - objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - } -} diff --git a/src/main/java/com/sergiubarsa/mybank/ApplicationRunner.java b/src/main/java/com/sergiubarsa/mybank/ApplicationRunner.java index 12aefc0..2f498e1 100644 --- a/src/main/java/com/sergiubarsa/mybank/ApplicationRunner.java +++ b/src/main/java/com/sergiubarsa/mybank/ApplicationRunner.java @@ -1,10 +1,12 @@ package com.sergiubarsa.mybank; -import com.sergiubarsa.mybank.web.MyBankServlet; +import com.sergiubarsa.mybank.context.ApplicationConfiguration; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.Wrapper; import org.apache.catalina.startup.Tomcat; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; public class ApplicationRunner { @@ -15,13 +17,21 @@ void main() throws LifecycleException { tomcat.getConnector(); Context ctx = tomcat.addContext("", null); - Wrapper servlet = Tomcat.addServlet(ctx, "myBankServlet", new MyBankServlet()); + + Wrapper servlet = Tomcat.addServlet(ctx, "myDispatcherServlet", new DispatcherServlet(getCtx())); servlet.setLoadOnStartup(1); servlet.addMapping("/*"); tomcat.start(); } + AnnotationConfigWebApplicationContext getCtx() { + AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); + ctx.register(ApplicationConfiguration.class); + + return ctx; + } + private static int getPort() { String port = System.getProperty("server.port"); if (port == null) { diff --git a/src/main/java/com/sergiubarsa/mybank/context/ApplicationConfiguration.java b/src/main/java/com/sergiubarsa/mybank/context/ApplicationConfiguration.java new file mode 100644 index 0000000..bc3957e --- /dev/null +++ b/src/main/java/com/sergiubarsa/mybank/context/ApplicationConfiguration.java @@ -0,0 +1,63 @@ +package com.sergiubarsa.mybank.context; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.sergiubarsa.mybank.ApplicationRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.PropertySource; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver; +import org.thymeleaf.spring6.view.ThymeleafViewResolver; + +import java.time.Clock; + +@ComponentScan(basePackageClasses = ApplicationRunner.class) +@PropertySource("classpath:/application.properties") +@EnableWebMvc +public class ApplicationConfiguration { + + + @Bean + public Clock clock() { + return Clock.systemDefaultZone(); + } + + @Bean + public ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + + return objectMapper; + } + + @Bean + public ThymeleafViewResolver viewResolver(ApplicationContext context) { + ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); + viewResolver.setTemplateEngine(templateEngine(context)); + + viewResolver.setOrder(1); // optional + viewResolver.setViewNames(new String[] {"*.html", "*.xhtml"}); // optional + return viewResolver; + } + + @Bean + public SpringTemplateEngine templateEngine(ApplicationContext context) { + SpringTemplateEngine templateEngine = new SpringTemplateEngine(); + templateEngine.setTemplateResolver(templateResolver(context)); + return templateEngine; + } + + @Bean + public SpringResourceTemplateResolver templateResolver(ApplicationContext context) { + SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver(); + templateResolver.setPrefix("classpath:/templates/"); + templateResolver.setCacheable(false); + templateResolver.setApplicationContext(context); + return templateResolver; + } +} diff --git a/src/main/java/com/sergiubarsa/mybank/dto/ErrorDto.java b/src/main/java/com/sergiubarsa/mybank/dto/ErrorDto.java new file mode 100644 index 0000000..db6d53a --- /dev/null +++ b/src/main/java/com/sergiubarsa/mybank/dto/ErrorDto.java @@ -0,0 +1,29 @@ +package com.sergiubarsa.mybank.dto; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ErrorDto { + + private String errorMessage; + private final List failedFields = new ArrayList(); + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public List getFailedFields() { + return Collections.unmodifiableList(failedFields); + } + + public void addFailedField(String failedField) { + failedFields.add(failedField); + } + + +} diff --git a/src/main/java/com/sergiubarsa/mybank/dto/TransactionDto.java b/src/main/java/com/sergiubarsa/mybank/dto/TransactionDto.java new file mode 100644 index 0000000..ec43c25 --- /dev/null +++ b/src/main/java/com/sergiubarsa/mybank/dto/TransactionDto.java @@ -0,0 +1,43 @@ +package com.sergiubarsa.mybank.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class TransactionDto { + + + @Min(10) + @Max(100) + private int amount; + + @NotBlank + private String reference; + + @NotBlank + private String userId; + + public int getAmount() { + return amount; + } + + public String getReference() { + return reference; + } + + public void setAmount(int amount) { + this.amount = amount; + } + + public void setReference(String reference) { + this.reference = reference; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } +} diff --git a/src/main/java/com/sergiubarsa/mybank/model/Transaction.java b/src/main/java/com/sergiubarsa/mybank/model/Transaction.java index 439aebe..945cadc 100644 --- a/src/main/java/com/sergiubarsa/mybank/model/Transaction.java +++ b/src/main/java/com/sergiubarsa/mybank/model/Transaction.java @@ -8,12 +8,16 @@ public class Transaction { private int amount; private Instant timestamp; private String reference; + private String slogan; + private String userId; - public Transaction(int amount, UUID id, String reference, Instant timestamp) { + public Transaction(int amount, UUID id, String reference, Instant timestamp, String slogan, String userId) { this.amount = amount; this.id = id; this.reference = reference; this.timestamp = timestamp; + this.slogan = slogan; + this.userId = userId; } public int getAmount() { @@ -31,4 +35,12 @@ public String getReference() { public Instant getTimestamp() { return timestamp; } + + public String getSlogan() { + return slogan; + } + + public String getUserId() { + return userId; + } } diff --git a/src/main/java/com/sergiubarsa/mybank/services/TransactionService.java b/src/main/java/com/sergiubarsa/mybank/services/TransactionService.java index 492241a..94f8c63 100644 --- a/src/main/java/com/sergiubarsa/mybank/services/TransactionService.java +++ b/src/main/java/com/sergiubarsa/mybank/services/TransactionService.java @@ -1,6 +1,8 @@ package com.sergiubarsa.mybank.services; import com.sergiubarsa.mybank.model.Transaction; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; import java.time.Clock; import java.time.Instant; @@ -8,25 +10,36 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +@Service public class TransactionService { List transactions = new CopyOnWriteArrayList<>(); private final Clock clock; + private final String slogan; - public TransactionService(Clock clock) { + public TransactionService(Clock clock, @Value("${bank.slogan}") String slogan) { this.clock = clock; + this.slogan = slogan; } - public void createTransaction(int amount, String reference) { + public Transaction createTransaction(int amount, String reference, String userId) { Instant timestamp = clock.instant(); UUID uuid = UUID.randomUUID(); - Transaction transaction = new Transaction(amount, uuid, reference, timestamp); + + Transaction transaction = new Transaction(amount, uuid, reference, timestamp, slogan, userId ); transactions.add(transaction); + + return transaction; } public List findAll() { return Collections.unmodifiableList(transactions); } + + public List findAllForUserId(String userId) { + return transactions.stream().filter(trans -> userId.equals(trans.getUserId())).toList(); + } } diff --git a/src/main/java/com/sergiubarsa/mybank/web/MyBankController.java b/src/main/java/com/sergiubarsa/mybank/web/MyBankController.java new file mode 100644 index 0000000..a865b05 --- /dev/null +++ b/src/main/java/com/sergiubarsa/mybank/web/MyBankController.java @@ -0,0 +1,34 @@ +package com.sergiubarsa.mybank.web; + +import com.sergiubarsa.mybank.dto.TransactionDto; +import com.sergiubarsa.mybank.model.Transaction; +import com.sergiubarsa.mybank.services.TransactionService; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +public class MyBankController { + + private final TransactionService transactionService; + + public MyBankController(TransactionService transactionService) { + this.transactionService = transactionService; + } + + @GetMapping("/transactions") + public List get() { + return transactionService.findAll(); + } + + @PostMapping("/transactions") + public Transaction create(@RequestBody @Valid TransactionDto transactionDto) { + return transactionService.createTransaction(transactionDto.getAmount(), + transactionDto.getReference(), + transactionDto.getUserId()); + } +} diff --git a/src/main/java/com/sergiubarsa/mybank/web/MyBankHtmlController.java b/src/main/java/com/sergiubarsa/mybank/web/MyBankHtmlController.java new file mode 100644 index 0000000..90e8e6a --- /dev/null +++ b/src/main/java/com/sergiubarsa/mybank/web/MyBankHtmlController.java @@ -0,0 +1,36 @@ +package com.sergiubarsa.mybank.web; + +import com.sergiubarsa.mybank.model.Transaction; +import com.sergiubarsa.mybank.services.TransactionService; +import com.sergiubarsa.mybank.web.forms.CreateTransactionForm; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; + +import java.util.List; + +@Controller +public class MyBankHtmlController { + + private final TransactionService transactionService; + + public MyBankHtmlController(TransactionService transactionService) { + this.transactionService = transactionService; + } + + @GetMapping("/account/{userId}") + public String getMapping(@PathVariable("userId") String userId, Model model) { + List transactions = transactionService.findAllForUserId(userId); + model.addAttribute("transactions", transactions); + return "account.html"; + } + + @PostMapping("/transactionsviahtml") + public String createViaHtml(@ModelAttribute CreateTransactionForm form) { + transactionService.createTransaction(form.getAmount(), form.getReference(), form.getReceivingUserId()); + return "redirect:/account/" + form.getReceivingUserId(); + } +} diff --git a/src/main/java/com/sergiubarsa/mybank/web/MyBankRestControllerAdvice.java b/src/main/java/com/sergiubarsa/mybank/web/MyBankRestControllerAdvice.java new file mode 100644 index 0000000..d10693b --- /dev/null +++ b/src/main/java/com/sergiubarsa/mybank/web/MyBankRestControllerAdvice.java @@ -0,0 +1,26 @@ +package com.sergiubarsa.mybank.web; + +import com.sergiubarsa.mybank.dto.ErrorDto; +import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class MyBankRestControllerAdvice { + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + public ErrorDto handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { + ErrorDto errorDto = new ErrorDto(); + errorDto.setErrorMessage("Validation failed"); + exception.getBindingResult().getFieldErrors() + .stream() + .map(FieldError::getField) + .forEach(errorDto::addFailedField); + return errorDto; + } + +} diff --git a/src/main/java/com/sergiubarsa/mybank/web/MyBankServlet.java b/src/main/java/com/sergiubarsa/mybank/web/MyBankServlet.java deleted file mode 100644 index 005262d..0000000 --- a/src/main/java/com/sergiubarsa/mybank/web/MyBankServlet.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.sergiubarsa.mybank.web; - -import com.sergiubarsa.mybank.Application; -import com.sergiubarsa.mybank.model.Transaction; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.io.IOException; -import java.util.List; - -public class MyBankServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - if (req.getRequestURI().equalsIgnoreCase("/transactions")) { - List allTransactions = Application.transactionService.findAll(); - - resp.setContentType("application/json; charset=UTF-8"); - resp.getWriter().print(Application.objectMapper.writeValueAsString(allTransactions)); - } - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) { - if (req.getRequestURI().equalsIgnoreCase("/transactions")) { - int amount = Integer.parseInt(req.getParameter("amount")); - String reference = req.getParameter("reference"); - Application.transactionService.createTransaction(amount, reference); - } - } -} diff --git a/src/main/java/com/sergiubarsa/mybank/web/forms/CreateTransactionForm.java b/src/main/java/com/sergiubarsa/mybank/web/forms/CreateTransactionForm.java new file mode 100644 index 0000000..348ff9c --- /dev/null +++ b/src/main/java/com/sergiubarsa/mybank/web/forms/CreateTransactionForm.java @@ -0,0 +1,32 @@ +package com.sergiubarsa.mybank.web.forms; + +public class CreateTransactionForm { + + private String receivingUserId; + private int amount; + private String reference; + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } + + public String getReceivingUserId() { + return receivingUserId; + } + + public void setReceivingUserId(String receivingUserId) { + this.receivingUserId = receivingUserId; + } + + public String getReference() { + return reference; + } + + public void setReference(String reference) { + this.reference = reference; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..b3c98f2 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +bank.slogan=Hello Bank \ No newline at end of file diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html new file mode 100644 index 0000000..612e757 --- /dev/null +++ b/src/main/resources/templates/account.html @@ -0,0 +1,60 @@ + + + + Transactions + + +

Your Transactions

+ + + + + + + + + + + + + + + + + + + + + + + + + +
#IDAmountReferenceTimestampSloganUser
1abc$refdatesloganuser
+ +

No transactions recorded. Life is boring.

+ +

Create Transaction

+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + + \ No newline at end of file