Skip to content
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
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,39 @@ __pycache__/

### VisualStudio Patch ###
# By default, sensitive information, such as encrypted password

# Compiled class file
*.class

# Log file
*.log

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
!gradle/wrapper/gradle-wrapper.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*

build
.gradle
.idea
*.iml
out/


# should be stored in the .pubxml.user file.

# End of https://www.gitignore.io/api/c++,latex,cmake,netbeans,qtcreator,visualstudio
40 changes: 40 additions & 0 deletions http/client/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1'
}
}

plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm' version '1.3.21'
}

apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'java'
apply plugin: 'kotlin'

jar {
manifest {
attributes 'Main-Class': 'ru.hse.spb.networks.sharkova.http.MainKt'
}

from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}

group 'ru.hse.spb.networks.sharkova.http'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
mavenCentral()
}

dependencies {
implementation 'com.google.code.gson:gson:2.8.5'
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testCompile group: 'junit', name: 'junit', version: '4.12'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package ru.hse.spb.networks.sharkova.http

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import ru.hse.spb.networks.sharkova.http.httputils.HttpResponseParser
import ru.hse.spb.networks.sharkova.http.httputils.HttpRequest
import ru.hse.spb.networks.sharkova.http.httputils.HttpResponse
import java.io.*
import java.net.Socket
import java.nio.charset.Charset
import java.util.ArrayList


/**
* Basic HTTP client implementation.
* Supported commands: add product, list products, buy product, exit, output help.
*/
class Client(private val host: String, port: Int) {
private val serverSocket = Socket(host, port)
private val serverOutput = DataInputStream(serverSocket.getInputStream())
private val outputStreamWriter = OutputStreamWriter(serverSocket.getOutputStream(),
Charset.forName("UTF-8"))

companion object {
private const val ADD_PRODUCT = 1
private const val LIST_PRODUCTS = 2
private const val BUY_PRODUCT = 3
private const val EXIT = 4
private const val HELP = 5
}

/**
* Runs the client request-response cycle, reading the user input and handling it accordingly
* (sending HTTP request, receiving HTTP response).
*/
fun run() {
outputUsage()
while (true) {
val input = readLine()
when (input?.toIntOrNull()) {
ADD_PRODUCT -> {
println("Input product name, price, and amount, each on a new line")
val name = readLine()
val price = readLine()?.toIntOrNull()
val amount = readLine()?.toIntOrNull()
if (name == null) {
outputIncorrectArgumentMessage("name")
} else if (price == null || price < 0) {
outputIncorrectArgumentMessage("price")
} else if (amount == null || amount < 0) {
outputIncorrectArgumentMessage("amount")
} else {
addProduct(name, price.toString(), amount.toString())
}
}
LIST_PRODUCTS -> {
listProducts()
}
BUY_PRODUCT -> {
println("Input product id. " +
"If you don't know the id, input -1 to return to the main menu " +
"and input 2 to list all products")
val id = readLine()?.toIntOrNull() ?: outputIncorrectArgumentMessage("id")
if (id != -1) {
buyProduct(id.toString())
}
}
EXIT -> {
stop()
return
}
HELP -> {
outputUsage()
}
else -> {
println("Incorrect command number.")
outputUsage()
}
}
println()
}
}

private fun addProduct(name: String, price: String, amount: String) {
val request = HttpRequest("POST", "add",
mapOf("name" to name, "price" to price, "amount" to amount), mapOf("Host" to host))
outputStreamWriter.write(request.toString())
outputStreamWriter.flush()
val response = HttpResponseParser.parseInput(serverOutput)
if (response.getStatusCode() != HttpResponse.StatusCode.OK) {
println("Could not add product.")
}

}

private fun listProducts() {
val request = HttpRequest("GET", "list", fields = mapOf("Host" to host))
outputStreamWriter.write(request.toString())
outputStreamWriter.flush()
val response = HttpResponseParser.parseInput(serverOutput)
if (response.getStatusCode() == HttpResponse.StatusCode.OK) {
val products = jsonToProducts(response.body)
for (product in products) {
println("Id: ${product.id}; name: ${product.name.replace("%20", " ")}; " +
"price: ${product.price}; amount: ${product.amount}")
}
} else {
println("Could not retrieve product list. ${response.statusCode} ${response.reasonPhrase}")
}
}

private fun buyProduct(id: String) {
val request = HttpRequest("POST", "buy", mapOf("id" to id), mapOf("Host" to host))
outputStreamWriter.write(request.toString())
outputStreamWriter.flush()
val response = HttpResponseParser.parseInput(serverOutput)
if (response.getStatusCode() == HttpResponse.StatusCode.OK) {
println(String(response.body))
} else {
println("Could not buy product. ${response.statusCode} ${response.reasonPhrase}")
}
}

private fun stop() {
serverSocket.close()
}

private fun outputUsage() {
println("Input a single number corresponding to the following commands:")
println("1: Add product")
println("2: List products")
println("3: Buy product")
println("4: Exit program")
println("5: Output this help message")
}

private fun outputIncorrectArgumentMessage(argumentName: String) {
println("Incorrect argument: $argumentName")
}

private fun jsonToProducts(byteArray: ByteArray): List<Product> {
val reader = JsonReader(StringReader(String(byteArray)))
val listType = object : TypeToken<ArrayList<Product>>(){}.type
return Gson().fromJson(reader, listType)
}


private data class Product(val id: Int, val name: String, val price: Int, var amount: Int)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ru.hse.spb.networks.sharkova.http

import ru.hse.spb.networks.sharkova.http.httputils.MalformedHttpException
import java.io.IOException
import java.lang.IllegalArgumentException

fun main(args: Array<String>) {
if (args.size != 2 || args[1].toIntOrNull() == null) {
println("Incorrect arguments. Required arguments: [host name] [port number]")
return
}

try {
val client = Client(args[0], args[1].toInt())
client.run()
} catch (e: IOException) {
outputErrorMessage()
println("Server might currently be unavailable.")
} catch (e: IllegalArgumentException) {
outputErrorMessage()
} catch (e: MalformedHttpException) {
println("Server was unable to response. ")
}
}

private fun outputErrorMessage() = println("Unable to connect to the server. Make sure the arguments are correct.")
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ru.hse.spb.networks.sharkova.http.httputils

/**
* This class represents an HTTP request.
*/
data class HttpRequest(private val method: String,
private val urlStart: String,
private val queryParameters: Map<String, String> = emptyMap(),
private val fields: Map<String, String> = emptyMap(),
private val body: String = "") {
private val completeUrlString: String
init {
val urlBuilder = StringBuilder("/$urlStart")
if (queryParameters.isNotEmpty()) {
urlBuilder.append("?")
for (parameter in queryParameters) {
urlBuilder.append(parameter.key)
urlBuilder.append("=")
urlBuilder.append(parameter.value.replace(Regex("[\\s]"), "%20"))
urlBuilder.append("&")
}
urlBuilder.setLength(urlBuilder.length - 1)
}
completeUrlString = urlBuilder.toString()
}

override fun toString(): String {
val requestBuilder = StringBuilder("$method $completeUrlString HTTP/1.1\r\n")
for (field in fields) {
requestBuilder.append("${field.key}: ${field.value}\r\n")
}
requestBuilder.append("\r\n")
requestBuilder.append(body)
return requestBuilder.toString()
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package ru.hse.spb.networks.sharkova.http.httputils

/**
* This class represents an HTTP response.
*/
data class HttpResponse(val httpVersion: HttpVersion,
val statusCode: Int,
val reasonPhrase: String,
val fields: Map<String, String>,
val body: ByteArray) {

/**
* This method allows us to check the status code and process it accordingly.
* It is more readable than simply using constants in client code.
*/
fun getStatusCode(): StatusCode = when (statusCode) {
200 -> StatusCode.OK
400 -> StatusCode.BAD_REQUEST
404 -> StatusCode.NOT_FOUND
405 -> StatusCode.METHOD_NOT_ALLOWED
422 -> StatusCode.UNPROCESSABLE_ENTITY
500 -> StatusCode.SERVER_ERROR
501 -> StatusCode.NOT_IMPLEMENTED
else -> StatusCode.NOT_IMPLEMENTED
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as HttpResponse

if (httpVersion != other.httpVersion) return false
if (statusCode != other.statusCode) return false
if (reasonPhrase != other.reasonPhrase) return false
if (fields != other.fields) return false
if (!body.contentEquals(other.body)) return false

return true
}

override fun hashCode(): Int {
var result = httpVersion.hashCode()
result = 31 * result + statusCode
result = 31 * result + reasonPhrase.hashCode()
result = 31 * result + fields.hashCode()
result = 31 * result + body.contentHashCode()
return result
}

data class HttpVersion(val majorNumber: Int, val minorNumber: Int)

enum class StatusCode {
OK,
BAD_REQUEST,
NOT_FOUND,
METHOD_NOT_ALLOWED,
UNPROCESSABLE_ENTITY,
SERVER_ERROR,
NOT_IMPLEMENTED
}
}
Loading