Skip to content
Open
6 changes: 6 additions & 0 deletions http/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
**/out/
**/.gradle/
**/build/
**/libraries/
**/.idea/
local.properties
30 changes: 30 additions & 0 deletions http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Электронный магазин

## Добавление товара
### Формат запроса:
**POST /add?name=\<name>&price=\<price>&amount=\<amount>**
* **\<name>** — наименование товара
* **\<price>** — строковое представление целочисленной цены за единицу товара
* **\<amount>** — строковое представление количества добавляемого товара

## Получение информации о товарах
### Формат запроса:
**GET /list**

### Формат ответа:
Тело ответа содержит JSON массив с элементами, имеющими следующие поля:
* **id** — строковое представление целочисленного идентификатора товара
* **name** — наименование товара
* **price** — строковое представление целочисленной цены за единицу товара
* **amount** — строковое представление количество товара

## Покупка товара
### Формат запроса:
**POST /buy?id=\<id>**
* **id** — идентификатор товара

### Формат ответа:
Тело ответа содержит одну из следующих строк:
* **Confirmed** — товар успешно куплен
* **Not enough product in stock** — товар уже распродан
* **No product found** - запрошенного товара не существует
30 changes: 30 additions & 0 deletions http/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.3.21'
}

group 'ru.hse.spb.kazakov.server'
version '1.0'

repositories {
mavenCentral()
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation 'com.google.code.gson:gson:2.8.5'
testCompile group: 'junit', name: 'junit', version: '4.12'
}

compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}

jar {
manifest {
attributes 'Main-Class': 'ru.hse.spb.kazakov.server.MainKt'
}
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}
135 changes: 135 additions & 0 deletions http/src/main/kotlin/ru/hse/spb/kazakov/server/Server.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package ru.hse.spb.kazakov.server

import com.google.gson.Gson
import ru.hse.spb.kazakov.server.http.*
import java.io.DataInputStream
import java.io.OutputStreamWriter
import java.lang.Exception
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketException
import java.nio.charset.Charset
import java.util.concurrent.locks.ReentrantReadWriteLock

@ExperimentalUnsignedTypes
class Server(port: Int) {
private val serverSocket = ServerSocket(port)
private val clientsSockets = mutableListOf<Socket>()
private val serverThreads = mutableListOf<Thread>()
private val products = mutableListOf<Product>()
private val productsAccess = ReentrantReadWriteLock()

fun run() {
val acceptClientCycle = {
try {
while (true) {
val clientSocket = serverSocket.accept()
clientsSockets.add(clientSocket)
val clientThread = Thread(RequestResponseCycle(clientSocket))
serverThreads.add(clientThread)
clientThread.start()
}
} catch (exception: SocketException) {}
}

val acceptClientsThread = Thread(acceptClientCycle)
serverThreads.add(acceptClientsThread)
acceptClientsThread.start()
}

fun stop() {
serverSocket.close()
clientsSockets.forEach { it.close() }
serverThreads.forEach { it.join() }
}

private fun productsToJson(): String {
productsAccess.readLock().lock()
val productsJson = Gson().toJson(products)
productsAccess.readLock().unlock()
return productsJson
}

private fun addProduct(name: String, price: UInt, amount: UInt) {
productsAccess.writeLock().lock()
val product = Product(products.size, name, price, amount)
products.add(product)
productsAccess.writeLock().unlock()
}

private data class Product(val id: Int, val name: String, val price: UInt, var amount: UInt)

private inner class RequestResponseCycle(clientSocket: Socket) : Runnable {
private val dataInputStream = DataInputStream(clientSocket.getInputStream())
private val outputStream = OutputStreamWriter(clientSocket.getOutputStream(), Charset.forName("UTF-8"))

override fun run() {
while (true) {
val request = try {
parseHttpRequest(dataInputStream)
} catch (exception: MalformedHttpException) {
outputStream.write(HttpResponse(HttpResponseType.BAD_REQUEST).toString())
continue
} catch (exception: Exception) {
outputStream.write(HttpResponse(HttpResponseType.SERVER_ERROR).toString())
break
}

val response = processRequest(request)
outputStream.write(response.toString())
outputStream.flush()
}
}

private fun processRequest(request: HttpRequest): HttpResponse =
when (request.requestLine.method) {
"GET" -> {
if (request.requestLine.url.path != "/list") {
HttpResponse(HttpResponseType.NOT_FOUND)
} else {
HttpResponse(HttpResponseType.OK, ContentType.JSON, productsToJson())
}
}

"POST" -> {
when (request.requestLine.url.path) {
"/add" -> {
val queryParameters = request.requestLine.url.queryParameters
val name = queryParameters["name"]
val price = queryParameters["price"]?.toUInt()
val amount = queryParameters["amount"]?.toUInt()
if (name == null || price == null || amount == null || price == 0U || amount == 0U) {
HttpResponse(HttpResponseType.UNPROCESSABLE_ENTITY)
} else {
addProduct(name, price, amount)
HttpResponse(HttpResponseType.OK)
}
}

"/buy" -> {
val id = request.requestLine.url.queryParameters["id"]?.toInt()
productsAccess.writeLock().lock()
if (id == null) {
productsAccess.writeLock().unlock()
HttpResponse(HttpResponseType.UNPROCESSABLE_ENTITY)
} else if (id < 0 || id >= products.size) {
productsAccess.writeLock().unlock()
HttpResponse(HttpResponseType.OK, ContentType.TEXT, "No product found")
} else {
val product = products[id]
val resultMessage =
if (product.amount > 0U) "Confirmed".also { product.amount-- } else "Not enough product in stock"
productsAccess.writeLock().unlock()
HttpResponse(HttpResponseType.OK, ContentType.TEXT, resultMessage)
}
}

else -> HttpResponse(HttpResponseType.NOT_FOUND)
}
}

else -> HttpResponse(HttpResponseType.NOT_IMPLEMENTED)
}
}

}
27 changes: 27 additions & 0 deletions http/src/main/kotlin/ru/hse/spb/kazakov/server/http/HttpRequest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ru.hse.spb.kazakov.server.http

data class HttpRequest(
val requestLine: RequestLine,
val messageBody: ByteArray,
val fields: Map<String, String>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as HttpRequest

if (requestLine != other.requestLine) return false
if (!messageBody.contentEquals(other.messageBody)) return false
if (fields != other.fields) return false

return true
}

override fun hashCode(): Int {
var result = requestLine.hashCode()
result = 31 * result + messageBody.contentHashCode()
result = 31 * result + fields.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package ru.hse.spb.kazakov.server.http

import com.sun.xml.internal.messaging.saaj.util.TeeInputStream
import java.io.*
import java.net.MalformedURLException
import java.util.*


fun parseHttpRequest(inputStream: DataInputStream): HttpRequest {
val bufferedReader = BufferedReader(InputStreamReader(getHeaderStream(inputStream)))

val requestLine = try {
parseRequestLine(bufferedReader)
} catch (exception: MalformedURLException) {
throw MalformedHttpException()
}
val fields = parseFields(bufferedReader)
val body = parseBody(inputStream, fields["Content-Length"])

return HttpRequest(requestLine, body, fields)
}

private fun getHeaderStream(inputStream: DataInputStream): InputStream {
val output = ByteArrayOutputStream()
val input = TeeInputStream(inputStream, output)
val slidingWindow = ArrayDeque<Int>()

var ch = input.read()
while (ch != -1) {
if (slidingWindow.size == "\r\n\r\n".length) {
slidingWindow.removeFirst()
}
slidingWindow.addLast(ch)

val stringVal = slidingWindow.toIntArray().joinToString("") { it.toChar().toString() }
if (stringVal == "\r\n\r\n") {
break
}
ch = input.read()
}

if (ch == -1) {
throw MalformedURLException()
}

return ByteArrayInputStream(output.toByteArray())
}

private val requestLineRegexp = Regex("""([^\s]+)\s+([^\s]+)\s+HTTP/(\d+)\.(\d+)""")
private fun parseRequestLine(inputStream: BufferedReader): RequestLine {
var line = inputStream.readLine()
while (line != null && line.isEmpty()) {
line = inputStream.readLine()
}
if (line == null) {
throw MalformedHttpException()
}

val matchResult = requestLineRegexp.matchEntire(line) ?: throw MalformedHttpException()
val groups = matchResult.groupValues
val httpVersion = RequestLine.HttpVersion(groups[3].toInt(), groups[4].toInt())

return RequestLine(matchResult.groupValues[1], httpVersion, URL("http://" + groups[2]))
}

private val fieldRegexp = Regex("""\s*([^\s]+)\s*:\s*(.*)\s*""")
private fun parseFields(bufferedReader: BufferedReader): Map<String, String> {
val fields = HashMap<String, String>()

var line = bufferedReader.readLine()
while (line != null && !line.isEmpty()) {
val matchResult = fieldRegexp.matchEntire(line) ?: throw MalformedHttpException()
val groups = matchResult.groupValues
fields[groups[1]] = groups[2]
line = bufferedReader.readLine()
}

return fields
}

private fun parseBody(input: DataInputStream, bodySize: String?): ByteArray =
if (bodySize == null) {
byteArrayOf()
} else {
val size = try {
bodySize.toInt()
} catch (exception: NumberFormatException) {
throw MalformedHttpException()
}
val result = ByteArray(size)
input.readFully(result)
result
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ru.hse.spb.kazakov.server.http

import java.nio.charset.Charset

class HttpResponse(
private val responseType: HttpResponseType,
private val contentType: ContentType = ContentType.TEXT,
private val body: String = ""
) {
override fun toString(): String =
responseType.toString() + contentType + getContentLengthField() + "\r\n" + body

private fun getContentLengthField() = "Content-Length: ${body.toByteArray(Charset.forName("UTF-8")).size}\r\n"
}

enum class HttpResponseType {
OK {
override fun toString(): String = "HTTP/1.1 200 OK\r\n"
},

BAD_REQUEST {
override fun toString(): String = "HTTP/1.1 400 Bad Request\r\n"
},

NOT_FOUND {
override fun toString(): String = "HTTP/1.1 404 Not Found\r\n"
},

METHOD_NOT_ALLOWED {
override fun toString(): String = "HTTP/1.1 405 Method Not Allowed\r\n"
},

UNPROCESSABLE_ENTITY {
override fun toString(): String = "HTTP/1.1 422 Unprocessable Entity\r\n"
},

SERVER_ERROR {
override fun toString(): String = "HTTP/1.1 500 Internal Server Error\r\n"
},

NOT_IMPLEMENTED {
override fun toString(): String = "HTTP/1.1 501 Not Implemented\r\n"
}
}

enum class ContentType {
TEXT {
override fun toString(): String = "Content-Type: text/plain\r\n"
},

JSON {
override fun toString(): String = "Content-Type: application/json\r\n"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package ru.hse.spb.kazakov.server.http

class MalformedHttpException : Exception()
Loading