Add support for queued endpoints

This commit is contained in:
2020-07-02 11:06:48 +02:00
parent 549e0c162e
commit 5db58df705
10 changed files with 151 additions and 37 deletions

View File

@@ -1,6 +1,6 @@
package com.bartlomiejpluta.ttsserver.core.lua.lib
import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointType
import com.bartlomiejpluta.ttsserver.core.web.endpoint.ResponseType
import fi.iki.elonen.NanoHTTPD
import org.luaj.vm2.LuaValue
import org.luaj.vm2.lib.TwoArgFunction
@@ -9,13 +9,10 @@ class HTTPLibrary : TwoArgFunction() {
override fun call(modname: LuaValue, env: LuaValue): LuaValue {
val methods = LuaValue.tableOf()
val responses = LuaValue.tableOf()
val endpoints = LuaValue.tableOf()
NanoHTTPD.Method.values().forEach { methods.set(it.name, it.name) }
NanoHTTPD.Response.Status.values().forEach { responses.set(it.name, it.requestStatus) }
EndpointType.values().forEach { endpoints.set(it.name, it.name) }
env.set("Method", methods)
env.set("Response", responses)
env.set("Endpoint", endpoints)
env.set("Status", responses)
return methods
}

View File

@@ -3,30 +3,48 @@ package com.bartlomiejpluta.ttsserver.core.lua.loader
import android.content.Context
import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory
import com.bartlomiejpluta.ttsserver.core.web.endpoint.DefaultEndpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointType
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.QueuedEndpoint
import com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate
import fi.iki.elonen.NanoHTTPD.Method
import org.luaj.vm2.LuaClosure
import org.luaj.vm2.LuaNil
import org.luaj.vm2.LuaString
import org.luaj.vm2.LuaTable
import java.lang.IllegalArgumentException
import java.util.concurrent.ExecutorService
class EndpointLoader(private val context: Context, private val sandboxFactory: SandboxFactory) {
class EndpointLoader(
private val context: Context,
private val sandboxFactory: SandboxFactory
) {
fun loadEndpoints(): List<DefaultEndpoint> {
fun loadEndpoints(): List<Endpoint> {
val scripts = context.getExternalFilesDir("Endpoints")?.listFiles() ?: emptyArray()
return scripts
.map { sandboxFactory.createSandbox().loadfile(it.absolutePath).call() }
.map { it as? LuaTable ?: throw IllegalArgumentException("Expected single table to be returned") }
.map {
it as? LuaTable
?: throw IllegalArgumentException("Expected single table to be returned")
}
.map { createEndpoint(it) }
}
private fun createEndpoint(luaTable: LuaTable) = DefaultEndpoint(
private fun createEndpoint(luaTable: LuaTable) = when (parseQueued(luaTable)) {
false -> createDefaultEndpoint(luaTable)
true -> createQueuedEndpoint(luaTable)
}
private fun createDefaultEndpoint(luaTable: LuaTable): Endpoint = DefaultEndpoint(
uri = parseUri(luaTable),
method = parseMethod(luaTable),
accepts = parseAccepts(luaTable),
consumer = parseConsumer(luaTable)
)
private fun createQueuedEndpoint(luaTable: LuaTable): Endpoint = QueuedEndpoint(
uri = parseUri(luaTable),
method = parseMethod(luaTable),
type = parseType(luaTable),
accepts = parseAccepts(luaTable),
consumer = parseConsumer(luaTable)
)
@@ -40,24 +58,29 @@ class EndpointLoader(private val context: Context, private val sandboxFactory: S
private fun parseConsumer(luaTable: LuaTable) = luaTable.get("consumer")
.takeIf { it !is LuaNil }
?.let { it as? LuaClosure ?: throw IllegalArgumentException("'consumer' must be a function'") }
?.let {
it as? LuaClosure ?: throw IllegalArgumentException("'consumer' must be a function'")
}
?: throw IllegalArgumentException("'consumer' field is required")
private fun parseAccepts(luaTable: LuaTable) = luaTable.get("accepts")
.takeIf { it !is LuaNil }
?.let { it as? LuaString ?: throw IllegalArgumentException("'accepts' must be of string type'") }
?.let {
it as? LuaString ?: throw IllegalArgumentException("'accepts' must be of string type'")
}
?.tojstring()
?: "text/plain"
private fun parseType(luaTable: LuaTable) = luaTable.get("type")
private fun parseQueued(luaTable: LuaTable) = luaTable.get("queued")
.takeIf { it !is LuaNil }
?.let { it as? LuaString ?: throw IllegalArgumentException("'type' must be of string type'") }
?.let { EndpointType.valueOf(it.tojstring()) }
?: EndpointType.DEFAULT
?.checkboolean()
?: false
private fun parseMethod(luaTable: LuaTable) = luaTable.get("method")
.takeIf { it !is LuaNil }
?.let { it as? LuaString ?: throw IllegalArgumentException("'method' must be of string type'") }
?.let {
it as? LuaString ?: throw IllegalArgumentException("'method' must be of string type'")
}
?.let { Method.valueOf(it.tojstring()) }
?: Method.GET
}

View File

@@ -13,7 +13,7 @@ class SonosQueue(
private val preferences: SharedPreferences,
private val networkUtil: NetworkUtil
) {
private val queue: BlockingQueue<SonosDTO> = LinkedBlockingQueue()
val queue: BlockingQueue<SonosDTO> = LinkedBlockingQueue()
private var consumer: Thread? = null
fun run() {

View File

@@ -11,7 +11,6 @@ import java.io.FileInputStream
class DefaultEndpoint(
private val uri: UriTemplate,
private val type: EndpointType,
private val accepts: String,
private val method: Method,
private val consumer: LuaClosure
@@ -22,19 +21,18 @@ class DefaultEndpoint(
return null
}
if ((session.headers["content-type"]?.let { it != accepts } != false)) {
return null
}
val matchingResult = uri.match(session.uri)
if (!matchingResult.matched) {
return null
}
val params = LuaValue.tableOf().also { params ->
matchingResult.variables
.map { LuaValue.valueOf(it.key) to LuaValue.valueOf(it.value) }
.forEach { params.set(it.first, it.second) }
}
val request = Request.of(extractBody(session), matchingResult.variables)
val response = consumer.call(LuaValue.valueOf(extractBody(session)), params).checktable()
val response = consumer.call(request.body, request.params).checktable()
return parseResponse(response)
}

View File

@@ -1,6 +0,0 @@
package com.bartlomiejpluta.ttsserver.core.web.endpoint
enum class EndpointType {
DEFAULT,
QUEUE
}

View File

@@ -0,0 +1,52 @@
package com.bartlomiejpluta.ttsserver.core.web.endpoint
import com.bartlomiejpluta.ttsserver.core.sonos.worker.SonosWorker
import com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate
import fi.iki.elonen.NanoHTTPD
import fi.iki.elonen.NanoHTTPD.newFixedLengthResponse
import org.luaj.vm2.LuaClosure
import java.util.concurrent.BlockingQueue
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
class QueuedEndpoint(
private val uri: UriTemplate,
private val accepts: String,
private val method: NanoHTTPD.Method,
consumer: LuaClosure
) : Endpoint {
private val queue = LinkedBlockingQueue<Request>()
private val worker = Thread(Worker(queue, consumer)).also { it.name = uri.template }
override fun hit(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response? {
if (session.method != method) {
return null
}
if ((session.headers["content-type"]?.let { it != accepts } != false)) {
return null
}
val matchingResult = uri.match(session.uri)
if (!matchingResult.matched) {
return null
}
val request = Request.of(extractBody(session), matchingResult.variables)
queue.add(request)
return newFixedLengthResponse(NanoHTTPD.Response.Status.ACCEPTED, "text/plain", "")
}
private fun extractBody(session: NanoHTTPD.IHTTPSession): String {
return mutableMapOf<String, String>().let {
session.parseBody(it)
it["postData"] ?: ""
}
}
fun runWorker() = worker.start()
fun stopWorker() = worker.interrupt()
}

View File

@@ -0,0 +1,16 @@
package com.bartlomiejpluta.ttsserver.core.web.endpoint
import org.luaj.vm2.LuaValue
class Request private constructor(rawBody: String, paramsMap: Map<String, String>) {
val body = LuaValue.valueOf(rawBody)
val params = LuaValue.tableOf().also { params ->
paramsMap
.map { LuaValue.valueOf(it.key) to LuaValue.valueOf(it.value) }
.forEach { params.set(it.first, it.second) }
}
companion object {
fun of(body: String, params: Map<String, String>) = Request(body, params)
}
}

View File

@@ -0,0 +1,23 @@
package com.bartlomiejpluta.ttsserver.core.web.endpoint
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
import org.luaj.vm2.LuaClosure
import java.util.concurrent.BlockingQueue
class Worker(
private val queue: BlockingQueue<Request>,
private val consumer: LuaClosure
) : Runnable {
override fun run() = try {
while (ForegroundService.state == ServiceState.RUNNING) {
consume(queue.take())
}
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
private fun consume(request: Request) {
consumer.call(request.body, request.params)
}
}

View File

@@ -8,6 +8,9 @@ import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatus
import com.bartlomiejpluta.ttsserver.core.web.endpoint.DefaultEndpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.QueuedEndpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Worker
import com.bartlomiejpluta.ttsserver.core.web.exception.WebException
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
@@ -24,8 +27,12 @@ class WebServer(
private val preferences: SharedPreferences,
private val tts: TTSEngine,
private val sonos: SonosQueue,
private val endpoints: List<DefaultEndpoint>
private val endpoints: List<Endpoint>
) : NanoHTTPD(port) {
private val queuedEndpoints: List<QueuedEndpoint> = endpoints
.map { it as? QueuedEndpoint }
.filterNotNull()
private val speakersSilenceSchedulerEnabled: Boolean
get() = preferences.getBoolean(PreferenceKey.ENABLE_SPEAKERS_SILENCE_SCHEDULER, false)
@@ -202,6 +209,8 @@ class WebServer(
override fun start() {
super.start()
sonos.run()
queuedEndpoints.forEach { it.runWorker() }
LocalBroadcastManager
.getInstance(context)
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {
@@ -212,6 +221,8 @@ class WebServer(
override fun stop() {
super.stop()
sonos.stop()
queuedEndpoints.forEach { it.stopWorker() }
LocalBroadcastManager
.getInstance(context)
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {

View File

@@ -3,7 +3,7 @@ package com.bartlomiejpluta.ttsserver.core.web.uri
import com.bartlomiejpluta.ttsserver.core.web.exception.UriTemplateException
import java.util.regex.Pattern
class UriTemplate private constructor(uri: String) {
class UriTemplate private constructor(val template: String) {
private val variables = mutableListOf<String>()
private var pattern: Pattern
@@ -12,7 +12,7 @@ class UriTemplate private constructor(uri: String) {
var variableBuilder: StringBuilder? = null
var isVariable = false
uri.forEachIndexed { index, char ->
template.forEachIndexed { index, char ->
when {
char == '{' -> {
if (isVariable) {