Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow parsing request and writing responses using java streams #41

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/main/kotlin/com/google/actions/api/ActionResponse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import com.google.api.services.actions_fulfillment.v2.model.AppResponse
import com.google.api.services.actions_fulfillment.v2.model.ExpectedIntent
import com.google.api.services.actions_fulfillment.v2.model.RichResponse
import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse
import java.io.IOException
import java.io.OutputStream

/**
* Defines requirements of an object that represents a response from the Actions
Expand Down Expand Up @@ -58,6 +60,16 @@ interface ActionResponse {
*/
val helperIntent: ExpectedIntent?

/**
* Writes the JSON representation of the response to the given output stream.
*
* This is more efficient than calling [toJson] first and then writing the string.
*
* @param outputStream The output stream to write to. Must be closed by the caller.
*/
@Throws(IOException::class)
fun writeTo(outputStream: OutputStream)

/**
* Returns the JSON representation of the response.
*/
Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/com/google/actions/api/ActionsSdkApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.google.actions.api
import com.google.actions.api.impl.AogRequest
import com.google.actions.api.response.ResponseBuilder
import org.slf4j.LoggerFactory
import java.io.InputStream

/**
* Implementation of App for ActionsSDK based webhook. Developers must extend
Expand Down Expand Up @@ -50,6 +51,11 @@ open class ActionsSdkApp : DefaultApp() {
return AogRequest.create(inputJson, headers)
}

override fun createRequest(inputStream: InputStream, headers: Map<*, *>?): ActionRequest {
LOG.info("ActionsSdkApp.createRequest..")
return AogRequest.create(inputStream, headers)
}

override fun getResponseBuilder(request: ActionRequest): ResponseBuilder {
val responseBuilder = ResponseBuilder(
usesDialogflow = false,
Expand Down
15 changes: 15 additions & 0 deletions src/main/kotlin/com/google/actions/api/DefaultApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.google.actions.api

import com.google.actions.api.response.ResponseBuilder
import org.slf4j.LoggerFactory
import java.io.InputStream
import java.util.concurrent.CompletableFuture

/**
Expand All @@ -42,6 +43,20 @@ abstract class DefaultApp : App {
abstract fun createRequest(inputJson: String, headers: Map<*, *>?):
ActionRequest

/**
* Creates an ActionRequest from the specified input stream and metadata.
*
* This is semantically equivalent to reading the stream as a String using
* UTF-8 encoding and then calling `createRequest` with the resulting
* string.
*
* @param inputStream The input stream. Must be closed by the caller
* @param headers Map containing metadata, usually from the HTTP request
* headers.
*/
abstract fun createRequest(inputStream: InputStream, headers: Map<*, *>?):
ActionRequest

/**
* @return A ResponseBuilder for this App.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/com/google/actions/api/DialogflowApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.google.actions.api

import com.google.actions.api.impl.DialogflowRequest
import com.google.actions.api.response.ResponseBuilder
import java.io.InputStream

/**
* Implementation of App for Dialogflow based webhook. Developers must extend
Expand Down Expand Up @@ -48,6 +49,10 @@ open class DialogflowApp : DefaultApp() {
return DialogflowRequest.create(inputJson, headers)
}

override fun createRequest(inputStream: InputStream, headers: Map<*, *>?): ActionRequest {
return DialogflowRequest.create(inputStream, headers)
}

override fun getResponseBuilder(request: ActionRequest): ResponseBuilder {
val responseBuilder = ResponseBuilder(
usesDialogflow = true,
Expand Down
306 changes: 163 additions & 143 deletions src/main/kotlin/com/google/actions/api/impl/AogRequest.kt

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions src/main/kotlin/com/google/actions/api/impl/AogResponse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.google.actions.api.response.ResponseBuilder
import com.google.api.services.actions_fulfillment.v2.model.*
import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse
import com.google.gson.Gson
import java.io.OutputStream
import java.util.*

internal class AogResponse internal constructor(
Expand Down Expand Up @@ -85,12 +86,12 @@ internal class AogResponse internal constructor(
if (conversationData != null) {
val dataMap = HashMap<String, Any?>()
dataMap["data"] = conversationData
appResponse?.conversationToken = Gson().toJson(dataMap)
appResponse?.conversationToken = gson.toJson(dataMap)
}
if (userStorage != null) {
val dataMap = HashMap<String, Any?>()
dataMap["data"] = userStorage
appResponse?.userStorage = Gson().toJson(dataMap)
appResponse?.userStorage = gson.toJson(dataMap)
}
}
}
Expand Down Expand Up @@ -132,7 +133,15 @@ internal class AogResponse internal constructor(
appResponse?.expectedInputs = expectedInputs
}

override fun writeTo(outputStream: OutputStream) {
ResponseSerializer(sessionId).writeJsonV2To(this, outputStream)
}

override fun toJson(): String {
return ResponseSerializer(sessionId).toJsonV2(this)
}

companion object {
private val gson = Gson()
}
}
57 changes: 32 additions & 25 deletions src/main/kotlin/com/google/actions/api/impl/DialogflowRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import com.google.api.services.actions_fulfillment.v2.model.*
import com.google.api.services.dialogflow_fulfillment.v2.model.*
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import java.io.InputStream
import java.io.InputStreamReader
import java.lang.reflect.Type
import java.util.*

Expand Down Expand Up @@ -168,37 +170,42 @@ internal class DialogflowRequest internal constructor(
}

companion object {

fun create(body: String, headers: Map<*, *>?): DialogflowRequest {
val gson = Gson()
return create(gson.fromJson(body, JsonObject::class.java), headers)
}

fun create(json: JsonObject, headers: Map<*, *>?): DialogflowRequest {
val gsonBuilder = GsonBuilder()
gsonBuilder
.registerTypeAdapter(WebhookRequest::class.java,
WebhookRequestDeserializer())
.registerTypeAdapter(QueryResult::class.java,
QueryResultDeserializer())
.registerTypeAdapter(Context::class.java,
ContextDeserializer())
.registerTypeAdapter(OriginalDetectIntentRequest::class.java,
OriginalDetectIntentRequestDeserializer())

val gson = gsonBuilder.create()
val webhookRequest = gson.fromJson<WebhookRequest>(json,
WebhookRequest::class.java)
val aogRequest: AogRequest
private val gson = GsonBuilder()
.registerTypeAdapter(WebhookRequest::class.java,
WebhookRequestDeserializer())
.registerTypeAdapter(QueryResult::class.java,
QueryResultDeserializer())
.registerTypeAdapter(Context::class.java,
ContextDeserializer())
.registerTypeAdapter(OriginalDetectIntentRequest::class.java,
OriginalDetectIntentRequestDeserializer())
.create()

fun create(body: String, headers: Map<*, *>?): DialogflowRequest =
create(gson.fromJson(body, WebhookRequest::class.java), headers)

fun create(json: JsonObject, headers: Map<*, *>?): DialogflowRequest =
create(gson.fromJson(json, WebhookRequest::class.java), headers)

fun create(inputStream: InputStream, headers: Map<*, *>?): DialogflowRequest =
create(
gson.fromJson(InputStreamReader(inputStream, Charsets.UTF_8), WebhookRequest::class.java),
headers
)

private fun create(
webhookRequest: WebhookRequest,
headers: Map<*, *>?
): DialogflowRequest {

val originalDetectIntentRequest =
webhookRequest.originalDetectIntentRequest
val payload = originalDetectIntentRequest?.payload
if (payload != null) {
aogRequest = AogRequest.create(gson.toJson(payload), headers,
val aogRequest = if (payload != null) {
AogRequest.create(gson.toJson(payload), headers,
partOfDialogflowRequest = true)
} else {
aogRequest = AogRequest.create(JsonObject(), headers,
AogRequest.create(JsonObject(), headers,
partOfDialogflowRequest = true)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.google.api.services.actions_fulfillment.v2.model.AppResponse
import com.google.api.services.actions_fulfillment.v2.model.ExpectedIntent
import com.google.api.services.actions_fulfillment.v2.model.RichResponse
import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse
import java.io.OutputStream

internal class DialogflowResponse internal constructor(
responseBuilder: ResponseBuilder) : ActionResponse {
Expand Down Expand Up @@ -59,6 +60,10 @@ internal class DialogflowResponse internal constructor(
override val helperIntent: ExpectedIntent?
get() = googlePayload?.helperIntent

override fun writeTo(outputStream: OutputStream) {
ResponseSerializer(sessionId).writeJsonV2To(this, outputStream)
}

override fun toJson(): String {
return ResponseSerializer(sessionId).toJsonV2(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import org.slf4j.LoggerFactory
import java.io.OutputStream
import java.io.OutputStreamWriter
import java.io.StringWriter
import java.io.Writer
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.set
Expand All @@ -36,6 +40,7 @@ internal class ResponseSerializer(
private companion object {
val includeVersionMetadata = false
val LOG = LoggerFactory.getLogger(ResponseSerializer::class.java.name)
val gson = GsonBuilder().create()

fun getLibraryMetadata(): Map<String, String> {
val metadataProperties = ResourceBundle.getBundle("metadata")
Expand All @@ -53,19 +58,27 @@ internal class ResponseSerializer(
)
}

fun toJsonV2(response: ActionResponse): String {
fun toJsonV2(response: ActionResponse): String =
StringWriter().use { writeJsonV2To(response, it) }.toString()

fun writeJsonV2To(response: ActionResponse, outputStream: OutputStream) {
val writer = OutputStreamWriter(outputStream, Charsets.UTF_8)
writeJsonV2To(response, writer)
writer.flush()
}

private fun writeJsonV2To(response: ActionResponse, writer: Writer) {
when (response) {
is DialogflowResponse -> return serializeDialogflowResponseV2(
response)
is AogResponse -> return serializeAogResponse(response)
is DialogflowResponse -> serializeDialogflowResponseV2(response, writer)
is AogResponse -> serializeAogResponse(response, writer)
}
LOG.warn("Unable to serialize the response.")
throw Exception("Unable to serialize the response")
}

private fun serializeDialogflowResponseV2(
dialogflowResponse: DialogflowResponse): String {
val gson = GsonBuilder().create()
dialogflowResponse: DialogflowResponse,
writer: Writer) {
val googlePayload = dialogflowResponse.googlePayload
val webhookResponse = dialogflowResponse.webhookResponse
val conversationData = dialogflowResponse.conversationData
Expand Down Expand Up @@ -97,7 +110,7 @@ internal class ResponseSerializer(
metadata["google_library"] = getLibraryMetadata()
webhookResponseMap["metadata"] = metadata
}
return gson.toJson(webhookResponseMap)
gson.toJson(webhookResponseMap, writer)
}

private fun setContext(
Expand Down Expand Up @@ -194,7 +207,7 @@ internal class ResponseSerializer(
if (userStorage != null) {
val dataMap = HashMap<String, Any?>()
dataMap["data"] = userStorage
this.userStorage = Gson().toJson(dataMap)
this.userStorage = gson.toJson(dataMap)
}
this.isSsml = false
}
Expand Down Expand Up @@ -228,7 +241,7 @@ internal class ResponseSerializer(
}

@Throws(Exception::class)
private fun serializeAogResponse(aogResponse: AogResponse): String {
private fun serializeAogResponse(aogResponse: AogResponse, writer: Writer) {
aogResponse.prepareAppResponse()
checkSimpleResponseIsPresent(aogResponse)
val appResponseMap = aogResponse.appResponse!!.toMutableMap()
Expand All @@ -239,7 +252,7 @@ internal class ResponseSerializer(
appResponseMap["ResponseMetadata"] = map
}

return Gson().toJson(appResponseMap)
gson.toJson(appResponseMap, writer)
}

@Throws(Exception::class)
Expand Down
32 changes: 26 additions & 6 deletions src/main/kotlin/com/google/actions/api/smarthome/SmartHomeApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.google.home.graph.v1.HomeGraphApiServiceProto
import io.grpc.ManagedChannelBuilder
import io.grpc.auth.MoreCallCredentials
import java.io.FileInputStream
import java.io.InputStream
import java.util.concurrent.CompletableFuture

abstract class SmartHomeApp : App {
Expand All @@ -49,6 +50,18 @@ abstract class SmartHomeApp : App {
return SmartHomeRequest.create(inputJson)
}

/**
* Builds a [SmartHomeRequest] object from an [InputStream].
*
* This is semantically equivalent as reading the input stream as an UTF-8 string and then calling createRequest
* with the resulting string.
*
* @param inputStream The input stream to read from. The stream must be closed by the caller.
* @return A parsed request object
*/
fun createRequest(inputStream: InputStream): SmartHomeRequest =
SmartHomeRequest.create(inputStream)

/**
* The intent handler for action.devices.SYNC that is implemented in your smart home Action
*
Expand Down Expand Up @@ -140,17 +153,24 @@ abstract class SmartHomeApp : App {

return try {
val request = createRequest(inputJson)
val response = routeRequest(request, headers)

val future: CompletableFuture<SmartHomeResponse> = CompletableFuture()
future.complete(response)
future.thenApply { this.getAsJson(it) }
.exceptionally { throwable -> throwable.message }
handleRequest(request, headers)
.thenApply { getAsJson(it) }
.exceptionally { throwable -> throwable.message }
} catch (e: Exception) {
handleError(e)
}
}

fun handleRequest(request: SmartHomeRequest, headers: Map<*, *>?): CompletableFuture<SmartHomeResponse> =
try {
val response = routeRequest(request, headers)
CompletableFuture.completedFuture(response)
} catch (e: Exception) {
CompletableFuture<SmartHomeResponse>()
.apply { completeExceptionally(e) }
}


@Throws(Exception::class)
private fun routeRequest(request: SmartHomeRequest, headers: Map<*, *>?): SmartHomeResponse {
when (request.javaClass) {
Expand Down
Loading