/*
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.appengine.demos.asyncrest;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.eclipse.jetty.util.ajax.JSON;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.AsyncContext;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet which makes REST calls asynchronously.
*
* <p>May be configured with init parameters:
* <dl>
* <dt>appid</dt>
* <dd>The Google app key to use</dd>
* </dl>
*
*/
public class AsyncRestServlet extends AbstractRestServlet {
static final String RESULTS_ATTR = "com.google.appengine.demos.asyncrest.client";
static final String DURATION_ATTR = "com.google.appengine.demos.asyncrest.duration";
static final String START_ATTR = "com.google.appengine.demos.asyncrest.start";
HttpClient client;
@Override
public void init(ServletConfig servletConfig) throws ServletException {
super.init(servletConfig);
SslContextFactory sslContextFactory = new SslContextFactory();
client = new HttpClient(sslContextFactory);
try {
client.start();
} catch (Exception e) {
throw new ServletException(e);
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (key == null) {
response.sendError(500, APPKEY + " not set");
return;
}
Long start = System.nanoTime();
// Do we have results yet?
Queue<Map<String, Object>> results =
(Queue<Map<String, Object>>) request.getAttribute(RESULTS_ATTR);
// If no results, this must be the first dispatch, so send the REST request(s).
if (results == null) {
// define results data structures
final Queue<Map<String, Object>> resultsQueue = new ConcurrentLinkedQueue<>();
request.setAttribute(RESULTS_ATTR, results = resultsQueue);
// Suspend the request.
// This is done before scheduling async handling to avoid race of
// dispatch before startAsync!
final AsyncContext async = request.startAsync();
async.setTimeout(30000);
// Extract keywords to search for.
String lat = sanitize(request.getParameter(LATITUDE_PARAM));
String longitude = sanitize(request.getParameter(LONGITUDE_PARAM));
String radius = sanitize(request.getParameter(RADIUS_PARAM));
String[] keywords = sanitize(request.getParameter(ITEMS_PARAM)).split(",");
final AtomicInteger outstanding = new AtomicInteger(keywords.length);
// Send request each keyword.
for (final String item : keywords) {
client.newRequest(restQuery(lat + "," + longitude, radius, item))
.method(HttpMethod.GET)
.send(
new AsyncRestRequest() {
@Override
void onLocationFound(Map<String, Object> result) {
resultsQueue.add(result);
}
@Override
void doComplete() {
if (outstanding.decrementAndGet() <= 0) {
async.dispatch();
}
}
});
}
// save timing info and return
request.setAttribute(START_ATTR, start);
request.setAttribute(DURATION_ATTR, System.nanoTime() - start);
return;
}
// We have results!
// Generate the response
String thumbs = generateResults(results);
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html><head>");
out.println(STYLE);
out.println("</head><body><small>");
long initial = (Long) request.getAttribute(DURATION_ATTR);
long start0 = (Long) request.getAttribute(START_ATTR);
long now = System.nanoTime();
long total = now - start0;
long generate = now - start;
long thread = initial + generate;
String loc = sanitize(request.getParameter(LOC_PARAM));
out.print(
"<b>Asynchronous: Requesting " + sanitize(request.getParameter(ITEMS_PARAM)) + " near "
+ (loc != null ? loc
: "lat=" + sanitize(request.getParameter(LATITUDE_PARAM)) + " long="
+ sanitize(request.getParameter(LONGITUDE_PARAM)))
+ "</b><br/>");
out.print("Total Time: " + ms(total) + "ms<br/>");
out.print(
"Thread held (<span class='red'>red</span>): "
+ ms(thread) + "ms (" + ms(initial)
+ " initial + " + ms(generate) + " generate )<br/>");
out.print("Async wait (<span class='green'>green</span>): " + ms(total - thread) + "ms<br/>");
out.println(
"<img border='0px' src='asyncrest/red.png' height='20px' width='" + width(initial) + "px'>"
+ "<img border='0px' src='asyncrest/green.png'"
+ " height='20px'"
+ " width='" + width(total - thread) + "px'>"
+ "<img border='0px' src='asyncrest/red.png'"
+ " height='20px'"
+ " width='" + width(generate) + "px'>");
out.println("<br/>");
out.print("First 5 results of " + results.size() + ":<br/>");
if ("".equals(thumbs)) {
out.print("<i>No results. Ensure " + APPKEY + " property is set correctly.</i>");
} else {
out.println(thumbs);
}
out.println("</small>");
out.println("</body></html>");
out.close();
}
private abstract class AsyncRestRequest extends Response.Listener.Adapter {
final Utf8StringBuilder utf8Content = new Utf8StringBuilder();
AsyncRestRequest() {
}
@Override
public void onContent(Response response, ByteBuffer content) {
byte[] bytes = BufferUtil.toArray(content);
utf8Content.append(bytes, 0, bytes.length);
}
@Override
public void onComplete(Result result) {
// Extract results.
Map<String, Object> data = (Map<String, Object>) JSON.parse(utf8Content.toString());
if (data != null) {
Object[] results = (Object[]) data.get("results");
if (results != null) {
for (Object o : results) {
onLocationFound((Map<String, Object>) o);
}
}
}
doComplete();
}
abstract void onLocationFound(Map<String, Object> details);
abstract void doComplete();
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}