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

Ivanov Sergey interview #44

Open
wants to merge 8 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
17 changes: 17 additions & 0 deletions src/main/java/com/devexperts/account/Account.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.devexperts.account;

import java.util.Objects;

public class Account {
private final AccountKey accountKey;
private final String firstName;
Expand Down Expand Up @@ -32,4 +34,19 @@ public Double getBalance() {
public void setBalance(Double balance) {
this.balance = balance;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Account account = (Account) o;
return Objects.equals(accountKey, account.accountKey) &&
Objects.equals(firstName, account.firstName) &&
Objects.equals(lastName, account.lastName);
}

@Override
public int hashCode() {
return Objects.hash(accountKey, firstName, lastName);
}
}
15 changes: 15 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,17 @@ private AccountKey(long accountId) {
public static AccountKey valueOf(long accountId) {
return new AccountKey(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 );
}
}
71 changes: 67 additions & 4 deletions src/main/java/com/devexperts/rest/AccountController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,77 @@
package com.devexperts.rest;

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.rest.data.ResponseMessage;
import com.devexperts.service.AccountService;
import com.devexperts.service.exceptions.NegativeBalanceException;
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.*;

@RestController
@RequestMapping("/api")
public class AccountController extends AbstractAccountController {

public ResponseEntity<Void> transfer(long sourceId, long targetId, double amount) {
return null;
private AccountService accountService;

@Autowired
public void setAccountService( AccountService accountService ) {
this.accountService = accountService;
}

//For test
@Deprecated
@GetMapping("test/init")
public ResponseEntity<Void> testInit() {
Account account1 = new Account( AccountKey.valueOf( 1 ), "Sergey", "Ivanov", 12d );
Account account2 = new Account( AccountKey.valueOf( 2 ), "Vika", "Okulist", 125.0 );
Account account3 = new Account( AccountKey.valueOf( 3 ), "Nikoly", "Frolov", 0.3d );
accountService.createAccount( account1 );
accountService.createAccount( account2 );
accountService.createAccount( account3 );

return ResponseEntity.ok().build();
}

@PostMapping("operations/transfer")
public ResponseEntity<Void> transfer(
@RequestParam("source_id") long sourceId,
@RequestParam("target_id") long targetId,
@RequestParam("amount") double amount
) {
Account source = accountService.getAccount( sourceId );
Account target = accountService.getAccount( targetId );

if ( source == null || target == null ) {
throw new NullPointerException ("account is not found");
}

accountService.transfer( source, target, amount );
return ResponseEntity.ok().build();
}

@ExceptionHandler({IllegalArgumentException.class})
public ResponseEntity<ResponseMessage> handleIllegalArgumentException( IllegalArgumentException ex ) {
return ResponseEntity.badRequest().body(
new ResponseMessage( ex.getMessage(),
400, "BAD_REQUEST", "/operations/transfer")
);
}

@ExceptionHandler({NegativeBalanceException.class})
public ResponseEntity<ResponseMessage> handleNegativeBalanceException( NegativeBalanceException ex ) {
return ResponseEntity.status( 500 ).body(
new ResponseMessage( ex.getMessage(),
500, "INTERNAL_SERVER_ERROR", "/operations/transfer")
);
}

@ExceptionHandler({NullPointerException.class})
public ResponseEntity<ResponseMessage> handleNullPointerException( NullPointerException ex ) {
return ResponseEntity.status( 404 ).body(
new ResponseMessage( ex.getMessage(),
404, "NOT_FOUND", "/operations/transfer")
);
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/devexperts/rest/data/ResponseMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.devexperts.rest.data;

import java.text.SimpleDateFormat;
import java.util.Date;

public class ResponseMessage {
private final String message;
private final int status;
private final String error;
private final Date date;
private final String path;

public ResponseMessage( String message, int status, String error, String path ) {
this.message = message;
this.status = status;
this.date = new Date(System.currentTimeMillis());;
this.error = error;
this.path = path;
}

public String getMessage() {
return message;
}

public int getStatus() {
return status;
}

public String getError() {
return error;
}

public String getDate() {
SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss z");
return formatter.format( date );
}

public String getPath() {
return path;
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/devexperts/service/AccountService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public interface AccountService {

/**
* Creates a new account
* Todo update???
* Todo if null???
*
* @param account account entity to add or update
* @throws IllegalArgumentException if account is already present
Expand Down
65 changes: 53 additions & 12 deletions src/main/java/com/devexperts/service/AccountServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,77 @@

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.service.exceptions.NegativeBalanceException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;

@Service
public class AccountServiceImpl implements AccountService {

private final List<Account> accounts = new ArrayList<>();
//Changed the collection, because the speed of searching for an element in HashMap is constant.
private final Map<AccountKey, Account> accounts = new HashMap<>();

@Override
public void clear() {
accounts.clear();
}

/**
* @throws NullPointerException if account null
*/
@Override
public void createAccount(Account account) {
accounts.add(account);
public void createAccount( Account account ) {
if ( account == null ) {
throw new NullPointerException( "account is null" );
}

if ( accounts.containsKey( account.getAccountKey() ) ) {
throw new IllegalArgumentException( "account is already present" );
}

accounts.put( account.getAccountKey(), account );
}

@Override
public Account getAccount(long id) {
return accounts.stream()
.filter(account -> account.getAccountKey() == AccountKey.valueOf(id))
.findAny()
.orElse(null);
public Account getAccount( long id ) {
return accounts.get( AccountKey.valueOf( id ) );
}

@Override
public void transfer(Account source, Account target, double amount) {
//do nothing for now
public void transfer( Account source, Account target, double amount ) {
checkArgumentForTransfer( source, target, amount );
transferAmount( source, target, amount );
}

protected void checkArgumentForTransfer( Account source, Account target, double amount ) {
if ( amount <= 0 ) {
throw new IllegalArgumentException( "amount is negative or zero" );
}

if ( source == null ) {
throw new IllegalArgumentException( "source is null" );
}

if ( target == null ) {
throw new IllegalArgumentException( "target is null" );
}

if ( source.getAccountKey().equals( target.getAccountKey() ) ) {
throw new IllegalArgumentException( "source and target is one account" );
}
}

private void transferAmount( Account source, Account target, double amount ) {
if ( source.getBalance() - amount < 0 ) {
throw new NegativeBalanceException( "source has insufficient funds on his account" );
}
source.setBalance( source.getBalance() - amount );
target.setBalance( target.getBalance() + amount );
}

protected Map<AccountKey, Account> getAccounts() {
return accounts;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.devexperts.service;

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.service.exceptions.NegativeBalanceException;

public class AccountTransferParallelService extends AccountServiceImpl {

private Account getAccount( AccountKey accountKey ) {
return getAccounts().get( accountKey );
}

@Override
public void transfer( Account source, Account target, double amount ) {
checkArgumentForTransfer( source, target, amount );

//get from the cache (to be sure)
Account sourceCash = getAccount( source.getAccountKey() );
Account targetCash = getAccount( target.getAccountKey() );

if (sourceCash == null) {
throw new IllegalArgumentException( "source-account is not found" );
}

if (targetCash == null) {
throw new IllegalArgumentException( "target-account is not found" );
}

//used an optimistic approach to blocking resources
synchronized ( sourceCash ) {
if ( sourceCash.getBalance() - amount < 0 ) {
throw new NegativeBalanceException( "source has insufficient funds on his account" );
}

source.setBalance( source.getBalance() - amount );
}

synchronized ( targetCash ) {
target.setBalance( target.getBalance() + amount );
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.devexperts.service.exceptions;

public class NegativeBalanceException extends IllegalArgumentException {

public NegativeBalanceException() {
super();
}

public NegativeBalanceException(String s) {
super(s);
}

public NegativeBalanceException(String message, Throwable cause) {
super(message, cause);
}

public NegativeBalanceException(Throwable cause) {
super(cause);
}
}
11 changes: 11 additions & 0 deletions src/main/resources/sql/data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
INSERT INTO ACCOUNTS (FIRST_NAME, LAST_NAME, BALANCE)
VALUES ('Сергей', 'Иванов', 170000.34),
('Николай', 'Гумилёв', 20000),
('Дарт', 'Вейдер', 7000000.07),
('Сергей', 'Иванов', 4500000);

INSERT INTO TRANSFERS (SOURCE_ID, TARGET_ID, AMOUNT, TRANSFER_TIME)
VALUES (1, 3, 999, '2019-02-23'),
(1, 4, 2000, '2020-07-04'),
(1, 2, 760, '208-01-08'),
(4, 3, 10000, '2020-07-25')
12 changes: 12 additions & 0 deletions src/main/resources/sql/data/accounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
DROP TABLE IF EXISTS ACCOUNTS;

CREATE TABLE ACCOUNTS
(
ID LONG NOT NULL AUTO_INCREMENT,
FIRST_NAME VARCHAR(100) NOT NULL,
LAST_NAME VARCHAR(100) NOT NULL,
BALANCE DOUBLE,
CONSTRAINT ACCOUNTS_PK
PRIMARY KEY (ID)
);

12 changes: 12 additions & 0 deletions src/main/resources/sql/data/transfers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
DROP TABLE IF EXISTS TRANSFERS;

CREATE TABLE TRANSFERS
(
ID LONG NOT NULL AUTO_INCREMENT,
SOURCE_ID LONG NOT NULL,
TARGET_ID LONG NOT NULL,
AMOUNT DOUBLE NOT NULL,
TRANSFER_TIME TIMESTAMP NOT NULL,
CONSTRAINT TRANSFERS_PK
PRIMARY KEY (ID)
);
10 changes: 10 additions & 0 deletions src/main/resources/sql/select.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
SELECT SOURCE_ID
FROM TRANSFERS t
JOIN ACCOUNTS a_source
ON t.SOURCE_ID = a_source.ID
JOIN ACCOUNTS a_target
ON t.TARGET_ID = a_target.ID
WHERE (a_source.FIRST_NAME <> a_target.FIRST_NAME OR a_source.LAST_NAME <> a_target.LAST_NAME)
AND TRANSFER_TIME >= '2019-01-01'
GROUP BY SOURCE_ID
HAVING SUM(AMOUNT) > 1000
Loading