eu.smartfp7.EdgeNode.ReplayFeed.java Source code

Java tutorial

Introduction

Here is the source code for eu.smartfp7.EdgeNode.ReplayFeed.java

Source

/* 
 * 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;
    }

}