Add support for queued endpoints
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
package com.bartlomiejpluta.ttsserver.core.lua.lib
|
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 fi.iki.elonen.NanoHTTPD
|
||||||
import org.luaj.vm2.LuaValue
|
import org.luaj.vm2.LuaValue
|
||||||
import org.luaj.vm2.lib.TwoArgFunction
|
import org.luaj.vm2.lib.TwoArgFunction
|
||||||
@@ -9,13 +9,10 @@ class HTTPLibrary : TwoArgFunction() {
|
|||||||
override fun call(modname: LuaValue, env: LuaValue): LuaValue {
|
override fun call(modname: LuaValue, env: LuaValue): LuaValue {
|
||||||
val methods = LuaValue.tableOf()
|
val methods = LuaValue.tableOf()
|
||||||
val responses = LuaValue.tableOf()
|
val responses = LuaValue.tableOf()
|
||||||
val endpoints = LuaValue.tableOf()
|
|
||||||
NanoHTTPD.Method.values().forEach { methods.set(it.name, it.name) }
|
NanoHTTPD.Method.values().forEach { methods.set(it.name, it.name) }
|
||||||
NanoHTTPD.Response.Status.values().forEach { responses.set(it.name, it.requestStatus) }
|
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("Method", methods)
|
||||||
env.set("Response", responses)
|
env.set("Status", responses)
|
||||||
env.set("Endpoint", endpoints)
|
|
||||||
return methods
|
return methods
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,30 +3,48 @@ package com.bartlomiejpluta.ttsserver.core.lua.loader
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory
|
import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory
|
||||||
import com.bartlomiejpluta.ttsserver.core.web.endpoint.DefaultEndpoint
|
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 com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate
|
||||||
import fi.iki.elonen.NanoHTTPD.Method
|
import fi.iki.elonen.NanoHTTPD.Method
|
||||||
import org.luaj.vm2.LuaClosure
|
import org.luaj.vm2.LuaClosure
|
||||||
import org.luaj.vm2.LuaNil
|
import org.luaj.vm2.LuaNil
|
||||||
import org.luaj.vm2.LuaString
|
import org.luaj.vm2.LuaString
|
||||||
import org.luaj.vm2.LuaTable
|
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()
|
val scripts = context.getExternalFilesDir("Endpoints")?.listFiles() ?: emptyArray()
|
||||||
|
|
||||||
return scripts
|
return scripts
|
||||||
.map { sandboxFactory.createSandbox().loadfile(it.absolutePath).call() }
|
.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) }
|
.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),
|
uri = parseUri(luaTable),
|
||||||
method = parseMethod(luaTable),
|
method = parseMethod(luaTable),
|
||||||
type = parseType(luaTable),
|
|
||||||
accepts = parseAccepts(luaTable),
|
accepts = parseAccepts(luaTable),
|
||||||
consumer = parseConsumer(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")
|
private fun parseConsumer(luaTable: LuaTable) = luaTable.get("consumer")
|
||||||
.takeIf { it !is LuaNil }
|
.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")
|
?: throw IllegalArgumentException("'consumer' field is required")
|
||||||
|
|
||||||
private fun parseAccepts(luaTable: LuaTable) = luaTable.get("accepts")
|
private fun parseAccepts(luaTable: LuaTable) = luaTable.get("accepts")
|
||||||
.takeIf { it !is LuaNil }
|
.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()
|
?.tojstring()
|
||||||
?: "text/plain"
|
?: "text/plain"
|
||||||
|
|
||||||
private fun parseType(luaTable: LuaTable) = luaTable.get("type")
|
private fun parseQueued(luaTable: LuaTable) = luaTable.get("queued")
|
||||||
.takeIf { it !is LuaNil }
|
.takeIf { it !is LuaNil }
|
||||||
?.let { it as? LuaString ?: throw IllegalArgumentException("'type' must be of string type'") }
|
?.checkboolean()
|
||||||
?.let { EndpointType.valueOf(it.tojstring()) }
|
?: false
|
||||||
?: EndpointType.DEFAULT
|
|
||||||
|
|
||||||
private fun parseMethod(luaTable: LuaTable) = luaTable.get("method")
|
private fun parseMethod(luaTable: LuaTable) = luaTable.get("method")
|
||||||
.takeIf { it !is LuaNil }
|
.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()) }
|
?.let { Method.valueOf(it.tojstring()) }
|
||||||
?: Method.GET
|
?: Method.GET
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ class SonosQueue(
|
|||||||
private val preferences: SharedPreferences,
|
private val preferences: SharedPreferences,
|
||||||
private val networkUtil: NetworkUtil
|
private val networkUtil: NetworkUtil
|
||||||
) {
|
) {
|
||||||
private val queue: BlockingQueue<SonosDTO> = LinkedBlockingQueue()
|
val queue: BlockingQueue<SonosDTO> = LinkedBlockingQueue()
|
||||||
private var consumer: Thread? = null
|
private var consumer: Thread? = null
|
||||||
|
|
||||||
fun run() {
|
fun run() {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import java.io.FileInputStream
|
|||||||
|
|
||||||
class DefaultEndpoint(
|
class DefaultEndpoint(
|
||||||
private val uri: UriTemplate,
|
private val uri: UriTemplate,
|
||||||
private val type: EndpointType,
|
|
||||||
private val accepts: String,
|
private val accepts: String,
|
||||||
private val method: Method,
|
private val method: Method,
|
||||||
private val consumer: LuaClosure
|
private val consumer: LuaClosure
|
||||||
@@ -22,19 +21,18 @@ class DefaultEndpoint(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((session.headers["content-type"]?.let { it != accepts } != false)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
val matchingResult = uri.match(session.uri)
|
val matchingResult = uri.match(session.uri)
|
||||||
if (!matchingResult.matched) {
|
if (!matchingResult.matched) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val params = LuaValue.tableOf().also { params ->
|
val request = Request.of(extractBody(session), matchingResult.variables)
|
||||||
matchingResult.variables
|
|
||||||
.map { LuaValue.valueOf(it.key) to LuaValue.valueOf(it.value) }
|
|
||||||
.forEach { params.set(it.first, it.second) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val response = consumer.call(request.body, request.params).checktable()
|
||||||
val response = consumer.call(LuaValue.valueOf(extractBody(session)), params).checktable()
|
|
||||||
return parseResponse(response)
|
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.engine.TTSEngine
|
||||||
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatus
|
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatus
|
||||||
import com.bartlomiejpluta.ttsserver.core.web.endpoint.DefaultEndpoint
|
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.core.web.exception.WebException
|
||||||
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
|
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
|
||||||
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
|
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
|
||||||
@@ -24,8 +27,12 @@ class WebServer(
|
|||||||
private val preferences: SharedPreferences,
|
private val preferences: SharedPreferences,
|
||||||
private val tts: TTSEngine,
|
private val tts: TTSEngine,
|
||||||
private val sonos: SonosQueue,
|
private val sonos: SonosQueue,
|
||||||
private val endpoints: List<DefaultEndpoint>
|
private val endpoints: List<Endpoint>
|
||||||
) : NanoHTTPD(port) {
|
) : NanoHTTPD(port) {
|
||||||
|
private val queuedEndpoints: List<QueuedEndpoint> = endpoints
|
||||||
|
.map { it as? QueuedEndpoint }
|
||||||
|
.filterNotNull()
|
||||||
|
|
||||||
private val speakersSilenceSchedulerEnabled: Boolean
|
private val speakersSilenceSchedulerEnabled: Boolean
|
||||||
get() = preferences.getBoolean(PreferenceKey.ENABLE_SPEAKERS_SILENCE_SCHEDULER, false)
|
get() = preferences.getBoolean(PreferenceKey.ENABLE_SPEAKERS_SILENCE_SCHEDULER, false)
|
||||||
|
|
||||||
@@ -202,6 +209,8 @@ class WebServer(
|
|||||||
override fun start() {
|
override fun start() {
|
||||||
super.start()
|
super.start()
|
||||||
sonos.run()
|
sonos.run()
|
||||||
|
queuedEndpoints.forEach { it.runWorker() }
|
||||||
|
|
||||||
LocalBroadcastManager
|
LocalBroadcastManager
|
||||||
.getInstance(context)
|
.getInstance(context)
|
||||||
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {
|
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {
|
||||||
@@ -212,6 +221,8 @@ class WebServer(
|
|||||||
override fun stop() {
|
override fun stop() {
|
||||||
super.stop()
|
super.stop()
|
||||||
sonos.stop()
|
sonos.stop()
|
||||||
|
queuedEndpoints.forEach { it.stopWorker() }
|
||||||
|
|
||||||
LocalBroadcastManager
|
LocalBroadcastManager
|
||||||
.getInstance(context)
|
.getInstance(context)
|
||||||
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {
|
.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 com.bartlomiejpluta.ttsserver.core.web.exception.UriTemplateException
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
class UriTemplate private constructor(uri: String) {
|
class UriTemplate private constructor(val template: String) {
|
||||||
private val variables = mutableListOf<String>()
|
private val variables = mutableListOf<String>()
|
||||||
private var pattern: Pattern
|
private var pattern: Pattern
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ class UriTemplate private constructor(uri: String) {
|
|||||||
var variableBuilder: StringBuilder? = null
|
var variableBuilder: StringBuilder? = null
|
||||||
var isVariable = false
|
var isVariable = false
|
||||||
|
|
||||||
uri.forEachIndexed { index, char ->
|
template.forEachIndexed { index, char ->
|
||||||
when {
|
when {
|
||||||
char == '{' -> {
|
char == '{' -> {
|
||||||
if (isVariable) {
|
if (isVariable) {
|
||||||
|
|||||||
Reference in New Issue
Block a user