Skip to content
This repository has been archived by the owner on Jan 17, 2022. It is now read-only.

Vasiliev Denis #57

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/main/java/com/devexperts/account/AccountKey.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.devexperts.account;

import java.util.Objects;

/**
* Unique Account identifier
*
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
import org.springframework.http.ResponseEntity;

public abstract class AbstractAccountController {
abstract ResponseEntity<Void> transfer(long sourceId, long targetId, double amount);
abstract ResponseEntity<String> transfer(Long sourceId, Long targetId, Double amount);
}
38 changes: 34 additions & 4 deletions src/main/java/com/devexperts/rest/AccountController.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<Void> 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);
}
}
}
37 changes: 28 additions & 9 deletions src/main/java/com/devexperts/service/AccountServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import com.devexperts.account.AccountKey;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

@Service
public class AccountServiceImpl implements AccountService {

private final List<Account> accounts = new ArrayList<>();
private final Map<AccountKey, Account> accounts = new ConcurrentHashMap<>();

@Override
public void clear() {
Expand All @@ -19,19 +20,37 @@ 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);
}

// lock flag
private final ReentrantLock lock = new ReentrantLock();

@Override
public void transfer(Account source, Account target, double amount) {
//do nothing for now
if (amount < 0 || source == target) {
throw new IllegalArgumentException();
}
lock.lock();
try {
double sourceBalance = source.getBalance();
double targetBalance = target.getBalance();
if (sourceBalance < amount)
throw new IllegalStateException();

source.setBalance(sourceBalance - amount);
target.setBalance(targetBalance + amount);
} finally {
lock.unlock();
}
}
}
7 changes: 7 additions & 0 deletions src/main/resources/sql/data/accounts.sql
Original file line number Diff line number Diff line change
@@ -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")
);
13 changes: 13 additions & 0 deletions src/main/resources/sql/data/transfers.sql
Original file line number Diff line number Diff line change
@@ -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")
)
;
3 changes: 3 additions & 0 deletions src/main/resources/sql/select.sql
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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());
}
}
113 changes: 113 additions & 0 deletions src/test/java/com/devexperts/service/AccountServiceImplTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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_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);
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);
}
}