diff --git a/examples/lab1/APlusBLib/APlusBLib.vcxproj b/examples/lab1/APlusBLib/APlusBLib.vcxproj
index 8472111..3a3b784 100644
--- a/examples/lab1/APlusBLib/APlusBLib.vcxproj
+++ b/examples/lab1/APlusBLib/APlusBLib.vcxproj
@@ -126,7 +126,7 @@
true
_DEBUG;APLUSBLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
true
- Use
+ NotUsing
pch.h
@@ -143,7 +143,7 @@
true
NDEBUG;APLUSBLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
true
- Use
+ NotUsing
pch.h
diff --git a/examples/lab2/README.md b/examples/lab2/README.md
new file mode 100644
index 0000000..f0dc3b2
--- /dev/null
+++ b/examples/lab2/README.md
@@ -0,0 +1,11 @@
+## Запуск сервера
+
+```
+python server.py 8080
+```
+
+## Запуск клиента
+
+```
+python main.py
+```
diff --git a/examples/lab3/.gitignore b/examples/lab3/.gitignore
new file mode 100644
index 0000000..4b78f38
--- /dev/null
+++ b/examples/lab3/.gitignore
@@ -0,0 +1,7 @@
+*.iml
+*.ipr
+*.iws
+.idea
+out
+target
+*.class
diff --git a/examples/lab3/README.md b/examples/lab3/README.md
new file mode 100644
index 0000000..759a677
--- /dev/null
+++ b/examples/lab3/README.md
@@ -0,0 +1,41 @@
+# Задание
+
+Написать систему мгновенного обмена сообщениями между несколькими пользователями. В проекте должна присутствовать
+возможность сохранения состояния в формат, поддерживающий валидацию по схеме. Валидация должна производиться либо
+в программе при импорте данных, либо в юнит-тестах, проверяющих корректность сохранения состояния.
+
+# Описание реализации
+
+Между клиентом и сервером происходит обмен JSON сообщениями вида (класс `datatypes.Message`):
+```
+{"senderId":"AD60","receiverId":"C399","body":"hello", timestamp:"2020-09-11T08:54:45.528807100Z"}
+```
+ - `senderId` - идентификатор отправителя,
+ - `receiverId` - идентификатор получателя,
+ - `body` - текстовое сообщение,
+ - `timestamp` - время создания сообщения.
+
+При подключении клиент должен отправить JSON сообщение со своим именем (класс `datatypes.ClientInfo`):
+```
+{"timestamp":"2020-09-11T08:49:44.644320700Z", "login":"0FB9"}
+```
+
+При получении и отправлении сообщения на сервере производится сохранение поля `timestamp` в JSON файл (класс `datatypes.ClientState`)
+в заранее созданную папку `states_path`, указанную при старте сервера в качестве аргумента. Пример сохранённого состояния пользователя
+0FB9 в файле 0FB9.json:
+```
+{"lastMessageTimestamp":"2020-09-11T08:49:44.644320700Z"}
+```
+
+Для данной структуры разработана JSON схема `resources/schemas/v0.0.1/ClientStateSchema.json`. Проверка соответствия
+сохраняемого состояния схеме производится юнит тестом `SchemaCompliance.checkClientStateSchemeCompliance`.
+
+# Инструкция запуска
+
+Тестовый запуск можно произвести из среды разработки или из консоли при наличии в системе утилиты sbt https://www.scala-sbt.org/.
+
+```
+sbt "runMain client.Server --host 192.168.0.2 --port 10000 --states_path ./client_states"
+sbt "runMain client.Client --host 192.168.0.2 --port 10000"
+```
+
diff --git a/examples/lab3/build.sbt b/examples/lab3/build.sbt
new file mode 100644
index 0000000..fe6e1d7
--- /dev/null
+++ b/examples/lab3/build.sbt
@@ -0,0 +1,12 @@
+name := "ConsoleChat"
+
+version := "0.1"
+
+scalaVersion := "2.13.3"
+
+// https://mvnrepository.com/artifact/com.github.java-json-tools/json-schema-validator
+libraryDependencies += "com.github.java-json-tools" % "json-schema-validator" % "2.2.14"
+libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.11.0"
+libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2"
+libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
+libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % Test
\ No newline at end of file
diff --git a/examples/lab3/project/build.properties b/examples/lab3/project/build.properties
new file mode 100644
index 0000000..302b6be
--- /dev/null
+++ b/examples/lab3/project/build.properties
@@ -0,0 +1 @@
+sbt.version = 1.3.13
\ No newline at end of file
diff --git a/examples/lab3/resources/schemas/v0.0.1/ClientStateSchema.json b/examples/lab3/resources/schemas/v0.0.1/ClientStateSchema.json
new file mode 100644
index 0000000..7534e47
--- /dev/null
+++ b/examples/lab3/resources/schemas/v0.0.1/ClientStateSchema.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json-schema.org/draft/2019-09/schema#",
+ "$id": "http://mychat.germanywestcentral.cloudapp.azure.com/schemas/v0.0.1/ClientStateSchema.json",
+ "title": "ClientState",
+ "type": "object",
+ "properties": {
+ "lastMessageTimestamp": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "lastMessageTimestamp"
+ ]
+}
\ No newline at end of file
diff --git a/examples/lab3/src/main/scala/client/Client.scala b/examples/lab3/src/main/scala/client/Client.scala
new file mode 100644
index 0000000..79a55f0
--- /dev/null
+++ b/examples/lab3/src/main/scala/client/Client.scala
@@ -0,0 +1,173 @@
+package client
+
+import java.io.{BufferedReader, IOException, InputStreamReader}
+import java.net.{InetSocketAddress, SocketAddress}
+import java.nio.channels.{NonReadableChannelException, NotYetConnectedException, SelectionKey, Selector, SocketChannel}
+
+import ch.qos.logback.classic.Level
+import com.typesafe.scalalogging.{LazyLogging, Logger}
+import datatypes.{ClientInfo, Message}
+import org.slf4j.LoggerFactory
+import ch.qos.logback.classic.Level
+import com.fasterxml.jackson.core.JsonProcessingException
+import com.fasterxml.jackson.databind.exc.MismatchedInputException
+import com.sun.mail.util.SocketConnectException
+
+import scala.collection.immutable.ArraySeq
+import scala.collection.mutable
+import scala.jdk.CollectionConverters._
+import scala.reflect.ClassTag
+
+class Client(host:String,
+ port:Int,
+ recieveCallback: Message=>Unit,
+ login:String
+ ) extends Runnable with LazyLogging {
+
+ @volatile var isRunning = false
+ private val selector = Selector.open()
+ private var clientChannel: SocketChannel = _
+ private val inbox = mutable.ArrayDeque[Message]()
+ private var isInitialized = false
+
+ def start(): Unit = {
+ isRunning = true
+
+ Runtime.getRuntime.addShutdownHook(new Thread() {
+ override def run(): Unit = {
+ Thread.currentThread().setName("CLEANER")
+ if (isRunning) {
+ logger.info("Closing connection.")
+ isRunning = false
+ Thread.sleep(10)
+ if (clientChannel != null) clientChannel.close()
+ }
+ }
+ })
+
+ val clientThread = new Thread(this, "CLIENT")
+ clientThread.start()
+ }
+
+ def leaveMessage(receiverId:String, body: String): Selector = {
+ val msg = Message(senderId = this.login, receiverId = receiverId, body = body)
+ inbox.addOne(msg)
+ clientChannel.register(selector, SelectionKey.OP_WRITE)
+ selector.wakeup()
+ }
+
+ def run (): Unit = {
+ clientChannel = SocketChannel.open()
+ try {
+ clientChannel.connect(new InetSocketAddress(host, port))
+ logger.info(s"Connected to $host:$port")
+ } catch {
+ case e: SocketConnectException =>
+ logger.info("Connection failed.")
+ System.exit(0)
+ }
+
+ clientChannel.configureBlocking(false)
+ clientChannel.register(selector, SelectionKey.OP_WRITE)
+
+ sendRecieveLoop()
+ }
+
+ private def sendRecieveLoop() = {
+ while (isRunning) {
+ selector.select()
+ for (key <- selector.selectedKeys().asScala) {
+ if (key.isValid && key.isReadable) {
+ receiveFromServer()
+ }
+ if (key.isValid && key.isWritable) {
+ sendToServer()
+ }
+ }
+ }
+ }
+
+ private def sendToServer() = {
+ if (!isInitialized) {
+ val clientInfo = ClientInfo(login = login)
+ send[ClientInfo](clientInfo)
+ isInitialized = true
+ }
+ if (isInitialized && inbox.nonEmpty) {
+ try {
+ val msg = inbox.removeHead()
+ send(msg)
+ } catch {
+ case e@(_: MismatchedInputException |
+ _: IOException) =>
+ logger.info(s"Client disconnected.")
+ System.exit(0)
+ case e@(_: NotYetConnectedException |
+ _: NonReadableChannelException |
+ _: JsonProcessingException) =>
+ logger.error(e.getMessage)
+ }
+ }
+ }
+
+ private def send[T](msg: T)(implicit ct: ClassTag[T]) = {
+ util.Util.send[T](msg, clientChannel)
+ clientChannel.register(selector, SelectionKey.OP_READ)
+ }
+
+ private def receiveFromServer() = {
+ try {
+ val msg = util.Util.recieve[Message](clientChannel)
+ if (msg.nonEmpty) {
+ recieveCallback(msg.get)
+ }
+ } catch {
+ case e@(_: MismatchedInputException |
+ _: IOException) =>
+ logger.info(s"Client disconnected.")
+ System.exit(0)
+ case e@(_: NotYetConnectedException |
+ _: NonReadableChannelException |
+ _: JsonProcessingException) =>
+ logger.error(e.getMessage)
+ }
+ }
+}
+
+object Client extends LazyLogging {
+ LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME).asInstanceOf[ch.qos.logback.classic.Logger].setLevel(Level.INFO)
+
+ val defaultArgs = Map(
+ "host" -> "0.0.0.0",
+ "port" -> "10000"
+ )
+
+ def uiLoop(client: Client): Unit = {
+ Thread.currentThread().setName("UI")
+ val consoleInput = new BufferedReader(new InputStreamReader(System.in))
+
+ while(client.isRunning) {
+ val line = consoleInput.readLine()
+ if (line != null) {
+ client.leaveMessage(util.Util.BROADCAST_RECIEVER_ID, line)
+ }
+ }
+ }
+
+ def main(args:Array[String]): Unit = {
+ val argsMap = args.toSeq.sliding(2,2).map{ case ArraySeq(k,v) => (k.replace("--",""), v) }.toMap
+ .withDefault(defaultArgs)
+ val host = argsMap("host")
+ val port = argsMap("port").toInt
+ val login = argsMap.getOrElse("login", util.Util.genId(java.time.Instant.now()))
+
+ val client = new Client(
+ host,
+ port,
+ message => println(s"${message.senderId}: ${message.body}"),
+ login = login)
+ client.start()
+
+ uiLoop(client)
+ }
+}
\ No newline at end of file
diff --git a/examples/lab3/src/main/scala/datatypes/ClientInfo.scala b/examples/lab3/src/main/scala/datatypes/ClientInfo.scala
new file mode 100644
index 0000000..74a1110
--- /dev/null
+++ b/examples/lab3/src/main/scala/datatypes/ClientInfo.scala
@@ -0,0 +1,8 @@
+package datatypes
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+case class ClientInfo (
+ @JsonProperty("timestamp") timestamp:String = java.time.Instant.now().toString,
+ @JsonProperty("login") login:String
+)
\ No newline at end of file
diff --git a/examples/lab3/src/main/scala/datatypes/ClientState.scala b/examples/lab3/src/main/scala/datatypes/ClientState.scala
new file mode 100644
index 0000000..9d6b3f1
--- /dev/null
+++ b/examples/lab3/src/main/scala/datatypes/ClientState.scala
@@ -0,0 +1,7 @@
+package datatypes
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+case class ClientState (
+ @JsonProperty("lastMessageTimestamp") lastMessageTimestamp:String
+)
diff --git a/examples/lab3/src/main/scala/datatypes/Message.scala b/examples/lab3/src/main/scala/datatypes/Message.scala
new file mode 100644
index 0000000..fb998e6
--- /dev/null
+++ b/examples/lab3/src/main/scala/datatypes/Message.scala
@@ -0,0 +1,10 @@
+package datatypes
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+case class Message(
+ @JsonProperty("timestamp") timestamp:String = java.time.Instant.now().toString,
+ @JsonProperty("senderId") senderId:String,
+ @JsonProperty("receiverId") receiverId:String,
+ @JsonProperty("body") body:String
+)
\ No newline at end of file
diff --git a/examples/lab3/src/main/scala/server/Client.scala b/examples/lab3/src/main/scala/server/Client.scala
new file mode 100644
index 0000000..5fbbcba
--- /dev/null
+++ b/examples/lab3/src/main/scala/server/Client.scala
@@ -0,0 +1,36 @@
+package server
+
+import java.nio.channels.SocketChannel
+import java.nio.file.Path
+
+import com.typesafe.scalalogging.LazyLogging
+import datatypes.{ClientState, Message}
+import util.SavableState
+
+import scala.collection.mutable
+
+class Client(
+ var login:String = "",
+ var lastMessageTimestamp:String = "",
+ val inbox:mutable.ArrayDeque[Message] = new mutable.ArrayDeque[Message](),
+ val socketChannel:SocketChannel,
+ var isInitialized:Boolean = false) extends SavableState with LazyLogging {
+
+ private def getStateFilePath(statesDir:Path): Path = statesDir.resolve(s"${login}.json")
+
+ def saveState(statesDir:Path):Unit = {
+ val filePath = getStateFilePath(statesDir)
+ val clientState = ClientState(lastMessageTimestamp = lastMessageTimestamp)
+ util.Util.writeJsonToFile(clientState, filePath)
+ logger.debug(s"Client $login state saved.")
+ }
+
+ def loadState(statesDir:Path):Unit = {
+ val filePath = getStateFilePath(statesDir)
+ val clientState = util.Util.readJsonFromFile[ClientState](filePath)
+ if (clientState.nonEmpty) {
+ lastMessageTimestamp = clientState.get.lastMessageTimestamp
+ logger.debug(s"Client $login state loaded.")
+ }
+ }
+}
diff --git a/examples/lab3/src/main/scala/server/Server.scala b/examples/lab3/src/main/scala/server/Server.scala
new file mode 100644
index 0000000..6eee80a
--- /dev/null
+++ b/examples/lab3/src/main/scala/server/Server.scala
@@ -0,0 +1,220 @@
+package server
+
+import java.io.IOException
+import java.net.{InetAddress, InetSocketAddress, ServerSocket, Socket, SocketAddress, SocketOption}
+import java.nio.ByteBuffer
+import java.nio.channels.{NonReadableChannelException, NotYetConnectedException, SelectionKey, Selector, ServerSocketChannel, SocketChannel}
+import java.nio.file.{Files, Path}
+import java.util.concurrent.{ConcurrentHashMap, Executor, Executors, TimeUnit}
+
+import ch.qos.logback.classic.Level
+import com.fasterxml.jackson.core.JsonProcessingException
+import com.fasterxml.jackson.databind.exc.MismatchedInputException
+import com.typesafe.scalalogging.LazyLogging
+import datatypes.{ClientInfo, Message}
+import org.slf4j.LoggerFactory
+
+import scala.collection.immutable.ArraySeq
+import scala.collection.mutable
+import scala.jdk.CollectionConverters._
+
+class Server(host:String, port:Int, val savedStatesDir:Path) extends Runnable with LazyLogging {
+ private val connectedClients = new mutable.HashMap[Int, Client]()
+ private val loggedinClients = new mutable.HashMap[String, Client]()
+ private val selector = Selector.open()
+
+ @volatile var isRunning = false
+
+ def run(): Unit = {
+ logger.info(s"Server is running on $host:$port.")
+ val serverSocketChannel = ServerSocketChannel.open()
+ serverSocketChannel.bind(new InetSocketAddress(host, port))
+ serverSocketChannel.configureBlocking(false)
+ serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT)
+
+ sendRecieveAcceptLoop(serverSocketChannel)
+ }
+
+ private def sendRecieveAcceptLoop(serverSocketChannel: ServerSocketChannel) = {
+ while (isRunning) {
+ selector.select()
+ for (key <- selector.selectedKeys().asScala) {
+ if (key.isValid && key.isAcceptable)
+ acceptNewClient(serverSocketChannel)
+ if (key.isValid && key.isReadable)
+ receiveClientData(key)
+ if (key.isValid && key.isWritable)
+ sendDataToClient(key)
+ }
+ }
+ }
+
+ private def acceptNewClient(serverSocketChannel: ServerSocketChannel) = {
+ val clientChannel = serverSocketChannel.accept()
+ if (clientChannel != null) {
+ clientChannel.configureBlocking(false)
+ clientChannel.register(selector, SelectionKey.OP_READ)
+
+ val clientId = clientChannel.hashCode()
+ val client = new Client(socketChannel = clientChannel)
+
+ connectedClients.put(clientId, client)
+
+ logger.debug(s"Connection accepted.")
+ }
+ }
+
+ private def receiveClientData(key: SelectionKey) = {
+ val clientChannel = key.channel().asInstanceOf[SocketChannel]
+ val clientId = clientChannel.hashCode()
+ val client = connectedClients(clientId)
+ try {
+ if (!client.isInitialized) {
+ val clientInfo = util.Util.recieve[ClientInfo](clientChannel)
+ if (clientInfo.nonEmpty) {
+ loginClient(client, clientInfo)
+ } else {
+ throw new java.io.IOException("Bad client info. Initialization aborted.")
+ }
+ } else {
+ val msg = util.Util.recieve[Message](clientChannel)
+ if (msg.nonEmpty) {
+ client.lastMessageTimestamp = msg.get.timestamp
+ client.saveState(savedStatesDir)
+ routeMessage(msg.get, clientId)
+ }
+ }
+ } catch {
+ case e @ (_: com.fasterxml.jackson.databind.exc.MismatchedInputException |
+ _: java.io.IOException ) =>
+ logger.error(e.getMessage, e.getCause)
+ handleDisconnection(clientChannel, clientId)
+ case e @ (_ : java.nio.channels.NotYetConnectedException |
+ _ : java.nio.channels.NonReadableChannelException |
+ _ : com.fasterxml.jackson.core.JsonProcessingException) =>
+ logger.error(e.getMessage, e.getCause)
+ }
+ }
+
+ private def loginClient(client: Client, clientInfo: Option[ClientInfo]) = {
+ val login = clientInfo.get.login
+ if (loggedinClients.keySet.contains(login)) {
+ val msg = Message(body=s"$login is already connected.", receiverId = "", senderId = "SERVER")
+ leaveMessageForClient(msg, client)
+ } else {
+ loggedinClients.put(clientInfo.get.login, client)
+ client.login = clientInfo.get.login
+ client.loadState(savedStatesDir)
+ client.isInitialized = true
+ val msg = if ("".equals(client.lastMessageTimestamp)) {
+ Message(body = s"Hello new client with login ${client.login} !", receiverId = "", senderId = "SERVER")
+ } else {
+ Message(body = s"Welcome back ${client.login} !", receiverId = "", senderId = "SERVER")
+ }
+ leaveMessageForClient(msg, client)
+ logger.info(s"Client ${client.login} logged in.")
+ }
+ }
+
+ private def handleDisconnection(clientChannel: SocketChannel, clientId: Int) = {
+ clientChannel.close()
+ val disconnectedClient = connectedClients.remove(clientId)
+ if (disconnectedClient.nonEmpty) {
+ loggedinClients.remove(disconnectedClient.get.login)
+ val msg = Message(body = s"Client ${disconnectedClient.get.login} left.", receiverId = "", senderId = "SERVER")
+ leaveMessageForConnectedClients(msg)
+ }
+ logger.info(s"Client ${disconnectedClient.get.login} disconnected.")
+ }
+
+ private def sendDataToClient(key: SelectionKey) = {
+ val clientChannel = key.channel().asInstanceOf[SocketChannel]
+ val clientId = clientChannel.hashCode()
+ val client = connectedClients(clientId)
+ val clientInbox = client.inbox
+ if (clientInbox.nonEmpty)
+ try {
+ val messageToSend = clientInbox.removeHead()
+ client.lastMessageTimestamp = messageToSend.timestamp
+ client.saveState(savedStatesDir)
+ util.Util.send(messageToSend, clientChannel)
+ clientChannel.register(selector, SelectionKey.OP_READ)
+ } catch {
+ case e@(_: MismatchedInputException |
+ _: IOException) =>
+ handleDisconnection(clientChannel, clientId)
+ case e@(_: NotYetConnectedException |
+ _: NonReadableChannelException |
+ _: JsonProcessingException) =>
+ logger.error(e.getMessage, e.getCause)
+ }
+ }
+
+ private def routeMessage(msg: Message, senderId:Int) = {
+ msg.receiverId match {
+ case _ =>
+ for ((recieverId, reciever) <- connectedClients.iterator) {
+ if (recieverId != senderId) {
+ leaveMessageForClient(msg, reciever)
+ }
+ }
+ }
+ }
+
+ private def leaveMessageForConnectedClients(msg:Message) = {
+ for ((recieverId, reciever) <- connectedClients.iterator)
+ leaveMessageForClient(msg, reciever)
+ }
+
+ private def leaveMessageForClient(msg: Message, reciever: Client) = {
+ reciever.inbox.addOne(msg)
+ reciever.socketChannel.register(selector, SelectionKey.OP_WRITE)
+ }
+
+ def start(): Unit = {
+ isRunning = true
+ Runtime.getRuntime.addShutdownHook(new Thread() {
+ override def run(): Unit = {
+ if (isRunning) {
+ logger.info("Closing connections.")
+ isRunning = false
+ Thread.sleep(10)
+ for (c <- connectedClients.values) {
+ try c.socketChannel.close()
+ }
+ }
+ }
+ })
+ this.run()
+ }
+}
+
+object Server extends LazyLogging {
+ LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME).asInstanceOf[ch.qos.logback.classic.Logger].setLevel(Level.DEBUG)
+
+ val defaultArgs = Map(
+ "host" -> "0.0.0.0",
+ "port" -> "10000",
+ "states_path" -> "./clientStates/"
+ )
+
+ // how socket connection and key.isConnectable are related?
+ // how socket connection and socket.isConnected are related?
+ def main(args:Array[String]): Unit = {
+ val argsMap = args.toSeq.sliding(2,2).map{ case ArraySeq(k,v) => (k.replace("--",""), v) }.toMap
+ .withDefault(defaultArgs)
+ val host = argsMap("host")
+ val port = argsMap("port").toInt
+ val statesPath = Path.of(argsMap("statesPath")).toAbsolutePath
+
+ if (Files.notExists(statesPath)) {
+ logger.info(s"Path statesPath = ${statesPath} does not exist. Aborting.")
+ System.exit(0)
+ }
+
+ logger.info(s"Using ${statesPath} for client states.")
+
+ val server = new Server(host, port, statesPath)
+ server.start()
+ }
+}
\ No newline at end of file
diff --git a/examples/lab3/src/main/scala/util/SavableState.scala b/examples/lab3/src/main/scala/util/SavableState.scala
new file mode 100644
index 0000000..d7570ca
--- /dev/null
+++ b/examples/lab3/src/main/scala/util/SavableState.scala
@@ -0,0 +1,8 @@
+package util
+
+import java.nio.file.Path
+
+trait SavableState {
+ def loadState(statesDir:Path):Unit
+ def saveState(statesDir:Path):Unit
+}
diff --git a/examples/lab3/src/main/scala/util/Util.scala b/examples/lab3/src/main/scala/util/Util.scala
new file mode 100644
index 0000000..a41d4ea
--- /dev/null
+++ b/examples/lab3/src/main/scala/util/Util.scala
@@ -0,0 +1,81 @@
+package util
+
+import java.nio.ByteBuffer
+import java.nio.channels.SocketChannel
+import java.nio.charset.{Charset, StandardCharsets}
+import java.nio.file.{Files, Path, StandardOpenOption}
+import java.security.MessageDigest
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.scala.{DefaultScalaModule, ScalaObjectMapper}
+import com.typesafe.scalalogging.LazyLogging
+
+import scala.collection.mutable
+import scala.reflect.ClassTag
+
+object Util extends LazyLogging {
+ val BUFFER_SIZE = 1024
+ val ID_LIMIT = 2
+ val BROADCAST_RECIEVER_ID = "-1"
+
+ private val objectMapper = new ObjectMapper() with ScalaObjectMapper
+ objectMapper.registerModule(DefaultScalaModule)
+ private val charset = Charset.forName( "utf-8" )
+ private val decoder = charset.newDecoder
+
+ def recieveString(socketChannel:SocketChannel) : String = {
+ val stringBuilder = new mutable.StringBuilder()
+ val buffer = ByteBuffer.allocateDirect(BUFFER_SIZE)
+ while (true) {
+ val readBytes = socketChannel.read(buffer)
+ if ((readBytes == 0) || (readBytes == -1))
+ return stringBuilder.mkString
+ buffer.flip()
+ val charBuffer = decoder.decode( buffer )
+ stringBuilder.appendAll(charBuffer.array())
+ }
+ ""
+ }
+
+ def sendString(str: String, socketChannel:SocketChannel): Unit = {
+ logger.debug(s"Send: $str")
+ val buffer = ByteBuffer.wrap(str.getBytes())
+ socketChannel.write(buffer)
+ }
+
+ def recieve[T](sc:SocketChannel)(implicit ct: ClassTag[T]): Option[T] = {
+ val raw = recieveString(sc)
+ if ("".equals(raw)) return None
+ val v = objectMapper.readerFor(ct.runtimeClass).readValue[T](raw)
+ logger.debug(s"Recieve: ${v.toString}")
+ Some(v)
+ }
+
+ def send[T](x:T, sc:SocketChannel)(implicit ct: ClassTag[T]): Unit =
+ sendString(objectMapper.writerFor(ct.runtimeClass).writeValueAsString(x), sc)
+
+ def genId(s: Any): String = {
+ val md5 = java.security.MessageDigest.getInstance("MD5").digest(s.toString.getBytes)
+ md5.map("%02X" format _).take(ID_LIMIT).mkString
+ }
+
+ def writeJsonToFile[T](x:T, file:Path)(implicit ct: ClassTag[T]) = {
+ val json = objectMapper.writerFor(ct.runtimeClass).writeValueAsString(x)
+ Files.writeString(
+ file,
+ json,
+ StandardCharsets.UTF_8,
+ StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING)
+ }
+
+ def readJsonFromFile[T](file:Path)(implicit ct: ClassTag[T]):Option[T] = {
+ if (Files.exists(file)) {
+ val json = Files.readString(file)
+ val v = objectMapper.readerFor(ct.runtimeClass).readValue[T](json)
+ Some(v)
+ } else {
+ None
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/lab3/src/test/scala/SchemaCompliance.scala b/examples/lab3/src/test/scala/SchemaCompliance.scala
new file mode 100644
index 0000000..a90999c
--- /dev/null
+++ b/examples/lab3/src/test/scala/SchemaCompliance.scala
@@ -0,0 +1,26 @@
+import java.nio.file.Path
+
+import com.github.fge.jackson.JsonLoader
+import com.github.fge.jsonschema.main.JsonSchemaFactory
+import datatypes.ClientState
+import org.scalatest.FunSuite
+
+class SchemaCompliance extends FunSuite {
+ test("checkClientStateSchemeCompliance") {
+ val clientState = ClientState(lastMessageTimestamp = "2020-09-11T08:54:45.528807100Z")
+ val clientStateFilePath = Path.of("./target/checkClientStateSchemeCompliance.json")
+ val schemaPath = Path.of("./resources/schemas/v0.0.1/ClientStateSchema.json")
+
+ util.Util.writeJsonToFile(clientState, clientStateFilePath)
+ val jsonToValidate = JsonLoader.fromFile(clientStateFilePath.toFile)
+
+ val sf = JsonSchemaFactory.byDefault()
+ val schema = sf.getJsonSchema(JsonLoader.fromFile(schemaPath.toFile))
+
+ val report = schema.validate(jsonToValidate)
+ println(report)
+
+ assert(report.isSuccess)
+ }
+
+}