Skip to content

Commit 968dff7

Browse files
committed
Allow parsing request and writing responses using java streams
This fixes actions-on-google#39
1 parent 7f3b1e2 commit 968dff7

15 files changed

+700
-230
lines changed

src/main/kotlin/com/google/actions/api/ActionResponse.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import com.google.api.services.actions_fulfillment.v2.model.AppResponse
2020
import com.google.api.services.actions_fulfillment.v2.model.ExpectedIntent
2121
import com.google.api.services.actions_fulfillment.v2.model.RichResponse
2222
import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse
23+
import java.io.IOException
24+
import java.io.OutputStream
2325

2426
/**
2527
* Defines requirements of an object that represents a response from the Actions
@@ -58,6 +60,16 @@ interface ActionResponse {
5860
*/
5961
val helperIntent: ExpectedIntent?
6062

63+
/**
64+
* Writes the JSON representation of the response to the given output stream.
65+
*
66+
* This is more efficient than calling [toJson] first and then writing the string.
67+
*
68+
* @param outputStream The output stream to write to. Must be closed by the caller.
69+
*/
70+
@Throws(IOException::class)
71+
fun writeTo(outputStream: OutputStream)
72+
6173
/**
6274
* Returns the JSON representation of the response.
6375
*/

src/main/kotlin/com/google/actions/api/ActionsSdkApp.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.google.actions.api
1919
import com.google.actions.api.impl.AogRequest
2020
import com.google.actions.api.response.ResponseBuilder
2121
import org.slf4j.LoggerFactory
22+
import java.io.InputStream
2223

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

54+
override fun createRequest(inputStream: InputStream, headers: Map<*, *>?): ActionRequest {
55+
LOG.info("ActionsSdkApp.createRequest..")
56+
return AogRequest.create(inputStream, headers)
57+
}
58+
5359
override fun getResponseBuilder(request: ActionRequest): ResponseBuilder {
5460
val responseBuilder = ResponseBuilder(
5561
usesDialogflow = false,

src/main/kotlin/com/google/actions/api/DefaultApp.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.google.actions.api
1818

1919
import com.google.actions.api.response.ResponseBuilder
2020
import org.slf4j.LoggerFactory
21+
import java.io.InputStream
2122
import java.util.concurrent.CompletableFuture
2223

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

46+
/**
47+
* Creates an ActionRequest from the specified input stream and metadata.
48+
*
49+
* This is semantically equivalent to reading the stream as a String using
50+
* UTF-8 encoding and then calling `createRequest` with the resulting
51+
* string.
52+
*
53+
* @param inputStream The input stream. Must be closed by the caller
54+
* @param headers Map containing metadata, usually from the HTTP request
55+
* headers.
56+
*/
57+
abstract fun createRequest(inputStream: InputStream, headers: Map<*, *>?):
58+
ActionRequest
59+
4560
/**
4661
* @return A ResponseBuilder for this App.
4762
*/

src/main/kotlin/com/google/actions/api/DialogflowApp.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.google.actions.api
1818

1919
import com.google.actions.api.impl.DialogflowRequest
2020
import com.google.actions.api.response.ResponseBuilder
21+
import java.io.InputStream
2122

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

52+
override fun createRequest(inputStream: InputStream, headers: Map<*, *>?): ActionRequest {
53+
return DialogflowRequest.create(inputStream, headers)
54+
}
55+
5156
override fun getResponseBuilder(request: ActionRequest): ResponseBuilder {
5257
val responseBuilder = ResponseBuilder(
5358
usesDialogflow = true,

src/main/kotlin/com/google/actions/api/impl/AogRequest.kt

Lines changed: 315 additions & 148 deletions
Large diffs are not rendered by default.

src/main/kotlin/com/google/actions/api/impl/AogResponse.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.google.actions.api.response.ResponseBuilder
2222
import com.google.api.services.actions_fulfillment.v2.model.*
2323
import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse
2424
import com.google.gson.Gson
25+
import java.io.OutputStream
2526
import java.util.*
2627

2728
internal class AogResponse internal constructor(
@@ -85,12 +86,12 @@ internal class AogResponse internal constructor(
8586
if (conversationData != null) {
8687
val dataMap = HashMap<String, Any?>()
8788
dataMap["data"] = conversationData
88-
appResponse?.conversationToken = Gson().toJson(dataMap)
89+
appResponse?.conversationToken = gson.toJson(dataMap)
8990
}
9091
if (userStorage != null) {
9192
val dataMap = HashMap<String, Any?>()
9293
dataMap["data"] = userStorage
93-
appResponse?.userStorage = Gson().toJson(dataMap)
94+
appResponse?.userStorage = gson.toJson(dataMap)
9495
}
9596
}
9697
}
@@ -132,7 +133,15 @@ internal class AogResponse internal constructor(
132133
appResponse?.expectedInputs = expectedInputs
133134
}
134135

136+
override fun writeTo(outputStream: OutputStream) {
137+
ResponseSerializer(sessionId).writeJsonV2To(this, outputStream)
138+
}
139+
135140
override fun toJson(): String {
136141
return ResponseSerializer(sessionId).toJsonV2(this)
137142
}
143+
144+
companion object {
145+
private val gson = Gson()
146+
}
138147
}

src/main/kotlin/com/google/actions/api/impl/DialogflowRequest.kt

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import com.google.api.services.actions_fulfillment.v2.model.*
2323
import com.google.api.services.dialogflow_fulfillment.v2.model.*
2424
import com.google.gson.*
2525
import com.google.gson.reflect.TypeToken
26+
import java.io.InputStream
27+
import java.io.InputStreamReader
2628
import java.lang.reflect.Type
2729
import java.util.*
2830

@@ -168,38 +170,43 @@ internal class DialogflowRequest internal constructor(
168170
}
169171

170172
companion object {
171-
172-
fun create(body: String, headers: Map<*, *>?): DialogflowRequest {
173-
val gson = Gson()
174-
return create(gson.fromJson(body, JsonObject::class.java), headers)
175-
}
176-
177-
fun create(json: JsonObject, headers: Map<*, *>?): DialogflowRequest {
178-
val gsonBuilder = GsonBuilder()
179-
gsonBuilder
180-
.registerTypeAdapter(WebhookRequest::class.java,
181-
WebhookRequestDeserializer())
182-
.registerTypeAdapter(QueryResult::class.java,
183-
QueryResultDeserializer())
184-
.registerTypeAdapter(Context::class.java,
185-
ContextDeserializer())
186-
.registerTypeAdapter(OriginalDetectIntentRequest::class.java,
187-
OriginalDetectIntentRequestDeserializer())
188-
189-
val gson = gsonBuilder.create()
190-
val webhookRequest = gson.fromJson<WebhookRequest>(json,
191-
WebhookRequest::class.java)
192-
val aogRequest: AogRequest
173+
private val gson = GsonBuilder()
174+
.registerTypeAdapter(WebhookRequest::class.java,
175+
WebhookRequestDeserializer())
176+
.registerTypeAdapter(QueryResult::class.java,
177+
QueryResultDeserializer())
178+
.registerTypeAdapter(Context::class.java,
179+
ContextDeserializer())
180+
.registerTypeAdapter(OriginalDetectIntentRequest::class.java,
181+
OriginalDetectIntentRequestDeserializer())
182+
.create()
183+
184+
fun create(body: String, headers: Map<*, *>?): DialogflowRequest =
185+
create(gson.fromJson(body, WebhookRequest::class.java), headers)
186+
187+
fun create(json: JsonObject, headers: Map<*, *>?): DialogflowRequest =
188+
create(gson.fromJson(json, WebhookRequest::class.java), headers)
189+
190+
fun create(inputStream: InputStream, headers: Map<*, *>?): DialogflowRequest =
191+
create(
192+
gson.fromJson(InputStreamReader(inputStream), WebhookRequest::class.java),
193+
headers
194+
)
195+
196+
private fun create(
197+
webhookRequest: WebhookRequest,
198+
headers: Map<*, *>?
199+
): DialogflowRequest {
193200

194201
val originalDetectIntentRequest =
195-
webhookRequest.originalDetectIntentRequest
202+
webhookRequest.originalDetectIntentRequest
196203
val payload = originalDetectIntentRequest?.payload
197-
if (payload != null) {
198-
aogRequest = AogRequest.create(gson.toJson(payload), headers,
199-
partOfDialogflowRequest = true)
204+
val aogRequest = if (payload != null) {
205+
AogRequest.create(gson.toJson(payload), headers,
206+
partOfDialogflowRequest = true)
200207
} else {
201-
aogRequest = AogRequest.create(JsonObject(), headers,
202-
partOfDialogflowRequest = true)
208+
AogRequest.create(JsonObject(), headers,
209+
partOfDialogflowRequest = true)
203210
}
204211

205212
return DialogflowRequest(webhookRequest, aogRequest)

src/main/kotlin/com/google/actions/api/impl/DialogflowResponse.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.google.api.services.actions_fulfillment.v2.model.AppResponse
2525
import com.google.api.services.actions_fulfillment.v2.model.ExpectedIntent
2626
import com.google.api.services.actions_fulfillment.v2.model.RichResponse
2727
import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse
28+
import java.io.OutputStream
2829

2930
internal class DialogflowResponse internal constructor(
3031
responseBuilder: ResponseBuilder) : ActionResponse {
@@ -59,6 +60,10 @@ internal class DialogflowResponse internal constructor(
5960
override val helperIntent: ExpectedIntent?
6061
get() = googlePayload?.helperIntent
6162

63+
override fun writeTo(outputStream: OutputStream) {
64+
ResponseSerializer(sessionId).writeJsonV2To(this, outputStream)
65+
}
66+
6267
override fun toJson(): String {
6368
return ResponseSerializer(sessionId).toJsonV2(this)
6469
}

src/main/kotlin/com/google/actions/api/impl/io/ResponseSerializer.kt

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import com.google.api.services.dialogflow_fulfillment.v2.model.WebhookResponse
2626
import com.google.gson.Gson
2727
import com.google.gson.GsonBuilder
2828
import org.slf4j.LoggerFactory
29+
import java.io.OutputStream
30+
import java.io.OutputStreamWriter
31+
import java.io.StringWriter
32+
import java.io.Writer
2933
import java.util.*
3034
import kotlin.collections.ArrayList
3135
import kotlin.collections.set
@@ -36,6 +40,7 @@ internal class ResponseSerializer(
3640
private companion object {
3741
val includeVersionMetadata = false
3842
val LOG = LoggerFactory.getLogger(ResponseSerializer::class.java.name)
43+
val gson = GsonBuilder().create()
3944

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

56-
fun toJsonV2(response: ActionResponse): String {
61+
fun toJsonV2(response: ActionResponse): String =
62+
StringWriter().use { writeJsonV2To(response, it) }.toString()
63+
64+
fun writeJsonV2To(response: ActionResponse, outputStream: OutputStream) {
65+
val writer = OutputStreamWriter(outputStream, Charsets.UTF_8)
66+
writeJsonV2To(response, writer)
67+
writer.flush()
68+
}
69+
70+
private fun writeJsonV2To(response: ActionResponse, writer: Writer) {
5771
when (response) {
58-
is DialogflowResponse -> return serializeDialogflowResponseV2(
59-
response)
60-
is AogResponse -> return serializeAogResponse(response)
72+
is DialogflowResponse -> serializeDialogflowResponseV2(response, writer)
73+
is AogResponse -> serializeAogResponse(response, writer)
6174
}
6275
LOG.warn("Unable to serialize the response.")
6376
throw Exception("Unable to serialize the response")
6477
}
6578

6679
private fun serializeDialogflowResponseV2(
67-
dialogflowResponse: DialogflowResponse): String {
68-
val gson = GsonBuilder().create()
80+
dialogflowResponse: DialogflowResponse,
81+
writer: Writer) {
6982
val googlePayload = dialogflowResponse.googlePayload
7083
val webhookResponse = dialogflowResponse.webhookResponse
7184
val conversationData = dialogflowResponse.conversationData
@@ -97,7 +110,7 @@ internal class ResponseSerializer(
97110
metadata["google_library"] = getLibraryMetadata()
98111
webhookResponseMap["metadata"] = metadata
99112
}
100-
return gson.toJson(webhookResponseMap)
113+
gson.toJson(webhookResponseMap, writer)
101114
}
102115

103116
private fun setContext(
@@ -194,7 +207,7 @@ internal class ResponseSerializer(
194207
if (userStorage != null) {
195208
val dataMap = HashMap<String, Any?>()
196209
dataMap["data"] = userStorage
197-
this.userStorage = Gson().toJson(dataMap)
210+
this.userStorage = gson.toJson(dataMap)
198211
}
199212
this.isSsml = false
200213
}
@@ -228,7 +241,7 @@ internal class ResponseSerializer(
228241
}
229242

230243
@Throws(Exception::class)
231-
private fun serializeAogResponse(aogResponse: AogResponse): String {
244+
private fun serializeAogResponse(aogResponse: AogResponse, writer: Writer) {
232245
aogResponse.prepareAppResponse()
233246
checkSimpleResponseIsPresent(aogResponse)
234247
val appResponseMap = aogResponse.appResponse!!.toMutableMap()
@@ -239,7 +252,7 @@ internal class ResponseSerializer(
239252
appResponseMap["ResponseMetadata"] = map
240253
}
241254

242-
return Gson().toJson(appResponseMap)
255+
gson.toJson(appResponseMap, writer)
243256
}
244257

245258
@Throws(Exception::class)

src/main/kotlin/com/google/actions/api/smarthome/SmartHomeApp.kt

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.google.home.graph.v1.HomeGraphApiServiceProto
2323
import io.grpc.ManagedChannelBuilder
2424
import io.grpc.auth.MoreCallCredentials
2525
import java.io.FileInputStream
26+
import java.io.InputStream
2627
import java.util.concurrent.CompletableFuture
2728

2829
abstract class SmartHomeApp : App {
@@ -49,6 +50,18 @@ abstract class SmartHomeApp : App {
4950
return SmartHomeRequest.create(inputJson)
5051
}
5152

53+
/**
54+
* Builds a [SmartHomeRequest] object from an [InputStream].
55+
*
56+
* This is semantically equivalent as reading the input stream as an UTF-8 string and then calling createRequest
57+
* with the resulting string.
58+
*
59+
* @param inputStream The input stream to read from. The stream must be closed by the caller.
60+
* @return A parsed request object
61+
*/
62+
fun createRequest(inputStream: InputStream): SmartHomeRequest =
63+
SmartHomeRequest.create(inputStream)
64+
5265
/**
5366
* The intent handler for action.devices.SYNC that is implemented in your smart home Action
5467
*
@@ -140,17 +153,24 @@ abstract class SmartHomeApp : App {
140153

141154
return try {
142155
val request = createRequest(inputJson)
143-
val response = routeRequest(request, headers)
144-
145-
val future: CompletableFuture<SmartHomeResponse> = CompletableFuture()
146-
future.complete(response)
147-
future.thenApply { this.getAsJson(it) }
148-
.exceptionally { throwable -> throwable.message }
156+
handleRequest(request, headers)
157+
.thenApply { getAsJson(it) }
158+
.exceptionally { throwable -> throwable.message }
149159
} catch (e: Exception) {
150160
handleError(e)
151161
}
152162
}
153163

164+
fun handleRequest(request: SmartHomeRequest, headers: Map<*, *>?): CompletableFuture<SmartHomeResponse> =
165+
try {
166+
val response = routeRequest(request, headers)
167+
CompletableFuture.completedFuture(response)
168+
} catch (e: Exception) {
169+
CompletableFuture<SmartHomeResponse>()
170+
.apply { completeExceptionally(e) }
171+
}
172+
173+
154174
@Throws(Exception::class)
155175
private fun routeRequest(request: SmartHomeRequest, headers: Map<*, *>?): SmartHomeResponse {
156176
when (request.javaClass) {

0 commit comments

Comments
 (0)