Coverage Summary for Class: StaticContent (it.unibo.tuprolog.argumentation.parallel.federation)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| StaticContent |
100%
(1/1)
|
100%
(1/1)
|
|
100%
(3/3)
|
100%
(12/12)
|
package it.unibo.tuprolog.argumentation.parallel.federation
import org.java_websocket.WebSocket
import org.java_websocket.WebSocketImpl
import org.java_websocket.client.WebSocketClient
import org.java_websocket.drafts.Draft
import org.java_websocket.drafts.Draft_6455
import org.java_websocket.enums.HandshakeState
import org.java_websocket.framing.CloseFrame
import org.java_websocket.handshake.ClientHandshake
import org.java_websocket.handshake.HandshakeBuilder
import org.java_websocket.handshake.Handshakedata
import org.java_websocket.handshake.ServerHandshake
import org.java_websocket.handshake.ServerHandshakeBuilder
import org.java_websocket.server.WebSocketServer
import java.net.InetSocketAddress
import java.net.URI
import java.nio.ByteBuffer
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* The links of [site] over WebSockets, to sites on any platform (JVM, Node.js, browsers): the links it accepts once
* it [listen]s, and those it opens ([connect]). Each frame is a text message; those of a link arrive in order, and one
* longer than a few tens of megabytes closes the link. The port listened on answers the plain HTTP requests for the
* [pages] it serves as well. The failures of a link, and those of the handling of its frames, are reported on the
* standard error.
*/
class WebSocketLinks(
private val site: LinkListener,
) : AutoCloseable {
private var server: Server? = null
private val clients = ConcurrentHashMap.newKeySet<Client>()
/**
* The page a plain HTTP GET of a path (as `/join?x=1`) is answered with, on the port listened on: none (null) for
* a request refused (404), as for every other request but those opening a WebSocket. Asked as each request comes.
*/
@Volatile var pages: (path: String) -> StaticContent? = { null }
/** Accepts the links of other sites on [port] of [host] (an ephemeral port for 0): the port taken */
fun listen(
port: Int = 0,
host: String = "0.0.0.0",
): Int {
check(server == null) { "Already listening" }
val started = Server(InetSocketAddress(host, port)).also { it.start() }
started.started.await()
started.failure?.let { throw it }
server = started
return started.port
}
/**
* Opens a link to the site listening at [uri] (ws://host:port), waiting for it at most [timeoutMillis]: [code]
* presented if given (see [Site.requiresCode]), and the link given to [opening] before it opens
*/
@JvmOverloads
fun connect(
uri: String,
timeoutMillis: Long = 10_000,
code: String? = null,
opening: (Link) -> Unit = {},
) {
val client = Client(URI(uri), code)
clients += client
opening(client.link)
// opened, if closed at once since (a site refusing it)
check(client.connectBlocking(timeoutMillis, TimeUnit.MILLISECONDS) || client.opened) { "No site at $uri" }
}
/** Closes every link, and stops listening */
override fun close() {
clients.forEach { it.closeBlocking() }
server?.stop()
server = null
}
// A link on an open WebSocket: sends from several threads, one at a time
private class SocketLink(
private val socket: WebSocket,
override val outgoing: Boolean = false,
override val code: String? = null,
) : Link {
override val address: String? get() = socket.remoteSocketAddress?.address?.hostAddress
override fun send(frame: String) {
synchronized(this) { if (socket.isOpen) socket.send(frame) }
}
override fun close() = socket.close()
}
// The plain HTTP GETs of the pages served, answered as a WebSocket handshake would be, by another draft: the
// requests opening a WebSocket are left to the draft of the WebSockets
private inner class Pages(
private var page: StaticContent? = null,
) : Draft_6455(emptyList(), LONGEST_FRAME) {
override fun acceptHandshakeAsServer(handshake: ClientHandshake): HandshakeState {
// a WebSocket, unlike other upgrades (h2c), which are ignored
if (handshake.getFieldValue("Upgrade").equals("websocket", ignoreCase = true)) return HandshakeState.NOT_MATCHED
page = pages(handshake.resourceDescriptor)
return if (page == null) HandshakeState.NOT_MATCHED else HandshakeState.MATCHED
}
override fun postProcessHandshakeResponseAsServer(
request: ClientHandshake,
response: ServerHandshakeBuilder,
): HandshakeBuilder =
response.apply {
val served = page!!
put("Content-Type", served.contentType)
put("Content-Length", served.content.size.toString())
put("Cache-Control", "no-store")
put("Connection", "close")
content = served.content
}
// the response of a page, rather than the one switching to a WebSocket
override fun createHandshake(
handshake: Handshakedata,
withContent: Boolean,
): List<ByteBuffer> {
val head =
handshake.iterateHttpFields().asSequence().joinToString("") { "$it: ${handshake.getFieldValue(it)}\r\n" }
return listOf(ByteBuffer.wrap("HTTP/1.1 200 OK\r\n$head\r\n".encodeToByteArray() + handshake.content))
}
override fun copyInstance(): Draft = Pages()
}
private inner class Server(
address: InetSocketAddress,
) : WebSocketServer(address, listOf(Pages(), Draft_6455(emptyList(), LONGEST_FRAME))) {
val started = CountDownLatch(1)
var failure: Exception? = null
private val links = ConcurrentHashMap<WebSocket, SocketLink>()
init {
isReuseAddr = true
}
override fun onStart() = started.countDown()
// a page served: the connection closed once it is written
override fun onOpen(
socket: WebSocket,
handshake: ClientHandshake,
) = if (socket.draft is Pages) {
(socket as WebSocketImpl).flushAndClose(CloseFrame.NORMAL, "", false)
} else {
site.opened(SocketLink(socket).also { links[socket] = it })
}
override fun onMessage(
socket: WebSocket,
message: String,
) = links[socket]?.let { handled { site.received(it, message) } } ?: Unit
override fun onClose(
socket: WebSocket,
code: Int,
reason: String,
remote: Boolean,
) = links.remove(socket)?.let { site.closed(it) } ?: Unit
override fun onError(
socket: WebSocket?,
error: Exception,
) {
// an error on a link closes it (onClose follows), one of the server stops it
if (socket == null) {
failure = error
started.countDown()
} else {
report(error)
}
}
}
private inner class Client(
uri: URI,
code: String?,
) : WebSocketClient(uri, Draft_6455(emptyList(), LONGEST_FRAME)) {
val link: Link = SocketLink(this, outgoing = true, code = code)
@Volatile var opened = false
override fun onOpen(handshake: ServerHandshake) {
opened = true
site.opened(link)
}
override fun onMessage(message: String) = handled { site.received(link, message) }
override fun onClose(
code: Int,
reason: String,
remote: Boolean,
) {
clients -= this
site.closed(link)
}
// an error closes the link (onClose follows); not one of a site gone, or not there
override fun onError(error: Exception) {
if (opened) report(error)
}
}
}
/** A page served over plain HTTP: its [content], of the media type [contentType] (as `text/html; charset=utf-8`) */
class StaticContent(
val content: ByteArray,
val contentType: String,
)