Skip to content

Commit 5357dda

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

File tree

11 files changed

+660
-215
lines changed

11 files changed

+660
-215
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 & 147 deletions
Large diffs are not rendered by default.

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/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) {

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616

1717
package com.google.actions.api.smarthome
1818

19+
import com.google.gson.JsonObject
1920
import org.json.JSONObject
21+
import org.json.JSONTokener
22+
import java.io.IOException
23+
import java.io.InputStream
2024

2125
/**
2226
* A representation of the JSON payload received during a smart home request.
@@ -32,8 +36,13 @@ open class SmartHomeRequest {
3236
}
3337

3438
companion object {
35-
fun create(inputJson: String): SmartHomeRequest {
36-
val json = JSONObject(inputJson)
39+
fun create(inputStream: InputStream): SmartHomeRequest =
40+
create(JSONObject(JSONTokener(inputStream)))
41+
42+
fun create(inputJson: String): SmartHomeRequest =
43+
create(JSONObject(inputJson))
44+
45+
private fun create(json: JSONObject): SmartHomeRequest {
3746
val requestId = json.getString("requestId")
3847
val inputs = json.getJSONArray("inputs")
3948
val request = inputs.getJSONObject(0)

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,25 @@ import com.google.protobuf.Struct
2222
import com.google.protobuf.util.JsonFormat
2323
import org.json.JSONException
2424
import org.json.JSONObject
25+
import org.json.JSONStringer
26+
import org.json.JSONTokener
27+
import org.json.JSONWriter
28+
import java.io.OutputStream
29+
import java.io.OutputStreamWriter
30+
import java.io.StringWriter
2531

2632
/**
2733
* A representation of the JSON payload that should be sent during a smart home request.
2834
*
2935
* @see <a href="https://developers.google.com/actions/smarthome/develop/process-intents">Public documentation</a>
3036
*/
3137
open class SmartHomeResponse {
38+
open fun writeToStream(outputStream: OutputStream) {
39+
val writer = OutputStreamWriter(outputStream)
40+
build().write(writer)
41+
writer.flush()
42+
}
43+
3244
open fun build(): JSONObject {
3345
return JSONObject() // Return empty object
3446
}

0 commit comments

Comments
 (0)