How to build actions with Java Client Library on Google AppEngine
Last week, a new client library has been released. The name is “Actions on Google Java/Kotlin Client Library.
Until now, most developers can use a client library called “Actions on Google Client Library for Node.js”. That is, we had only the way to build actions with Node.js. But, currently, we can build actions with Java programming language easily using the Java/Kotlin Client Library.
The abstract of usage was described on the following article:
In this article, I introduce how to build actions with the Java/Kotlin Client Library and to deploy them to the Google AppEngine.
Prerequisite
Before building actions, you need to install the following:
- Java Development Kit Version 1.8 or higher.
- Gradle version 5.1.1 or higher.
- Google Cloud SDK
- App Engine SDK for Java (you can install it with the following command)
gcloud components install app-engine-java
- You already have your Actions on Google project and your Dialogflow agent that has already been connected to the AoG project.
Also, you have a project on Google Cloud Platform, which the billing has already been enabled.
If you don’t know how to build actions, you can study it with codelabs:
Create Project
Let’s get started to build your action. First, create your new project with Gradle. In this article, we use “simple-fulfillment-java” as the project name and “jp.eisbahn.actions.simpleaction” as the package name (The “jp.eisbahn” is the domain I have).
$ mkdir simple-fulfillment-appengine
$ cd simple-fulfillment-appengine
$ gradle init
The gradle
command asks you some questions. Answer them with the following:
- Select type of project to generate: 1 (basic)
- Select build script DSL: 1 (groovy)
- Project name: “simple-fulfillment-java”
Some files including build.gradle
file are generated.
Edit Build File
After creating your project, the build.gradle
file is empty. Replace it with the following content:
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.google.cloud.tools:appengine-gradle-plugin:1.+'
}
}repositories {
jcenter()
mavenCentral()
}apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'com.google.cloud.tools.appengine'dependencies {
compile 'com.google.appengine:appengine-api-1.0-sdk:+'
compile group: 'com.google.actions', name: 'actions-on-google', version: '1.0.0'
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
}appengine {
deploy {
}
}group = 'jp.eisbahn.actions.simpleaction'
version = '1.0-SNAPSHOT'sourceCompatibility = '1.8'
targetCompatibility = '1.8'
Create Fulfillment Code
Next, you need to create your fulfillment code for your action. Create a source directory with the following command:
$ mkdir -p src/main/java/jp/eisbahn/actions/simpleaction
Then, create your fulfillment file called “SimpleApp.java” in the simpleaction
directory with the following content:
package jp.eisbahn.actions.simpleaction;import com.google.actions.api.ActionRequest;
import com.google.actions.api.ActionResponse;
import com.google.actions.api.DialogflowApp;
import com.google.actions.api.ForIntent;
import com.google.actions.api.response.ResponseBuilder;public class SimpleApp extends DialogflowApp { @ForIntent("Default Welcome Intent")
public ActionResponse welcome(ActionRequest request) {
ResponseBuilder builder = getResponseBuilder(request);
builder.add("Hello, Google AppEngine!");
return builder.build();
}}
In the code above, the SimpleApp
class extends the DialogflowApp
class. That is, the class can handle requests from Dialogflow Agent. And, you can create intent handler methods using ForIntent
annotation.
The first argument has information of the request. And, you can create the ActionResponse
instance with the ResponseBuilder
class.
You can know how to build responses by reading the API reference above.
Create Servlet Code
By the way, the SimpleApp
class does not have any abilities to handle HTTP requests. Instead, you need to create a handler class to handle requests and responses. In this article, create a Java Servlet code to handle HTTP requests and response.
Create your fulfillment file called “SimpleServlet.java” in the simpleaction
directory with the following content:
package jp.eisbahn.actions.simpleaction;import com.google.actions.api.App;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;@WebServlet(name = "SimpleServlet", urlPatterns = {"/"}, loadOnStartup = 1)
public class SimpleServlet extends HttpServlet { private App app = new SimpleApp(); @Override
protected void doPost(
HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String requestBody =
req.getReader().lines().collect(Collectors.joining());
Map<String, String> headersMap =
Collections.list(req.getHeaderNames())
.stream()
.collect(Collectors.toMap(
name -> name,
req::getHeader));
try {
String responseBody =
app.handleRequest(requestBody, headersMap).get();
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write(responseBody);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
resp.setStatus(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
resp.getWriter().write(e.getMessage());
}
}}
To call the SimpleApp
instance, you need the following information:
- The string of the request body.
- The
Map<String, String>
object that has all request header entries.
Then, call the handleRequest()
method of the SimpleApp
instance with these arguments. If the result of calling the method is successfully, the CompletableFuture<String>
object is returned. This is for asynchronous like Promise in JavaScript. In the code above, the actual result is retrieved by calling the get()
method.
The result is a string value of the response represents as JSON string. The SimpleServlet
sends the response string with the Writer object retrieved by calling the getWriter()
method.
Create Configuration File for AppEngine
Last, you need to prepare a configuration file for AppEngine. Create a new directory where you put the file by the following command:
$ mkdir -p src/main/webapp/WEB-INF
Then, create the following file called “appengine-web.xml” into the WEB-INF
directory:
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<threadsafe>true</threadsafe>
</appengine-web-app>
Deploy Your Code to AppEngine
Ok, you created all files. Now, you can deploy your code to the AppEngine. First, if other project is selected, you need to specify your target project that you want to deploy your code.
$ gcloud config set project <YOUR_PROJECT_ID>
Let’s deploy your code by the following command:
$ gradle appengineDeploy
If the deployment is successfully, you can test it by the following command:
$ curl -v -X POST -d "{}" https://<PROJECT_ID>.appspot.com/
It’s Ok, if you receive like the following output:
< HTTP/2 500
< x-cloud-trace-context: 4ba6987ee3351d91cd43fff194b79962;o=1
< date: Mon, 21 Jan 2019 00:36:39 GMT
< content-type: text/html
< server: Google Frontend
< content-length: 55
< alt-svc: quic=":443"; ma=2592000; v="44,43,39,35"
<
* Curl_http_done: called premature == 0
* Connection #0 to host azure-test-65d3c.appspot.com left intact
java.lang.Exception: Intent handler not found - INVALID%
The message “Intent handler not found” was returned by the Actions on Google Java/Kotlin Client Library.
After deploying, register the URL as the fulfillment webhook URL on your Dialogflow agent. Then, invoke your action on the Actions simulator. You should get the response “Hello, Google AppEngine!” from the Google Assistant.
Conclusion
If you’re a Java programmer, you can build your actions for Google Assistant with the Actions on Google Java/Kotlin Client Library and the Java language that you are used to. I’m happy if this article becomes useful as you when you’re interested in building actions for the Google Assistant.
Last, you can get the complete code set from the following GitHub repository: