Add support for queued endpoints
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.bartlomiejpluta.ttsserver.core.web.endpoint
|
||||
|
||||
enum class EndpointType {
|
||||
DEFAULT,
|
||||
QUEUE
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user