/*
* Copyright 2008 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.jstestdriver.server.handlers;
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.inject.Inject;
import com.google.jstestdriver.CapturedBrowsers;
import com.google.jstestdriver.Command;
import com.google.jstestdriver.FileInfo;
import com.google.jstestdriver.FileResult;
import com.google.jstestdriver.FileSource;
import com.google.jstestdriver.JsonCommand;
import com.google.jstestdriver.LoadedFiles;
import com.google.jstestdriver.Response;
import com.google.jstestdriver.Response.ResponseType;
import com.google.jstestdriver.SlaveBrowser;
import com.google.jstestdriver.protocol.BrowserLog;
import com.google.jstestdriver.protocol.BrowserStreamAcknowledged;
import com.google.jstestdriver.requesthandlers.RequestHandler;
import org.mortbay.jetty.MimeTypes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author jeremiele@google.com (Jeremie Lenfant-Engelmann)
*/
class BrowserQueryResponseHandler implements RequestHandler {
private static final Logger logger =
LoggerFactory.getLogger(BrowserQueryResponseHandler.class);
private final Gson gson = new Gson();
private final HttpServletRequest request;
private final HttpServletResponse response;
private final CapturedBrowsers browsers;
// TODO(corysmith): factor out a streaming session class.
private final ConcurrentMap<SlaveBrowser, List<String>> streamedResponses;
@Inject
public BrowserQueryResponseHandler(
HttpServletRequest request,
HttpServletResponse response,
CapturedBrowsers browsers,
ConcurrentMap<SlaveBrowser, List<String>> streamedResponses) {
this.request = request;
this.response = response;
this.browsers = browsers;
this.streamedResponses = streamedResponses;
}
@Override
public void handleIt() throws IOException {
logger.trace("Browser Query Post:\n\tpath:{}\n\tresponse:{}\n\tdone:{}\n\tresponseId:{}",
new Object[] {
request.getPathInfo().substring(1),
request.getParameter("response"),
request.getParameter("done"),
request.getParameter("responseId")
});
response.setContentType(MimeTypes.TEXT_JSON_UTF_8);
service(request.getPathInfo().substring(1),
request.getParameter("response"),
request.getParameter("done"),
request.getParameter("responseId"),
response.getWriter());
}
public void service(String id,
String response,
String done,
String responseId,
PrintWriter writer) {
SlaveBrowser browser = browsers.getBrowser(id);
if (browser != null) {
boolean isLast = Boolean.parseBoolean(done);
try {
serviceBrowser(response, isLast, responseId, writer, browser);
} catch (JsonParseException e) {
writer.print(gson.toJson(new JsonCommand(JsonCommand.CommandType.STOP, null)));
writer.flush();
throw new RuntimeException("Unable to parse: " + response, e);
}
} else {
// TODO(corysmith): handle this better.
logger.error("Unknown browser {} with response {}.\n Known {}", new Object[]{id, response, browsers.getSlaveBrowsers()});
writer.print(gson.toJson(new JsonCommand(JsonCommand.CommandType.STOP, Lists.newArrayList("Stopping due to missing browser."))));
try {
Thread.sleep(1000); // pause to make sure the browser doesn't spin.
} catch (InterruptedException e) {
}
}
writer.flush();
}
private void serviceBrowser(String response, Boolean done, String responseId, PrintWriter writer,
SlaveBrowser browser) throws JsonParseException {
addResponseId(responseId, browser);
browser.heartBeat();
Command command = null;
if (isResponseValid(response)) {
Response res = gson.fromJson(response, Response.class);
logger.trace("response type: " + res.getResponseType());
// TODO (corysmith): Replace this with polymorphism,
// using the response type to create disposable actions.
switch (res.getResponseType()) {
case BROWSER_READY:
handleFileLoadResult(browser, res);
// TODO(corysmith): Move the loading of files to a browser into the
// server
browser.addResponse(
new Response(ResponseType.FILE_LOAD_RESULT.toString(), res.getResponse(), browser
.getBrowserInfo(), "", res.getExecutionTime()), false);
browser.ready();
break;
case FILE_LOAD_RESULT:
handleFileLoadResult(browser, res);
browser.addResponse(res, done);
break;
case NOOP:
break;
case LOG:
BrowserLog log = gson.fromJson(res.getResponse(), res.getGsonType());
if (log.getLevel() == 1000) {
logger.info("Error in browser: " + res.toString());
} else {
logger.info("Message from the browser: " + res.toString());
}
browser.addResponse(res, done);
break;
// reset the browsers fileset.
case RESET_RESULT:
browser.resetFileSet();
logger.debug("Clearing fileset for {}", browser);
handleFileLoadResult(browser, res);
// queue the load results for the next command to be run.
browser.addResponse(
new Response(ResponseType.FILE_LOAD_RESULT.toString(), res.getResponse(), browser
.getBrowserInfo(), "", res.getExecutionTime()), false);
browser.addResponse(res, done);
break;
case UNKNOWN:
logger.error("Recieved Unknown: " + response);
browser.addResponse(res, done);
break;
case BROWSER_PANIC:
logger.debug("Browser panic for {}", res.toString());
default:
browser.addResponse(res, done);
break;
}
logger.trace("Received:\n done: {} \n res:\n {}\n", new Object[] {done, res});
}
if (isResponseIdValid(responseId) && !done && !isResponseValid(response)) {
logger.trace("Streaming query for ids {} from {}", streamedResponses.get(browser), browser);
}
// TODO(corysmith): What do we do?
if (!isResponseValid(response) && done && browser.isCommandRunning()) {
logger.error("Streaming ending, but no response sent for {} while running {}",
browser,
browser.getCommandRunning());
}
// TODO(corysmith): Refactoring the streaming into a separate layer.
if (!done) { // we are still streaming, so we respond with the streaming
// acknowledge.
// this is independent of receiving an actual response.
final String jsonResponse = gson.toJson(new BrowserStreamAcknowledged(streamedResponses.get(browser)));
logger.trace("sending jsonResponse {}", jsonResponse);
writer.print(jsonResponse);
writer.flush();
return;
} else {
streamedResponses.get(browser).clear();
}
if(command == null) {
command = browser.dequeueCommand();
browser.heartBeat();
}
logger.trace("sending command {}", command == null ? "null" : command.getCommand());
writer.print(command.getCommand());
}
/**
* @param browser
* @param res
*/
private void handleFileLoadResult(SlaveBrowser browser, Response res) {
LoadedFiles loadedFiles = gson.fromJson(res.getResponse(), res.getGsonType());
Collection<FileResult> allLoadedFiles = loadedFiles.getLoadedFiles();
logger.info("loaded {} files", allLoadedFiles.size());
browser.addFileResults(allLoadedFiles);
}
private boolean isResponseValid(String response) {
return response != null && !"null".equals(response) && !"undefined".equals(response) && response.length() > 0;
}
private void addResponseId(String responseId, SlaveBrowser browser) {
if (!streamedResponses.containsKey(browser)) {
streamedResponses.put(browser, new CopyOnWriteArrayList<String>());
}
if (!isResponseIdValid(responseId)) {
return;
}
streamedResponses.get(browser).add(responseId);
}
private boolean isResponseIdValid(String responseId) {
return !(responseId == null || "".equals(responseId));
}
}