From 2db74f6dcd5ad975ab9e11d4a5f0ccc8850410f2 Mon Sep 17 00:00:00 2001 From: birewallg Date: Tue, 17 Aug 2021 19:10:46 +0300 Subject: [PATCH 1/5] Task 1: add tests for service, code refactor --- .../com/devexperts/account/AccountKey.java | 19 +++++ .../service/AccountServiceImpl.java | 16 ++--- .../service/AccountServiceImplTest.java | 71 +++++++++++++++++++ 3 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 src/test/java/com/devexperts/service/AccountServiceImplTest.java diff --git a/src/main/java/com/devexperts/account/AccountKey.java b/src/main/java/com/devexperts/account/AccountKey.java index 1b0a233..1aae4af 100644 --- a/src/main/java/com/devexperts/account/AccountKey.java +++ b/src/main/java/com/devexperts/account/AccountKey.java @@ -1,5 +1,7 @@ package com.devexperts.account; +import java.util.Objects; + /** * Unique Account identifier * @@ -17,4 +19,21 @@ private AccountKey(long accountId) { public static AccountKey valueOf(long accountId) { return new AccountKey(accountId); } + + public long getAccountId() { + return accountId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AccountKey that = (AccountKey) o; + return accountId == that.accountId; + } + + @Override + public int hashCode() { + return Objects.hash(accountId); + } } diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java index 91261ba..6b85c9d 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -4,13 +4,12 @@ import com.devexperts.account.AccountKey; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; @Service public class AccountServiceImpl implements AccountService { - private final List accounts = new ArrayList<>(); + private final HashMap accounts = new HashMap<>(); @Override public void clear() { @@ -19,15 +18,16 @@ public void clear() { @Override public void createAccount(Account account) { - accounts.add(account); + if (account.getAccountKey() == null) + throw new NullPointerException(); + if (accounts.get(account.getAccountKey()) != null) + throw new IllegalArgumentException("Accounts map contain this key"); + accounts.put(account.getAccountKey(), account); } @Override public Account getAccount(long id) { - return accounts.stream() - .filter(account -> account.getAccountKey() == AccountKey.valueOf(id)) - .findAny() - .orElse(null); + return accounts.getOrDefault(AccountKey.valueOf(id), null); } @Override diff --git a/src/test/java/com/devexperts/service/AccountServiceImplTest.java b/src/test/java/com/devexperts/service/AccountServiceImplTest.java new file mode 100644 index 0000000..9570b26 --- /dev/null +++ b/src/test/java/com/devexperts/service/AccountServiceImplTest.java @@ -0,0 +1,71 @@ +package com.devexperts.service; + +import com.devexperts.account.Account; +import com.devexperts.account.AccountKey; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AccountServiceImplTest { + + private Account account; + private AccountServiceImpl service = new AccountServiceImpl(); + + @BeforeEach + void setUp() { + service = new AccountServiceImpl(); + AccountKey key = AccountKey.valueOf(100); + account = new Account(key, "name", "lastname", 0d); + } + + @Test + void test_NewAccountCreation() { + service.createAccount(account); + Account actual = service.getAccount(account.getAccountKey().getAccountId()); + + assertEquals(account.getAccountKey(), actual.getAccountKey()); + assertEquals(account, actual); + } + + @Test + void test_CreateOneKeyAccounts() { + service.createAccount(account); + + Account newAccount = account; + assertThrows( + IllegalArgumentException.class, () -> service.createAccount(newAccount) + ); + } + + @Test + void test_CreateNullKeyAccounts() { + account = new Account(null, "name", "lastname", 0d); + assertThrows( + NullPointerException.class, () -> service.createAccount(account) + ); + } + + @Test + void test_CreateAccounts() { + AccountKey keyOne = AccountKey.valueOf(1); + account = new Account(keyOne, "name1", "lastname1", 0d); + service.createAccount(account); + + AccountKey keyTwo = AccountKey.valueOf(2); + account = new Account(keyTwo, "name2", "lastname2", 0d); + service.createAccount(account); + + assertNotEquals(service.getAccount(keyOne.getAccountId()), service.getAccount(keyTwo.getAccountId())); + } + + @Test + void test_ClearAccountsMap() { + service.createAccount(account); + Account actual = service.getAccount(account.getAccountKey().getAccountId()); + assertNotNull(actual); + service.clear(); + actual = service.getAccount(account.getAccountKey().getAccountId()); + assertNull(actual); + } +} \ No newline at end of file From 14544e7ebf09b44a8b00165d10821ef0cfa1ffa9 Mon Sep 17 00:00:00 2001 From: birewallg Date: Tue, 17 Aug 2021 20:42:00 +0300 Subject: [PATCH 2/5] Task 2: add transfer function --- .../service/AccountServiceImpl.java | 11 ++++- .../service/AccountServiceImplTest.java | 42 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java index 6b85c9d..d2caf9f 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -32,6 +32,15 @@ public Account getAccount(long id) { @Override public void transfer(Account source, Account target, double amount) { - //do nothing for now + if (amount < 0 || source == target) { + throw new IllegalArgumentException(); + } + double sourceBalance = source.getBalance(); + double targetBalance = target.getBalance(); + if (sourceBalance < amount) + throw new IllegalArgumentException(); + + source.setBalance(sourceBalance - amount); + target.setBalance(targetBalance + amount); } } diff --git a/src/test/java/com/devexperts/service/AccountServiceImplTest.java b/src/test/java/com/devexperts/service/AccountServiceImplTest.java index 9570b26..7e139db 100644 --- a/src/test/java/com/devexperts/service/AccountServiceImplTest.java +++ b/src/test/java/com/devexperts/service/AccountServiceImplTest.java @@ -19,6 +19,48 @@ void setUp() { account = new Account(key, "name", "lastname", 0d); } + @Test + void test_TransferTest() { + AccountKey keyOne = AccountKey.valueOf(1); + account = new Account(keyOne, "name1", "lastname1", 100.0); + service.createAccount(account); + + AccountKey keyTwo = AccountKey.valueOf(2); + account = new Account(keyTwo, "name2", "lastname2", 100.0); + service.createAccount(account); + + double balanceOne = service.getAccount(keyOne.getAccountId()).getBalance(); + double balanceTwo = service.getAccount(keyTwo.getAccountId()).getBalance(); + assertEquals(balanceOne, balanceTwo); + + service.transfer( + service.getAccount(keyOne.getAccountId()), + service.getAccount(keyTwo.getAccountId()), + 50.0); + + balanceOne = service.getAccount(keyOne.getAccountId()).getBalance(); + balanceTwo = service.getAccount(keyTwo.getAccountId()).getBalance(); + + assertNotEquals(balanceOne, balanceTwo); + } + @Test + void test_TransferTestTrouble() { + AccountKey keyOne = AccountKey.valueOf(1); + account = new Account(keyOne, "name1", "lastname1", -100.0); + service.createAccount(account); + + AccountKey keyTwo = AccountKey.valueOf(2); + account = new Account(keyTwo, "name2", "lastname2", 100.0); + service.createAccount(account); + + assertThrows( + IllegalArgumentException.class, () -> service.transfer( + service.getAccount(keyOne.getAccountId()), + service.getAccount(keyTwo.getAccountId()), + 50.0) + ); + } + @Test void test_NewAccountCreation() { service.createAccount(account); From 1bdbabe899d3d94c69dcfc7969d700308509bff7 Mon Sep 17 00:00:00 2001 From: birewallg Date: Tue, 17 Aug 2021 22:18:06 +0300 Subject: [PATCH 3/5] Task 3: add multithread transactions --- .../service/AccountServiceImpl.java | 20 ++++++++--- .../AccountServiceImplMultithreadTest.java | 34 +++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/devexperts/service/AccountServiceImplMultithreadTest.java diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java index d2caf9f..cd6227f 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -4,12 +4,14 @@ import com.devexperts.account.AccountKey; import org.springframework.stereotype.Service; -import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; @Service public class AccountServiceImpl implements AccountService { - private final HashMap accounts = new HashMap<>(); + private final Map accounts = new ConcurrentHashMap<>(); @Override public void clear() { @@ -30,11 +32,16 @@ public Account getAccount(long id) { return accounts.getOrDefault(AccountKey.valueOf(id), null); } + // lock flag + private final ReentrantLock lock = new ReentrantLock(); + @Override public void transfer(Account source, Account target, double amount) { - if (amount < 0 || source == target) { - throw new IllegalArgumentException(); - } + if (amount < 0 || source == target) { + throw new IllegalArgumentException(); + } + lock.lock(); + try { double sourceBalance = source.getBalance(); double targetBalance = target.getBalance(); if (sourceBalance < amount) @@ -42,5 +49,8 @@ public void transfer(Account source, Account target, double amount) { source.setBalance(sourceBalance - amount); target.setBalance(targetBalance + amount); + } finally { + lock.unlock(); + } } } diff --git a/src/test/java/com/devexperts/service/AccountServiceImplMultithreadTest.java b/src/test/java/com/devexperts/service/AccountServiceImplMultithreadTest.java new file mode 100644 index 0000000..901cfca --- /dev/null +++ b/src/test/java/com/devexperts/service/AccountServiceImplMultithreadTest.java @@ -0,0 +1,34 @@ +package com.devexperts.service; + +import com.devexperts.account.Account; +import com.devexperts.account.AccountKey; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AccountServiceImplMultithreadTest { + @Test + void test_MultithreadAccountsTransaction() throws InterruptedException { + AccountServiceImpl service = new AccountServiceImpl(); + + AccountKey key = AccountKey.valueOf(1); + Account source = new Account(key, "name", "lastname", 100.0); + key = AccountKey.valueOf(2); + Account target = new Account(key, "name", "lastname", 100.0); + + CountDownLatch latch = new CountDownLatch(10); + for (int i = 0; i < 10; i++) { + new Thread(()->{ + service.transfer(source, target, 10.0); + latch.countDown(); + }).start(); + } + latch.await(1, TimeUnit.SECONDS); + + assertEquals(0.0, source.getBalance()); + assertEquals(200.0, target.getBalance()); + } +} From 04ef5a56383262ef4a3083a61f04ef5ceff97308 Mon Sep 17 00:00:00 2001 From: birewallg Date: Wed, 18 Aug 2021 14:26:45 +0300 Subject: [PATCH 4/5] Task 4: create rest service for transfer method --- .../rest/AbstractAccountController.java | 2 +- .../devexperts/rest/AccountController.java | 38 +++++++++++++++++-- .../service/AccountServiceImpl.java | 2 +- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/devexperts/rest/AbstractAccountController.java b/src/main/java/com/devexperts/rest/AbstractAccountController.java index dea5a3c..d02dc8a 100644 --- a/src/main/java/com/devexperts/rest/AbstractAccountController.java +++ b/src/main/java/com/devexperts/rest/AbstractAccountController.java @@ -3,5 +3,5 @@ import org.springframework.http.ResponseEntity; public abstract class AbstractAccountController { - abstract ResponseEntity transfer(long sourceId, long targetId, double amount); + abstract ResponseEntity transfer(Long sourceId, Long targetId, Double amount); } diff --git a/src/main/java/com/devexperts/rest/AccountController.java b/src/main/java/com/devexperts/rest/AccountController.java index b300282..6c72343 100644 --- a/src/main/java/com/devexperts/rest/AccountController.java +++ b/src/main/java/com/devexperts/rest/AccountController.java @@ -1,14 +1,44 @@ package com.devexperts.rest; +import com.devexperts.account.Account; +import com.devexperts.service.AccountServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; 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.*; @RestController @RequestMapping("/api") public class AccountController extends AbstractAccountController { + private final AccountServiceImpl service; + @Autowired + public AccountController(AccountServiceImpl service) { + this.service = service; + } + + @PostMapping("/operations/transfer") + public ResponseEntity transfer( + @RequestParam(value = "sourceId") Long sourceId, + @RequestParam(value = "targetId") Long targetId, + @RequestParam(value = "amount") Double amount) { + if(sourceId == null || targetId == null || amount == null) + return new ResponseEntity<>( + "One of the parameters in not present or amount is invalid", + HttpStatus.BAD_REQUEST); + + Account source = service.getAccount(sourceId); + Account target = service.getAccount(targetId); + if (source == null || target == null) + return new ResponseEntity<>( + "Account is not found", HttpStatus.NOT_FOUND); - public ResponseEntity transfer(long sourceId, long targetId, double amount) { - return null; + try { + service.transfer(source, target, amount); + return new ResponseEntity<>( + "Successful transfer", HttpStatus.OK); + } catch (IllegalStateException e) { + return new ResponseEntity<>( + "Insufficient account balance", HttpStatus.INTERNAL_SERVER_ERROR); + } } } diff --git a/src/main/java/com/devexperts/service/AccountServiceImpl.java b/src/main/java/com/devexperts/service/AccountServiceImpl.java index cd6227f..9c08166 100644 --- a/src/main/java/com/devexperts/service/AccountServiceImpl.java +++ b/src/main/java/com/devexperts/service/AccountServiceImpl.java @@ -45,7 +45,7 @@ public void transfer(Account source, Account target, double amount) { double sourceBalance = source.getBalance(); double targetBalance = target.getBalance(); if (sourceBalance < amount) - throw new IllegalArgumentException(); + throw new IllegalStateException(); source.setBalance(sourceBalance - amount); target.setBalance(targetBalance + amount); From 350dbe2028390f22d3cf88b007b9dafb45753b2d Mon Sep 17 00:00:00 2001 From: birewallg Date: Wed, 18 Aug 2021 17:02:40 +0300 Subject: [PATCH 5/5] Task 5: create 2 tables for accounts and corresponding transfers --- src/main/resources/sql/data/accounts.sql | 7 +++++++ src/main/resources/sql/data/transfers.sql | 13 +++++++++++++ src/main/resources/sql/select.sql | 3 +++ 3 files changed, 23 insertions(+) create mode 100644 src/main/resources/sql/data/accounts.sql create mode 100644 src/main/resources/sql/data/transfers.sql create mode 100644 src/main/resources/sql/select.sql diff --git a/src/main/resources/sql/data/accounts.sql b/src/main/resources/sql/data/accounts.sql new file mode 100644 index 0000000..44af79d --- /dev/null +++ b/src/main/resources/sql/data/accounts.sql @@ -0,0 +1,7 @@ +CREATE TABLE "accounts" ( + "id" INTEGER NOT NULL, + "first_name" VARCHAR(50) NULL DEFAULT NULL, + "last_name" VARCHAR(50) NULL DEFAULT NULL, + "balance" INTEGER NULL DEFAULT NULL, + PRIMARY KEY ("id") +); \ No newline at end of file diff --git a/src/main/resources/sql/data/transfers.sql b/src/main/resources/sql/data/transfers.sql new file mode 100644 index 0000000..de440e3 --- /dev/null +++ b/src/main/resources/sql/data/transfers.sql @@ -0,0 +1,13 @@ +CREATE TABLE "transfers" ( + "id" INTEGER NOT NULL, + "source_id" INTEGER, + "target_id" INTEGER, + "amount" INTEGER, + "transfer_time" TIMESTAMP, + PRIMARY KEY ("id"), + FOREIGN KEY ("source_id") + REFERENCES "accounts" ("id"), + FOREIGN KEY ("target_id") + REFERENCES "accounts" ("id") +) +; \ No newline at end of file diff --git a/src/main/resources/sql/select.sql b/src/main/resources/sql/select.sql new file mode 100644 index 0000000..a279603 --- /dev/null +++ b/src/main/resources/sql/select.sql @@ -0,0 +1,3 @@ +SELECT transfers.source_id FROM transfers +WHERE (transfers.transfer_time >= TIMESTAMP '2019-01-01') +GROUP BY transfers.source_id HAVING SUM(transfers.amount) > 1000;