diff --git a/http/PaymentSystem/.gitignore b/http/PaymentSystem/.gitignore new file mode 100644 index 0000000..ce8e2ae --- /dev/null +++ b/http/PaymentSystem/.gitignore @@ -0,0 +1,109 @@ + +# Created by https://www.gitignore.io/api/java,intellij +# Edit at https://www.gitignore.io/?templates=java,intellij + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +*.iml + User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +#auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + + +modules.xml +.idea/misc.xml +*.ipr +*.iml +# Sonarlint plugin +.idea/sonarlint + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# End of https://www.gitignore.io/api/java,intellij \ No newline at end of file diff --git a/http/PaymentSystem/PaymentSystem.iml b/http/PaymentSystem/PaymentSystem.iml new file mode 100644 index 0000000..78b2cc5 --- /dev/null +++ b/http/PaymentSystem/PaymentSystem.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/http/PaymentSystem/pom.xml b/http/PaymentSystem/pom.xml new file mode 100644 index 0000000..2343c03 --- /dev/null +++ b/http/PaymentSystem/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + payment-system + payment-system + 1.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + org.slf4j + slf4j-api + 1.7.26 + + + commons-io + commons-io + 2.5 + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.8 + + + com.fasterxml.jackson.core + jackson-databind + 2.9.8 + + + \ No newline at end of file diff --git a/http/PaymentSystem/readme.md b/http/PaymentSystem/readme.md new file mode 100644 index 0000000..1221190 --- /dev/null +++ b/http/PaymentSystem/readme.md @@ -0,0 +1,135 @@ +# Запросы +### Добавить пользователя +POST /user\ +input: +```json +{ + "login": "alol", + "password": "123" +} +``` +output: +```json +{ + "status": "OK", + "description": "description" +} +``` + + +### Выдать все кошельки для всех пользователей +GET /users\ +output: - массив логинов +```json +["user1", "user2", "user3"] +``` + +### Перевод от одного пользователя к другому +POST /send\ +input: +```json +{ + "fromLogin":"loginFrom", + "fromPassword":"passwordTo", + "toLogin":"loginTo", + "count":20 +} +``` + +output: +```json +{ + "status": "OK", + "description": "description" +} +``` + + +### Запросить перевод +POST /send +input: +```json +{ + "fromLogin":"loginFrom", + "fromPassword":"passwordTo", + "toLogin":"loginTo", + "count":20 +} +``` + +output: + ```json + { + "status": "OK", + "description": "", + "entityId": "p626u7copf2tha8rc3yz" +} +``` + +### Реакция на запрос перевода перевода +POST /accept +input +```json +{ + "login":"login", + "password":"123", + "key":"qe2bmngpw41niaa6junt", + "actionType":"ACCEPT" +} +``` +output: +```json +{ + "status": "OK", + "description": "" +} +``` + +### Проверить состояние кошелька +POST /wallet +input: +```json +{ + "login": "alol", + "password": "123" +} +``` +output: +```json +{ + "login": "olya2", + "count": 88 +} +``` + +### Просмотреть все запросы +POST /request +input: +```json +{ + "login": "alol", + "password": "123" +} +``` +output: +```json +[ + { + "from": "fromLogin", + "key": "ulrrp3ebqfq0nsrjdaz1", + "count": 30 + }, + { + "from": "user2", + "key": "5sqwz36u0gtf0v91mxfx", + "count": 20 + } +] +``` + + + + + + + diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ClientHandler.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ClientHandler.java new file mode 100644 index 0000000..38a9012 --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ClientHandler.java @@ -0,0 +1,156 @@ +package ru.hse.alyokhina.server; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import ru.hse.alyokhina.server.dto.*; +import ru.hse.alyokhina.server.repository.PaymentSystemRepository; +import ru.hse.alyokhina.server.exception.NotAuthorizedException; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.Socket; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + + +import static ru.hse.alyokhina.server.ReadWriteHelper.readBody; +import static ru.hse.alyokhina.server.ReadWriteHelper.writeResponse; +import static ru.hse.alyokhina.server.ReadWriteHelper.ContentType; +import static ru.hse.alyokhina.server.ReadWriteHelper.readHeaders; +import static ru.hse.alyokhina.server.ReadWriteHelper.readPath; + + +public class ClientHandler implements Runnable { + private final Socket client; + private final ObjectMapper _J = new ObjectMapper(); + private final PaymentSystemRepository paymentSystem = new PaymentSystemRepository(); + + public ClientHandler(@Nonnull Socket client) { + this.client = client; + } + + public void run() { + try { + final BufferedReader inReader = new BufferedReader( + new InputStreamReader(client.getInputStream(), "UTF-8")); + final BufferedWriter outWriter = new BufferedWriter( + new OutputStreamWriter(client.getOutputStream(), "UTF-8")); + String response; + String status; + ContentType contentType; + try { + final String path = readPath(inReader); + final Map headers = readHeaders(inReader); + final String body = readBody(inReader, headers); + System.out.println(path); + System.out.println(headers); + System.out.println(body); + response = apply(path, body); + status = "200"; + contentType = ContentType.JSON; + } catch (IllegalArgumentException e) { + status = "400"; + response = e.getMessage(); + contentType = ContentType.TEXT; + } catch (NotAuthorizedException e) { + status = "401"; + response = e.getMessage(); + contentType = ContentType.TEXT; + } catch (ClassNotFoundException e) { + status = "404"; + response = e.getMessage(); + contentType = ContentType.TEXT; + } catch (Throwable e) { + status = "500"; + response = e.getMessage(); + contentType = ContentType.TEXT; + } + writeResponse(status, response, contentType, outWriter); + client.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + + private String apply(@Nonnull final String path, @Nonnull final String body) throws JsonProcessingException + , ClassNotFoundException + , NotAuthorizedException { + final String pathParts[] = path.split(" "); + if (pathParts.length < 2) { + throw new IllegalArgumentException("Failed to parse http path"); + } + if (pathParts[0].toLowerCase().equals("post") + && pathParts[1].equals("/user")) { + UserInfo userInfo; + try { + userInfo = _J.readValue(body, UserInfo.class); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to parse body as UserInfo", e); + } + final DefaultResponse response = paymentSystem.createUser(userInfo); + return _J.writeValueAsString(response); + } else if (pathParts[0].toLowerCase().equals("get") + && pathParts[1].equals("/users")) { + return _J.writeValueAsString(paymentSystem.getUsers()); + } else if (pathParts[0].toLowerCase().equals("post") + && pathParts[1].equals("/send")) { + TransferRequest transferRequest; + try { + transferRequest = _J.readValue(body, TransferRequest.class); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to parse body as TransferRequest", e); + } + final DefaultResponse response = paymentSystem.sendTransfer(transferRequest); + return _J.writeValueAsString(response); + } else if (pathParts[0].toLowerCase().equals("post") + && pathParts[1].equals("/request")) { + TransferRequest transferRequest; + try { + transferRequest = _J.readValue(body, TransferRequest.class); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to parse body as TransferRequest", e); + } + final DefaultResponse response = paymentSystem.requestTransfer(transferRequest); + return _J.writeValueAsString(response); + } else if (pathParts[0].toLowerCase().equals("post") + && pathParts[1].equals("/accept")) { + TransferRequestAccept transferRequestAccept; + try { + transferRequestAccept = _J.readValue(body, TransferRequestAccept.class); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to parse body as TransferRequestAccept", e); + } + final DefaultResponse response = paymentSystem.acceptRequest(transferRequestAccept); + return _J.writeValueAsString(response); + } else if (pathParts[0].toLowerCase().equals("post") + && pathParts[1].equals("/wallet")) { + UserInfo userInfo; + try { + userInfo = _J.readValue(body, UserInfo.class); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to parse body as UserInfo", e); + } + final WalletInfo response = paymentSystem.getCountMoney(userInfo); + return _J.writeValueAsString(response); + } else if (pathParts[0].toLowerCase().equals("post") + && pathParts[1].equals("/requests")) { + UserInfo userInfo; + try { + userInfo = _J.readValue(body, UserInfo.class); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to parse body as UserInfo", e); + } + final List response = paymentSystem.getRequests(userInfo); + return _J.writeValueAsString(response); + } + + throw new ClassNotFoundException(pathParts[0] + " " + pathParts[1] + " not found"); + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Main.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Main.java new file mode 100644 index 0000000..f7813da --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Main.java @@ -0,0 +1,10 @@ +package ru.hse.alyokhina.server; + +import java.io.IOException; + +public class Main { + public static void main(String[] args) throws IOException { + final Server server = new Server(8000); + server.start(); + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ReadWriteHelper.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ReadWriteHelper.java new file mode 100644 index 0000000..b28f920 --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ReadWriteHelper.java @@ -0,0 +1,77 @@ +package ru.hse.alyokhina.server; + +import javax.annotation.Nonnull; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +public class ReadWriteHelper { + public enum ContentType {JSON, TEXT} + + public static void writeResponse(@Nonnull final String statusCode, + @Nonnull final String body, + @Nonnull final ContentType contentType, + @Nonnull final BufferedWriter writer) throws IOException, NoSuchElementException { + writer.write("HTTP/1.1 " + statusCode + "\n"); + writer.write("Content-Type: " + convertContentType(contentType) + "\n" + + "Content-Length: " + body.length() + "\n"); + writer.write("\n"); + writer.write(body); + writer.flush(); + } + + @Nonnull + public static String convertContentType(@Nonnull final ContentType contentType) throws NoSuchElementException { + switch (contentType) { + case JSON: + return "application/json"; + case TEXT: + return "text/plain"; + default: + throw new NoSuchElementException("Failed to convert " + contentType); + } + } + + public static String readPath(@Nonnull final BufferedReader in) throws IOException { + return in.readLine(); + } + + @Nonnull + public static Map readHeaders(@Nonnull final BufferedReader in) throws IOException, IllegalArgumentException { + final Map headers = new HashMap(); + while (true) { + final String curLine = in.readLine(); + if ("".equals(curLine)) { + break; + } + final String[] keyAndValues = curLine.split(": "); + if (keyAndValues.length != 2) { + throw new IllegalArgumentException("Failed to parse http headers"); + } + headers.put(keyAndValues[0].toLowerCase(), keyAndValues[1]); + } + return headers; + } + + @Nonnull + public static String readBody(@Nonnull final BufferedReader in, @Nonnull final Map headers) throws IOException, + IllegalArgumentException { + if (headers.get("content-length") == null) { + return ""; + } + try { + final int contentLength = Integer.parseInt(headers.get("content-length")); + final char[] buffer = new char[contentLength]; + final int countRead = in.read(buffer); + if (countRead != contentLength) { + throw new IllegalArgumentException("Error during reading body, content-length = " + contentLength); + } + return new String(buffer); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Failed to parse content-length ", e); + } + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Server.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Server.java new file mode 100644 index 0000000..c7e6d10 --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Server.java @@ -0,0 +1,28 @@ +package ru.hse.alyokhina.server; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +public class Server { + private final int port; + private final ServerSocket serverSocket; + + public Server(int port) throws IOException { + this.port = port; + this.serverSocket = new ServerSocket(port); + } + + public void start() throws IOException { + while (true) { + final Socket client = serverSocket.accept(); + final Thread clientThread = new Thread(new ClientHandler(client)); + clientThread.start(); + } + + } + + public int getPort() { + return port; + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/ActionType.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/ActionType.java new file mode 100644 index 0000000..6afff8e --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/ActionType.java @@ -0,0 +1,5 @@ +package ru.hse.alyokhina.server.dto; + +public enum ActionType { + ACCEPT, CANCEL +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/DefaultResponse.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/DefaultResponse.java new file mode 100644 index 0000000..ce600dc --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/DefaultResponse.java @@ -0,0 +1,35 @@ +package ru.hse.alyokhina.server.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nonnull; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DefaultResponse { + private final Status status; + private final String description; + private final String entityId; + + @JsonCreator + public DefaultResponse(@JsonProperty("status") @Nonnull final Status status, + @JsonProperty("description") final String description, + @JsonProperty("entityId") final String entityId) { + this.status = status; + this.description = description; + this.entityId = entityId; + } + + public Status getStatus() { + return status; + } + + public String getDescription() { + return description; + } + + public String getEntityId() { + return entityId; + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/RequestInfo.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/RequestInfo.java new file mode 100644 index 0000000..18bb856 --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/RequestInfo.java @@ -0,0 +1,33 @@ +package ru.hse.alyokhina.server.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nonnull; + +public class RequestInfo { + private final String from; + private final String key; + private final long count; + + @JsonCreator + public RequestInfo(@JsonProperty("from") @Nonnull final String from, + @JsonProperty("key") @Nonnull final String key, + @JsonProperty("count") final long count) { + this.from = from; + this.key = key; + this.count = count; + } + + public String getFrom() { + return from; + } + + public String getKey() { + return key; + } + + public long getCount() { + return count; + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/Status.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/Status.java new file mode 100644 index 0000000..2810e23 --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/Status.java @@ -0,0 +1,5 @@ +package ru.hse.alyokhina.server.dto; + +public enum Status { + OK, ERROR +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequest.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequest.java new file mode 100644 index 0000000..2c6d6d2 --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequest.java @@ -0,0 +1,41 @@ +package ru.hse.alyokhina.server.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nonnull; + + +public class TransferRequest { + private final String fromLogin; + private final String toLogin; + private final String fromPassword; + private final long count; + + @JsonCreator + public TransferRequest(@JsonProperty("fromLogin") @Nonnull final String fromLogin, + @JsonProperty("toLogin") @Nonnull final String toLogin, + @JsonProperty("fromPassword") @Nonnull final String fromPassword, + @JsonProperty("count") final long count) { + this.fromLogin = fromLogin; + this.toLogin = toLogin; + this.fromPassword = fromPassword; + this.count = count; + } + + public String getFromLogin() { + return fromLogin; + } + + public String getToLogin() { + return toLogin; + } + + public String getFromPassword() { + return fromPassword; + } + + public long getCount() { + return count; + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequestAccept.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequestAccept.java new file mode 100644 index 0000000..f5176eb --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequestAccept.java @@ -0,0 +1,42 @@ +package ru.hse.alyokhina.server.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nonnull; + +public class TransferRequestAccept { + + private final String login; + private final String password; + private final String key; + private final ActionType actionType; + + @JsonCreator + public TransferRequestAccept(@JsonProperty("login") @Nonnull final String login, + @JsonProperty("password") @Nonnull final String password, + @JsonProperty("key") @Nonnull final String key, + @JsonProperty("actionType") @Nonnull ActionType actionType) { + + this.login = login; + this.password = password; + this.key = key; + this.actionType = actionType; + } + + public String getLogin() { + return login; + } + + public String getPassword() { + return password; + } + + public String getKey() { + return key; + } + + public ActionType getActionType() { + return actionType; + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/UserInfo.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/UserInfo.java new file mode 100644 index 0000000..b845636 --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/UserInfo.java @@ -0,0 +1,27 @@ +package ru.hse.alyokhina.server.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nonnull; + +public class UserInfo { + private final String login; + private final String password; + + @JsonCreator + public UserInfo(@JsonProperty("login") @Nonnull final String login, + @JsonProperty("password") @Nonnull final String password) { + + this.login = login; + this.password = password; + } + + public String getLogin() { + return login; + } + + public String getPassword() { + return password; + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/WalletInfo.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/WalletInfo.java new file mode 100644 index 0000000..0c0b2f9 --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/WalletInfo.java @@ -0,0 +1,27 @@ +package ru.hse.alyokhina.server.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nonnull; + +public class WalletInfo { + private final String login; + private final Long count; + + @JsonCreator + public WalletInfo(@JsonProperty("login") @Nonnull final String login, + @JsonProperty("count") @Nonnull final Long count) { + + this.login = login; + this.count = count; + } + + public String getLogin() { + return login; + } + + public Long getCount() { + return count; + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/exception/NotAuthorizedException.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/exception/NotAuthorizedException.java new file mode 100644 index 0000000..7520f69 --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/exception/NotAuthorizedException.java @@ -0,0 +1,7 @@ +package ru.hse.alyokhina.server.exception; + +public class NotAuthorizedException extends Exception { + public NotAuthorizedException(String msg) { + super(msg); + } +} diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/repository/PaymentSystemRepository.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/repository/PaymentSystemRepository.java new file mode 100644 index 0000000..690556e --- /dev/null +++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/repository/PaymentSystemRepository.java @@ -0,0 +1,236 @@ +package ru.hse.alyokhina.server.repository; + + +import ru.hse.alyokhina.server.dto.*; +import ru.hse.alyokhina.server.exception.NotAuthorizedException; + +import javax.annotation.Nonnull; + +import java.util.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + +public class PaymentSystemRepository { + private long defaultMoneyCount = 100; + + private static ReadWriteLock readWriteLockData = new ReentrantReadWriteLock(); + private static Lock readLockData = readWriteLockData.readLock(); + private static Lock writeLockData = readWriteLockData.writeLock(); + + private static ReadWriteLock readWriteLockRequest = new ReentrantReadWriteLock(); + private static Lock readLockRequest = readWriteLockRequest.readLock(); + private static Lock writeLockRequest = readWriteLockRequest.writeLock(); + + private static Map data = new HashMap<>(); + private static Map> requests = new HashMap<>(); + + @Nonnull + + public DefaultResponse createUser(@Nonnull final UserInfo request) { + writeLockData.lock(); + if (data.get(request.getLogin()) != null) { + writeLockData.unlock(); + throw new IllegalArgumentException("This login " + request.getLogin() + " is already in use."); + } + final User newUser = new User(request.getLogin(), request.getPassword(), defaultMoneyCount); + data.put(request.getLogin(), newUser); + writeLockData.unlock(); + writeLockRequest.lock(); + requests.put(request.getLogin(), new HashMap<>()); + writeLockRequest.unlock(); + return new DefaultResponse(Status.OK, "", null); + } + + @Nonnull + public Collection getUsers() { + readLockData.lock(); + final Set users = data.keySet(); + readLockData.unlock(); + return users; + } + + @Nonnull + public DefaultResponse sendTransfer(@Nonnull final TransferRequest transferRequest) throws NotAuthorizedException { + valid(transferRequest.getFromLogin(), transferRequest.getFromPassword(), transferRequest.getToLogin()); + boolean result = transfer(transferRequest.getFromLogin(), transferRequest.getFromLogin(), transferRequest.getCount()); + return result + ? new DefaultResponse(Status.OK, "", null) + : new DefaultResponse(Status.ERROR, "not enough money to transfer", null); + } + + + @Nonnull + public DefaultResponse requestTransfer(@Nonnull final TransferRequest transferRequest) throws NotAuthorizedException { + valid(transferRequest.getFromLogin(), transferRequest.getFromPassword(), transferRequest.getToLogin()); + writeLockRequest.lock(); + final Map requestsForTo = requests.get(transferRequest.getToLogin()); + final String key = generateKey(requestsForTo); + requestsForTo.put(key, new Request(transferRequest.getFromLogin(), transferRequest.getCount())); + writeLockRequest.unlock(); + return new DefaultResponse(Status.OK, "", key); + } + + @Nonnull + public DefaultResponse acceptRequest(@Nonnull TransferRequestAccept requestAccept) throws NotAuthorizedException { + valid(requestAccept.getLogin(), requestAccept.getPassword(), null); + + writeLockRequest.lock(); + final Map requestsForUser = requests.get(requestAccept.getLogin()); + final Request request = requestsForUser.get(requestAccept.getKey()); + if (request == null) { + writeLockRequest.unlock(); + return new DefaultResponse(Status.ERROR, "Request " + requestAccept.getKey() + " not found", null); + } + if (requestAccept.getActionType() == ActionType.ACCEPT) { + boolean result = transfer(requestAccept.getLogin(), request.loginFrom, request.count); + if (!result) { + writeLockRequest.unlock(); + new DefaultResponse(Status.ERROR, "not enough money to transfer", null); + } + } + requestsForUser.remove(requestAccept.getKey()); + writeLockRequest.unlock(); + return new DefaultResponse(Status.OK, "", null); + } + + @Nonnull + public WalletInfo getCountMoney(@Nonnull final UserInfo userInfo) throws NotAuthorizedException { + valid(userInfo.getLogin(), userInfo.getPassword(), null); + readLockData.lock(); + long countMoney = data.get(userInfo.getLogin()).count; + readLockData.unlock(); + return new WalletInfo(userInfo.getLogin(), countMoney); + } + + + @Nonnull + public List getRequests(@Nonnull final UserInfo userInfo) throws NotAuthorizedException { + valid(userInfo.getLogin(), userInfo.getPassword(), null); + readLockRequest.lock(); + final Map requestsForUser = requests.get(userInfo.getLogin()); + readLockRequest.unlock(); + if (requestsForUser == null) { + return new ArrayList<>(); + } + return requestsForUser.entrySet() + .stream() + .map(entry -> new RequestInfo(entry.getValue().loginFrom, + entry.getKey(), + entry.getValue().count)) + .collect(Collectors.toList()); + + } + + private String generateKey(@Nonnull final Map map) { + Random random = new Random(); + String newKey; + while (true) { + char[] chars = "abcdefghijklmnopqrstuvwxyz1234567890".toCharArray(); + StringBuilder sb = new StringBuilder(20); + for (int i = 0; i < 20; i++) { + char c = chars[random.nextInt(chars.length)]; + sb.append(c); + } + newKey = sb.toString(); + if (map.get(newKey) == null) { + return newKey; + } + } + } + + private boolean transfer(@Nonnull final String loginFrom, + @Nonnull final String loginTo, + final long count) { + writeLockData.lock(); + final User userFrom = data.get(loginFrom); + final User userTo = data.get(loginTo); + if (userFrom.count < count) { + writeLockData.unlock(); + return false; + } + data.put(userFrom.login, new User(userFrom.login, + userFrom.password, + userFrom.count - count)); + data.put(userTo.login, new User(userTo.login, userTo.password, userTo.count + count)); + writeLockData.unlock(); + return true; + } + + private void valid(@Nonnull final String loginFrom, + @Nonnull final String password, + final String loginTo) throws NotAuthorizedException { + readLockData.lock(); + final User userFrom = data.get(loginFrom); + readLockData.unlock(); + if (userFrom == null) { + + throw new NotAuthorizedException("user " + loginFrom + " not registered"); + } + + if (loginTo != null && data.get(loginTo) == null) { + throw new NotAuthorizedException("user " + loginTo + " not registered"); + } + if (!userFrom.password.equals(password)) { + + throw new NotAuthorizedException("wrong password for " + loginFrom); + } + } + + private class User { + private final String login; + private final String password; + private final long count; + + private User(@Nonnull final String login, @Nonnull final String password, long count) { + this.login = login; + this.password = password; + this.count = count; + } + + + @Override + public int hashCode() { + return Objects.hash(login, password, count); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof User)) { + return false; + } + final User that = (User) o; + return Objects.equals(this.login, that.login) + && Objects.equals(this.password, that.password) + && this.count == that.count; + } + } + + + private class Request { + private final String loginFrom; + private final long count; + + private Request(@Nonnull final String loginFrom, long count) { + this.loginFrom = loginFrom; + this.count = count; + } + + + @Override + public int hashCode() { + return Objects.hash(loginFrom, count); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Request)) { + return false; + } + final Request that = (Request) o; + return Objects.equals(this.loginFrom, that.loginFrom) + && this.count == that.count; + } + } +}