diff --git a/client/build.gradle b/client/build.gradle index f368093..e3b66ee 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "no.tornado:tornadofx:$tornadofx_version" implementation "com.jfoenix:jfoenix:8.0.8" + implementation 'com.google.code.gson:gson:2.8.5' } compileKotlin { diff --git a/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/Connection.kt b/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/Connection.kt new file mode 100644 index 0000000..3511810 --- /dev/null +++ b/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/Connection.kt @@ -0,0 +1,106 @@ +package org.hso.texturesyncclient.controller.net + +import com.google.gson.* +import org.hso.texturesyncclient.model.Texture +import java.io.* +import java.lang.RuntimeException +import java.net.* + +@Suppress("MemberVisibilityCanBePrivate") +class Connection(val address: InetAddress, val port: Int = 10796) : Closeable { + + var socket: Socket? = null + var output: DataOutputStream? = null + var input: DataInputStream? = null + + @Throws(IOException::class) + @Synchronized + private fun getStreams(): Pair { + val i: DataInputStream + val o: DataOutputStream + + if (socket == null || !socket!!.isConnected) { + val sock = Socket(address, port) + i = DataInputStream(BufferedInputStream(sock.getInputStream())) + o = DataOutputStream(BufferedOutputStream(sock.getOutputStream())) + + input = i + output = o + socket = sock + } else { + i = input!! + o = output!! + } + + return Pair(i, o) + } + + @Throws(IOException::class, ConnectionException::class) + @Synchronized + fun ping() { + val io = getStreams() + + val obj = JsonObject() + obj.add("ping", JsonObject()) + + JsonPackage(obj).write(io.second) + + when (val pkg = Package.read(io.first)) { + is JsonPackage -> return + is BinaryPackage -> throw ConnectionUnexpecedPacketException() + is ErrorPackage -> throw ConnectionErrorException(pkg) + else -> throw RuntimeException("Unreachable") + } + } + + @Throws(IOException::class, ConnectionException::class) + @Synchronized + fun query(query : Array) : Array { + val io = getStreams() + + val obj = JsonObject() + obj.add("query", { + val inner = JsonObject() + inner.add("query", { + val array = JsonArray() + for(queryString in query) { + array.add(queryString) + } + array + }()) + inner + }()) + + JsonPackage(obj).write(io.second) + + when (val pkg = Package.read(io.first)) { + is JsonPackage -> { + try { + return Gson().fromJson>(pkg.content, Array::class.java).map { + tex -> tex.toTexture() + }.toTypedArray() + } catch (e : JsonSyntaxException ){ + throw ConnectionInvalidJsonException() + } + } + is BinaryPackage -> throw ConnectionUnexpecedPacketException() + is ErrorPackage -> throw ConnectionErrorException(pkg) + else -> throw RuntimeException("Unreachable") + } + } + + @Throws(IOException::class) + @Synchronized + override fun close() { + if (output != null) { + output!!.close() + } + if (input != null) { + input!!.close() + } + if (socket != null) { + socket!!.close() + } + } + +} \ No newline at end of file diff --git a/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/InternalJsonModel.kt b/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/InternalJsonModel.kt new file mode 100644 index 0000000..f48b84f --- /dev/null +++ b/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/InternalJsonModel.kt @@ -0,0 +1,73 @@ +package org.hso.texturesyncclient.controller.net + +// These types will be converted to the model.DataModel + +import org.hso.texturesyncclient.model.Texture +import org.hso.texturesyncclient.model.TextureFormat +import java.lang.Exception +import java.util.* + +@Suppress("ArrayInDataClass") +internal data class InternalTexture( + val id: String, + val name: String, + val tags: Array, + val format: String, + val resolution: Array, + val added_on: Array, + val texture_hash: String +) { + + constructor(tex: Texture) : this( + id = tex.id.toString(), + name = tex.name, + tags = tex.tags.clone(), + format = when (tex.format) { + TextureFormat.PNG -> "png" + TextureFormat.JPEG -> "jpeg" + }, + resolution = arrayOf(tex.resolution.first, tex.resolution.second), + added_on = arrayOf( + tex.addedOn.get(Calendar.YEAR), // + tex.addedOn.get(Calendar.MONTH), // + tex.addedOn.get(Calendar.DAY_OF_MONTH) + ), + texture_hash = bytes2hexString(tex.textureHash) + ) + + @Throws(ConnectionException::class) + fun toTexture(): Texture { + try { + return Texture( + id = UUID.fromString(id), + name = name, + tags = tags.clone(), + format = when (format.toLowerCase()) { + "jpeg" -> TextureFormat.JPEG + "jpg" -> TextureFormat.JPEG + "png" -> TextureFormat.PNG + else -> throw ConnectionInvalidJsonException() + }, + resolution = Pair(resolution[0], resolution[1]), + addedOn = GregorianCalendar(added_on[0], added_on[1], added_on[2]), + textureHash = hexString2Bytes(texture_hash) + ) + } catch (e: Exception) { // i Know, but no time :[] + throw ConnectionInvalidJsonException() + } + } +} + +private fun bytes2hexString(bytes: ByteArray): String { + val s = StringBuilder(64) + for (byte in bytes) { + s.append(String.format("%02X"), byte) + } + return s.toString() +} + +private fun hexString2Bytes(bytes: String): ByteArray { + return ByteArray(bytes.length / 2) { i -> + bytes.substring(i * 2, i * 2 + 2).toInt(16).toByte() + } +} \ No newline at end of file diff --git a/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/Package.kt b/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/Package.kt new file mode 100644 index 0000000..e9f3bab --- /dev/null +++ b/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/Package.kt @@ -0,0 +1,161 @@ +package org.hso.texturesyncclient.controller.net + +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.IOException + +internal abstract class Package { + + @Throws(IOException::class) + abstract fun write(out: DataOutputStream) + + companion object { + + const val TYPE_ERROR: Byte = 0 + protected const val TYPE_ERROR_MAX_PAYLOAD: Int = 1024 + + const val TYPE_JSON: Byte = 1 + protected const val TYPE_JSON_MAX_PAYLOAD: Int = 16 * 1024 * 1024 + + const val TYPE_BINARY: Byte = 2 + protected const val TYPE_BINARY_MAX_PAYLOAD: Int = 512 * 1024 * 1024 + + @Throws(PacketException::class, IOException::class) + fun read(input: DataInputStream): Package { + // Type Byte + val type = input.readByte() + + // 3 Reserved Bytes + input.readByte() + input.readByte() + input.readByte() + + // 4 Len Bytes + val length = input.readInt() + + when (type) { + TYPE_ERROR -> if (length > TYPE_ERROR_MAX_PAYLOAD) { + throw PacketTooLongException() + } + + TYPE_JSON -> if (length > TYPE_JSON_MAX_PAYLOAD) { + throw PacketTooLongException() + } + + TYPE_BINARY -> if (length > TYPE_BINARY_MAX_PAYLOAD) { + throw PacketTooLongException() + } + + else -> throw PacketInvalidType() + } + + val payload = ByteArray(length) + input.readFully(payload) + + when (type) { + TYPE_ERROR -> { + val msg = String(payload) + val results = msg.split(' ', ignoreCase = false, limit = 2) + + return if (results.size == 2) { + val code = results[0].toIntOrNull() + + if (code == null) { + ErrorPackage(0, msg) + } else { + ErrorPackage(code, results[1]) + } + } else { + ErrorPackage(0, msg) + } + } + + TYPE_JSON -> { + try { + val obj = JsonParser().parse(String(payload)) + + return JsonPackage(obj) + } catch (e: JsonParseException) { + throw PacketInvalidData() + } + } + + TYPE_BINARY -> { + return BinaryPackage(payload) + } + + else -> { + // Unreachable + throw PacketInvalidType() + } + } + } + } + +} + +internal data class JsonPackage(val content: JsonElement) : Package() { + override fun write(out: DataOutputStream) { + val payload = content.toString().toByteArray() + // Tag Byte + out.writeByte(TYPE_JSON.toInt()) + + // 3 Reserved Bytes + out.writeByte(0x42) + out.writeByte(0x42) + out.writeByte(0x42) + + // Length of Payload + out.writeInt(payload.size) + + // Payload + out.write(payload) + + out.flush() + } +} + +@Suppress("ArrayInDataClass") +internal data class BinaryPackage(val content: ByteArray) : Package() { + override fun write(out: DataOutputStream) { + // Tag Byte + out.writeByte(TYPE_BINARY.toInt()) + + // 3 Reserved Bytes + out.writeByte(0x42) + out.writeByte(0x42) + out.writeByte(0x42) + + // Length of Payload + out.writeInt(content.size) + + // Payload + out.write(content) + + out.flush() + } +} + +internal data class ErrorPackage(val code: Int, val message: String) : Package() { + override fun write(out: DataOutputStream) { + val payload = "$code $message".toByteArray() + // Tag Byte + out.writeByte(TYPE_ERROR.toInt()) + + // 3 Reserved Bytes + out.writeByte(0x42) + out.writeByte(0x42) + out.writeByte(0x42) + + // Length of Payload + out.writeInt(payload.size) + + // Payload + out.write(payload) + + out.flush() + } +} \ No newline at end of file diff --git a/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/error.kt b/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/error.kt new file mode 100644 index 0000000..10dfd02 --- /dev/null +++ b/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/error.kt @@ -0,0 +1,19 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package org.hso.texturesyncclient.controller.net + +import java.lang.Exception + +sealed class ConnectionException(override val message : String) : Exception(message) + +class ConnectionErrorException(val errorCode : Int, val errorMessage : String) : ConnectionException("${errorCode} ${errorMessage}") { + internal constructor(err : ErrorPackage) : this(err.code, err.message) +} +class ConnectionUnexpecedPacketException : ConnectionException("Got Unexpected Type of Packet") +class ConnectionInvalidJsonException : ConnectionException("The Format of the Json Received is Unexpected.") + + +sealed class PacketException(msg: String) : ConnectionException(msg) +class PacketTooLongException : PacketException("The Package is too long.") +class PacketInvalidType : PacketException("The Package has an Invalid Type.") +class PacketInvalidData : PacketException("The Package has an Invalid Data. (e.g. Invalid Json.)") diff --git a/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/test.kt b/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/test.kt new file mode 100644 index 0000000..6f3a6a0 --- /dev/null +++ b/client/src/main/kotlin/org/hso/texturesyncclient/controller/net/test.kt @@ -0,0 +1,22 @@ +package org.hso.texturesyncclient.controller.net + +import java.net.* + +fun main() { + // Just some test code. + + val con = Connection(InetAddress.getByName("::1")) + + con.ping() + + + println("Query:") + for (tex in con.query( + arrayOf("Red", "Food") + )) { + println(tex.toString()) + } + println() + + con.close() +} \ No newline at end of file diff --git a/client/src/main/kotlin/org/hso/texturesyncclient/model/DataModel.kt b/client/src/main/kotlin/org/hso/texturesyncclient/model/DataModel.kt index 089a425..e473889 100644 --- a/client/src/main/kotlin/org/hso/texturesyncclient/model/DataModel.kt +++ b/client/src/main/kotlin/org/hso/texturesyncclient/model/DataModel.kt @@ -2,8 +2,7 @@ package org.hso.texturesyncclient.model import java.util.* -class DataModel { -} +class DataModel enum class TextureFormat { PNG, JPEG, @@ -16,6 +15,6 @@ data class Texture( val tags : Array, val format : TextureFormat, val resolution : Pair, - val addedOn : Date, + val addedOn : Calendar, val textureHash : ByteArray )