/*
* SMART FP7 - Search engine for MultimediA enviRonment generated contenT
* Webpage: http://smartfp7.eu
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* The Original Code is Copyright (c) 2012-2013 Athens Information Technology
* All Rights Reserved
*
* Contributor:
* Nikolaos Katsarakis nkat@ait.edu.gr
*/
package eu.smartfp7.EdgeNode;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TimeZone;
import java.util.Timer;
import java.util.TimerTask;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.lightcouch.CouchDbClient;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* Servlet implementation class ReplayFeed
*/
@WebServlet(name = "replayFeed", description = "Simulates a live feed", urlPatterns = { "/replayFeed" })
public class ReplayFeed extends HttpServlet {
private static final long serialVersionUID = 1L;
// CouchDB access parameters
private CouchDbClient feedsClient = null;
private String server, user, pass;
private int port;
// HashMap<String, Timer> timerList = null;
// HashMap<String, CouchDbClient> dbClientList = null;
// HashMap<String, Integer> dbClientUsage = null;
HashMap<String, ReplayTaskData> replayList = null;
Calendar cal; // Used for printing dates
/**
* @see HttpServlet#HttpServlet()
*/
public ReplayFeed() {
super();
}
/**
* @see Servlet#init(ServletConfig)
*/
public void init(ServletConfig config) throws ServletException {
try {
super.init(config);
} catch (ServletException e) {
System.err.println("Can not initialize servlet");
return;
}
// Read couchdb properties from file and initialise the client for feeds
Properties dbProps = new Properties();
try {
dbProps.load(getServletContext().getResourceAsStream("/WEB-INF/couchdb.properties"));
} catch (IOException e1) {
System.err.println("Can not open couchdb properties file");
return;
}
server = dbProps.getProperty("server");
port = Integer.parseInt(dbProps.getProperty("port"));
user = dbProps.getProperty("user");
pass = dbProps.getProperty("pass");
// Test for incorrect user/pass or CouchDB server offline
try {
feedsClient = new CouchDbClient("feeds", true, "http", server, port, user, pass);
} catch (Exception e) {
System.out
.println("Could not connect to CouchDB, check that the server is running and that the correct username/pass is set");
System.out.println("Current configuration: server=" + server + ", port=" + port + ", user= " + user
+ ", pass= " + pass);
return;
}
// timerList = new HashMap<String, Timer>();
// dbClientList = new HashMap<String, CouchDbClient>();
// dbClientUsage = new HashMap<String, Integer>();
replayList = new HashMap<String, ReplayTaskData>();
cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
}
/**
* @see Servlet#destroy()
*/
public void destroy() {
// Stop all running timers
cancelTimers();
// Close feeds connection
if (feedsClient != null) {
feedsClient.shutdown();
}
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
String action = request.getParameter("action");
String feedname = null;
if (action == null) {
try {
response.sendRedirect("replayFeed.html");
} catch (IOException e1) {
System.out.println("doGet IOException: Can not redirect");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
out.println("{\"error\":\"action parameter not specified\"}");
}
return;
}
// Read the feed name for the actions that need it
if (action.equals("start") || action.equals("stop") || action.equals("delete")) {
feedname = request.getParameter("feedname");
if (feedname == null) {
out.println("{\"error\":\"Start and stop actions require the parameter 'feedname'\"}");
return;
}
}
// Other parameters that are action-specific will be read from within the functions
int res;
if (action.equals("list"))
res = listRunningFeeds(out);
else if (action.equals("start"))
res = startReplay(feedname, out, request);
else if (action.equals("stop"))
res = stopReplay(feedname, out);
else if (action.equals("delete"))
res = deleteReplay(feedname, out);
else if (action.equals("delete_all"))
res = deleteAllFeeds(out, request);
else {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
out.println("{\"error\":\"Unknown action, should be one of: 'list', 'start', 'stop', 'delete', 'delete_all'\"}");
return;
}
// error messages will be printed by the function
if (res < 0) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} else if (res > 0) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
}
// All below functions return 0 if ok -1 if server error 1 if bad request
int listRunningFeeds(PrintWriter out) {
if (replayList == null || feedsClient == null) {
out.println("{\"error\":\"Server not correctly initialised\"}");
return -1;
}
int len;
StringBuilder replayNames = new StringBuilder();
for (String name : replayList.keySet()) {
replayNames.append("\"" + name + "\",");
}
// Delete last comma character in name list
if ((len = replayNames.length()) > 0)
replayNames.deleteCharAt(len - 1);
out.println("{\"list\":[" + replayNames.toString() + "]}");
return 0;
}
int deleteAllFeeds(PrintWriter out, HttpServletRequest request) {
if (replayList == null || feedsClient == null) {
out.println("{\"error\":\"Server not correctly initialised\"}");
return -1;
}
if (!"delete all replays".equals(request.getParameter("confirm"))) {
out.println("{\"error\":\"To prevent accidental deletion, an additional parameter 'confirm=delete+all+replays' is required\"}");
return 1;
}
cancelTimers();
// Wait a bit for the timers to stop
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
// Delete the replay databases and remove them from feeds list
long startTime = System.currentTimeMillis();
int doccount = 0;
for (String DBname : feedsClient.context().getAllDbs()) {
if (DBname.startsWith("replay_")) {
doccount++;
feedsClient.context().deleteDB(DBname, "delete database");
// also remove from feeds list if existing
String rev;
if ((rev = feedsClient.getRevision(DBname)) != null) {
feedsClient.remove(DBname, rev);
}
}
}
out.println("{\"status\": \"deleted " + doccount + " replays in " + (System.currentTimeMillis() - startTime)
+ " ms\"}");
return 0;
}
int deleteReplay(final String name, PrintWriter out) {
if (replayList == null || feedsClient == null) {
out.println("{\"error\":\"Server not correctly initialised\"}");
return -1;
}
// First stop the replay, ignore if the replay is not running
stopReplay(name, null);
StringBuffer error = new StringBuffer();
// Delete the database
if (feedsClient.context().getAllDbs().contains(name)) {
feedsClient.context().deleteDB(name, "delete database");
} else {
error.append("feed database was not found");
}
// And remove from feeds list if existing
String rev;
if ((rev = feedsClient.getRevision(name)) != null) {
feedsClient.remove(name, rev);
} else {
if (error.length() > 0) {
error.append(", ");
}
error.append("feed was not present in feeds list");
}
if (error.length() > 0) {
out.println("{\"error\":\"Deleted replay, but following error(s) occured: " + error.toString() + "\"}");
return 1;
} else {
out.println("{\"status\":\"Deleted replay\"}");
return 0;
}
}
/*
* Returns 0 on success, 1 on bad input, -1 on error and prints description in out
*/
int startReplay(final String name, PrintWriter out, HttpServletRequest request) {
if (replayList == null || feedsClient == null) {
out.println("{\"error\":\"Server not correctly initialised\"}");
return -1;
}
if (replayList.size() > 30) {
out.println("{\"error\":\"Too many replays currently running (" + replayList.size()
+ "), please stop some before retrying\"}");
return 1;
}
String newname;
// If the user provides a new name, prefix it with "replay_", else prefix the original name
if (request.getParameter("newname") != null) {
newname = "replay_" + Common.cleanString(request.getParameter("newname").toString());
} else
newname = "replay_" + name;
// Do not start a new replay if already running
if (replayList.containsKey(newname)) {
out.println("{\"status\":\"Replay into '" + newname + "' already running\"}");
return 0;
}
// Add a new entry on replayList to avoid starting more replays on consecutive requests
ReplayTaskData data = new ReplayTaskData();
replayList.put(newname, data);
// Check if name and newname already exist the database
List<String> DBs = feedsClient.context().getAllDbs();
// If source feed name does not exist, or the feeds db has no description return error
if (!DBs.contains(name) || !feedsClient.contains(name)) {
replayList.remove(newname);
out.println("{\"error\":\"Source feed does not exist or has no description in 'feeds' list\"}");
return 1;
}
// Open a connection to the source and destination DBs
try {
data.srcClient = new CouchDbClient(name, false, "http", server, port, user, pass);
// Create the new database if it does not exist
data.dstClient = new CouchDbClient(newname, true, "http", server, port, user, pass);
} catch (Exception e) {
replayList.remove(newname);
cancelReplay(data);
if (data.srcClient == null) {
// No connection could be made to source client
out.println("{\"error\":\"Could not access DB '" + name + "' \"}");
} else {
// Connection to srcClient was successful, but couldn't connect to target
out.println("{\"error\":\"Could not access DB '" + newname + "' \"}");
}
return -1;
}
// If the new db does not have design document for view, add it
if (!data.dstClient.contains("_design/get_data")) {
String viewDoc = "{\n"
+ "\t\"_id\": \"_design/get_data\",\n"
+ "\t\"language\": \"javascript\",\n"
+ "\t\"views\": {\n"
+ "\t\t \"by_date\": {\n"
+ "\t\t\t \"map\": \"function(doc) {\\nif(doc.timestamp && doc.data) {\\nemit(doc.timestamp, doc.data);\\n}\\n}\"\n"
+ "\t\t }\n" + "\t}\n" + "}\n";
try {
data.dstClient.saveJsonText(viewDoc);
} catch (Exception e) {
replayList.remove(newname);
// release previously initialised clients
cancelReplay(data);
out.println("{\"error\":\"Could not add design document in DB '" + newname + "' \"}");
return -1;
}
}
// In feeds database copy the feed description from original feed to new feed
// Get the revision of new feed description document, null means it does not exist
String rev = feedsClient.getRevision(newname);
try {
// Get the original feed description
JsonObject feedObj = feedsClient.find(JsonObject.class, name);
// Change the document id to match new feed
feedObj.addProperty("_id", newname);
// If the document was not present in feeds before, remove _rev property and save
if (rev == null) {
feedObj.remove("_rev");
feedsClient.save(feedObj);
}
// otherwise set _rev property to the revision and update the document
else {
feedObj.addProperty("_rev", rev);
feedsClient.update(feedObj);
}
} catch (Exception e) {
replayList.remove(newname);
// release previously initialised clients
cancelReplay(data);
out.println("{\"error\":\"Could not copy feed description from '" + name + "' to '" + newname + "' \"}");
return -1;
}
Long startMillis = null, endMillis = null;
String startDate = request.getParameter("start_date");
if (startDate != null) {
try {
startMillis = Long.parseLong(startDate);
} catch (NumberFormatException e) {
try {
startMillis = javax.xml.bind.DatatypeConverter.parseDateTime(startDate).getTimeInMillis();
} catch (IllegalArgumentException e1) {
out.println("{\"error\":\"Invalid start date '" + startDate + "'\"}");
return 1;
}
}
}
String endDate = request.getParameter("end_date");
if (endDate != null) {
try {
endMillis = Long.parseLong(endDate);
} catch (NumberFormatException e) {
try {
endMillis = javax.xml.bind.DatatypeConverter.parseDateTime(endDate).getTimeInMillis();
} catch (IllegalArgumentException e1) {
out.println("{\"error\":\"Invalid end date '" + endDate + "'\"}");
return 1;
}
}
}
if (startMillis != null && endMillis != null && startMillis > endMillis) {
out.println("{\"error\":\"End time is before start time\"}");
return 1;
}
String repeatStr = request.getParameter("repeats");
if (repeatStr != null) {
try {
data.repeats = Integer.parseInt(repeatStr);
} catch (NumberFormatException e) {
out.println("{\"error\":\"Invalid number of repeats '" + repeatStr + "'\"}");
return 1;
}
} else {
// only repeat once
data.repeats = 1;
}
List<JsonObject> resList;
// Get the first 2 documents, write the first one in the new feed DB and keep the other to
// send in the next iteration
if (startMillis != null && endMillis != null) {
resList = data.srcClient.view("get_data/by_date").startKey(startMillis).endKey(endMillis).limit(2)
.query(JsonObject.class);
} else if (startMillis != null) {
resList = data.srcClient.view("get_data/by_date").startKey(startMillis).limit(2).query(JsonObject.class);
} else if (endMillis != null) {
resList = data.srcClient.view("get_data/by_date").endKey(endMillis).limit(2).query(JsonObject.class);
} else {
resList = data.srcClient.view("get_data/by_date").limit(2).query(JsonObject.class);
}
if (resList == null || resList.size() != 2) {
replayList.remove(newname);
// release previously initialised clients
cancelReplay(data);
out.println("{\"error\":\"Feed '" + name + "' contains no data or only one measurement\"}");
return 1;
}
data.count = 0;
data.feedStart = resList.get(0).get("key").getAsLong();
data.nextDocTime = resList.get(1).get("key").getAsLong();
data.replayStart = System.currentTimeMillis();
data.feedEnd = (endMillis == null ? Long.MAX_VALUE : endMillis);
// Calculate the time difference from start to next document
long diff = data.nextDocTime - data.feedStart;
// Update time in the first document and save into DB
data.dstClient.save(changeDocumentTime(resList.get(0), data.replayStart));
// Update time in the second document and store for next run
data.nextDoc = changeDocumentTime(resList.get(1), data.replayStart + diff);
// Set the timer to save the next doc
data.tm = new Timer("timer_" + newname);
data.tm.schedule(new replayTimer(newname), diff);
out.println("{\"status\":\"Started replay of '" + name + "' into '" + newname + "'\"}");
return 0;
}
class replayTimer extends TimerTask {
String name;
replayTimer(String name) {
this.name = name;
}
@Override
public void run() {
ReplayTaskData data = replayList.get(name);
List<JsonObject> resList = null;
if (data == null)
return;
// Next document was already loaded before, save it
try {
data.dstClient.save(data.nextDoc);
} catch (org.lightcouch.CouchDbException e) {
System.err.println("CouchDB Exception saving");
}
if (data.nextDocTime + 1 < data.feedEnd) {
try {
// Get a new document
// resList = data.srcClient.view("get_data/by_date").startKey(data.nextDocTime + 1).limit(1)
// .query(JsonObject.class);
resList = data.srcClient.view("get_data/by_date").startKey(data.nextDocTime + 1)
.endKey(data.feedEnd).limit(1).query(JsonObject.class);
} catch (Exception e) {
System.err.println("CouchDB Exception reading");
}
}
// No more data available
if (resList == null || resList.size() != 1) {
boolean restart = false;
if (++data.count == data.repeats) {
// We have done the required number of repetitions
cancelReplay(data);
replayList.remove(name);
} else {
try {
resList = data.srcClient.view("get_data/by_date").startKey(data.feedStart).endKey(data.feedEnd)
.limit(1).query(JsonObject.class);
} catch (Exception e) {
System.err.println("CouchDB Exception reading");
}
if (resList != null && resList.size() == 1) {
// Restart a new loop
restart = true;
data.replayStart = System.currentTimeMillis();
}
}
// System.out.println("Source feed for '" + name + "' contains no more data");
if (!restart)
return;
}
data.nextDoc = resList.get(0);
data.nextDocTime = data.nextDoc.get("key").getAsLong();
// Calculate the difference between start of feed and doc time
long diff1 = data.nextDocTime - data.feedStart;
// Check how much time has passed since start of replay
long diff2 = System.currentTimeMillis() - data.replayStart;
// Calculate how much time remains till next run
long newTime = diff1 - diff2;
if (newTime < 0)
newTime = 0;
// Update the new document time based on diff1
data.nextDoc = changeDocumentTime(data.nextDoc, data.replayStart + diff1);
try {
// reschedule next timer according to actual time passed
data.tm.schedule(new replayTimer(name), newTime);
} catch (IllegalStateException e) {
// We can get here if the timer was stopped in the meantime, suppress warning
}
// System.out.println("Running " + name + ", count = " + data.count++ + ", nextTime = " + data.nextDocTime);
}
}
int stopReplay(String name, PrintWriter out) {
if (replayList == null)
return -1;
if (!replayList.containsKey(name)) {
if (out != null)
out.println("{\"error\":\"Replay of '" + name + "' not running\"}");
return 1;
}
ReplayTaskData data = replayList.get(name);
cancelReplay(data);
replayList.remove(name);
if (out != null)
out.println("{\"status\":\"Stopped replay of '" + name + "'\"}");
return 0;
}
private void cancelReplay(ReplayTaskData data) {
// Stop timer
if (data.tm != null)
data.tm.cancel();
// Close CouchDB clients
if (data.srcClient != null)
data.srcClient.shutdown();
if (data.dstClient != null)
data.dstClient.shutdown();
}
// Stop all running timers
private void cancelTimers() {
for (String name : replayList.keySet()) {
cancelReplay(replayList.get(name));
}
replayList.clear();
}
JsonObject changeDocumentTime(JsonObject Document, long newTimeMillis) {
String xmlDateTime;
boolean fixMillis = false;
/*
* If the millis is divisible by 1000, printDateTime does not add a decimal printout. This workaround fixes this
* by adding a millisecond before printing
*/
if (newTimeMillis % 1000 == 0) {
fixMillis = true;
newTimeMillis++;
}
synchronized (this) {
cal.setTimeInMillis(newTimeMillis);
xmlDateTime = javax.xml.bind.DatatypeConverter.printDateTime(cal);
}
// Undo the change done previously and also fix the printout
if (fixMillis) {
xmlDateTime = xmlDateTime.replace(".001Z", ".000Z");
newTimeMillis--;
}
JsonObject newDoc = new JsonObject();
newDoc.addProperty("_id", xmlDateTime);
newDoc.addProperty("timestamp", newTimeMillis);
JsonObject data = Document.get("value").getAsJsonObject();
// If "time" already exists, update it
if (data.get("time") != null) {
data.addProperty("time", xmlDateTime);
newDoc.add("data", data);
} else {
// If not already existing, add "time" property at the beginning
JsonObject data1 = new JsonObject();
data1.addProperty("time", xmlDateTime);
for (Entry<String, JsonElement> e : data.entrySet()) {
data1.add(e.getKey(), e.getValue());
}
newDoc.add("data", data1);
}
return newDoc;
}
String getDocument(String name) {
InputStream is = feedsClient.find(name);
InputStreamReader isr = null;
try {
isr = new InputStreamReader(is, "UTF-8");
} catch (UnsupportedEncodingException ignore) {
}
// Store the document to a StringBuilder in order to convert to string
StringBuilder docStrBuilder = new StringBuilder();
try {
char[] buf = new char[4 * 1024]; // 4 KB char buffer
int len;
while ((len = isr.read(buf, 0, buf.length)) != -1) {
docStrBuilder.append(buf, 0, len);
// System.out.println("Total read thus far:"+reqStrBuilder.toString());
}
} catch (IOException e1) {
System.err.println("Can not read input, received data:" + docStrBuilder.toString());
try {
is.close();
} catch (IOException e) {
}
return null;
}
// Close the input stream as requested by find()
try {
is.close();
} catch (IOException e) {
}
return docStrBuilder.toString();
}
class ReplayTaskData {
Timer tm;
CouchDbClient srcClient, dstClient;
JsonObject nextDoc;
int count, repeats;
long nextDocTime, replayStart, feedStart, feedEnd;
}
}