de.root1.kad.smartvisu.BackendServer.java Source code

Java tutorial

Introduction

Here is the source code for de.root1.kad.smartvisu.BackendServer.java

Source

/*
 * Copyright (C) 2015 Alexander Christian <alex(at)root1.de>. All rights reserved.
 * 
 * This file is part of KAD CometVisu Backend (KCVB).
 *
 *   KCVB is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   KCVB is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with KCVB.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.root1.kad.smartvisu;

import de.root1.kad.knxservice.KnxService;
import de.root1.kad.knxservice.KnxServiceDataListener;
import de.root1.kad.knxservice.KnxServiceException;
import de.root1.kad.knxservice.NamedThreadFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.json.simple.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author achristian
 */
public class BackendServer extends NanoHttpdSSE {

    private final Logger log = LoggerFactory.getLogger(getClass());
    private final String documentRoot;

    static final String REQUEST_LOGIN = "login";
    static final String REQUEST_READ = "read";
    static final String REQUEST_WRITE = "write";
    static final String REQUEST_FILTER = "filter";
    static final String REQUEST_RRDFETCH = "rrdfetch";
    static final String REQUEST_HOOK = "hook";

    static final String PARAM_SESSION = "session";
    static final String PARAM_VERSION = "version";
    static final String PARAM_DEVICE = "device";
    static final String PARAM_PASS = "pass";
    static final String PARAM_USER = "user";

    static final String PARAM_VALUE = "value";
    static final String PARAM_ADDR = "addr";
    static final String PARAM_DATA = "data";

    private final Map<String, UserSessionID> sessions = new HashMap<>();
    private Timer t = new Timer("SessionID Remover");
    private TimerTask tt = new TimerTask() {

        @Override
        public void run() {
            synchronized (sessions) {
                Iterator<String> iter = sessions.keySet().iterator();
                while (iter.hasNext()) {
                    String clientIp = iter.next();
                    UserSessionID session = sessions.get(clientIp);
                    if (!session.isValid()) {
                        log.info("Removing session due to timeout: {}", session);
                        iter.remove();
                    }
                }
            }
        }
    };
    private final KnxService knx;
    private final int SESSION_TIMEOUT;
    private boolean requireUserSession = false;
    private final ExecutorService pool = Executors.newCachedThreadPool(new NamedThreadFactory("AsyncPool"));

    BackendServer(int port, String documentRoot, KnxService knx, int sessionTimeout, boolean requireUserSession) {
        super(port);
        this.documentRoot = documentRoot;
        this.knx = knx;
        t.schedule(tt, 5000, 30 * 60 * 1000);
        SESSION_TIMEOUT = sessionTimeout;
        this.requireUserSession = requireUserSession;
    }

    private String extractClientIp(IHTTPSession session) {
        return session.getHeaders().get("http-client-ip");
    }

    private String extractUserSessionIdString(IHTTPSession session) {
        return session.getParms().get(PARAM_SESSION);
    }

    private Map<String, List<String>> getParams(IHTTPSession session) {
        return decodeParameters(session.getQueryParameterString());
    }

    @Override
    public IResponse serve(IHTTPSession session) {

        log.info("uri: {}", session.getUri());
        //        log.info("queryParameterString: {}", session.getQueryParameterString());
        log.info("params: {}", getParams(session));
        //        log.info("headers: {}", session.getHeaders());

        String uri = session.getUri();

        if (!uri.startsWith(documentRoot)) {
            Response response = new Response(
                    "<html><body>URI '" + uri + "' not handled by this server</body></html>");
            response.setStatus(Status.BAD_REQUEST);
            return response;
        }

        String resource = uri.substring(documentRoot.length());

        log.info("resource: {}", resource);

        switch (resource) {
        case REQUEST_LOGIN:
            return handleLogin(session);
        case REQUEST_FILTER:
            return handleFilter(session);
        case REQUEST_READ:
            return handleRead(session);
        case REQUEST_WRITE:
            return handleWrite(session);
        case REQUEST_RRDFETCH:
            return handleRrdfetch(session);
        case REQUEST_HOOK:
            return handleHook(session);
        default:
            Response response = new Response(
                    "<html><body>resource '" + resource + "' not handled by this server</body></html>");
            response.setStatus(Status.BAD_REQUEST);
            return response;

        }

    }

    /**
     * Validates user session information found in header. If found&valid,
     * session is renewed.
     *
     * @param session
     * @return not null, if session information in request is valid + session is
     * valid too, false null
     */
    private UserSessionID validateUserSessionInRequest(IHTTPSession session) {

        String clientIp = extractClientIp(session);
        String sessionIdString = extractUserSessionIdString(session);

        synchronized (sessions) {
            UserSessionID sessionId = sessions.get(clientIp);

            if (sessionId != null && sessionId.getId().toString().equals(sessionIdString) && sessionId.isValid()) {
                sessionId.renew();
                return sessionId;
            }
        }
        return null;
    }

    private UserSessionID createUserSessionID(IHTTPSession session) {
        String clientIp = session.getHeaders().get("http-client-ip");
        UserSessionID userSessionId = new UserSessionID(clientIp, SESSION_TIMEOUT);
        sessions.put(clientIp, userSessionId);
        log.info("Storing session: clientip={} sessionid={}", clientIp, userSessionId);
        return userSessionId;
    }

    private Response handleLogin(IHTTPSession session) {

        Map<String, String> parms = session.getParms();
        String user = parms.get(PARAM_USER);
        String pass = parms.get(PARAM_PASS);
        String device = parms.get(PARAM_DEVICE);
        log.info("login: user={}, pass={}, device={}", user, pass, device);

        JSONObject obj = new JSONObject();
        obj.put(PARAM_VERSION, PROTOCOL_VERSION);

        String userSessionIdString = requireUserSession ? createUserSessionID(session).getId().toString() : "0";

        obj.put(PARAM_SESSION, userSessionIdString);

        log.info("response: {}", obj.toJSONString());

        Response response = new Response(obj.toJSONString());
        response.addHeader("Access-Control-Allow-Origin", "*");

        return response;
    }

    private static final String PROTOCOL_VERSION = "0.0.1";

    private Response handleWrite(IHTTPSession session) {

        long start = System.currentTimeMillis();
        // s=SESSION&a=ADDRESS1&a=...&v=VALUE
        Map<String, List<String>> params = getParams(session);
        log.info("write params: {}", params);

        // FIXME CometVisu does not send the session at all?! So the following is disabled for now
        //        if (requireUserSession) {
        //        UserSessionID userSessionID = validateUserSessionInRequest(session);
        //        if (userSessionID==null) return new Response(Status.UNAUTHORIZED, MIME_PLAINTEXT, "");
        //        }
        List<String> addresses = params.get(PARAM_ADDR);
        String value = session.getParms().get(PARAM_VALUE);

        for (String address : addresses) {
            try {
                knx.write(address, value);
            } catch (KnxServiceException ex) {
                ex.printStackTrace();
            }
        }
        log.info("done with write for {}: {}ms", params, System.currentTimeMillis() - start);

        Response response = new Response(Status.OK, MIME_PLAINTEXT, "");
        response.addHeader("Access-Control-Allow-Origin", "*");
        return response;

    }

    private IResponse handleRead(final IHTTPSession session) {

        Map<String, List<String>> params = getParams(session);
        log.info("read params: {}", params);

        UserSessionID userSessionID = null;
        userSessionID = validateUserSessionInRequest(session);
        if (userSessionID == null && requireUserSession) {
            log.warn("No user session found. return {}", Status.UNAUTHORIZED);
            Response response = new Response(Status.UNAUTHORIZED, MIME_PLAINTEXT, "no user session found");
            response.addHeader("Access-Control-Allow-Origin", "*");
            return response;
        }

        final UserSessionID finalUserSessionId = userSessionID;

        final List<String> addresses = params.get(PARAM_ADDR);

        // heartbeat can not be read
        addresses.remove("KAD.smartVISU.heartbeat");

        JSONObject jsonResponse = new JSONObject();
        JSONObject jsonData = new JSONObject();

        final SseResponse sse = new SseResponse(session);
        sse.addHeader("Access-Control-Allow-Origin", "*");
        sse.addHeader("Access-Control-Expose-Headers", "*");

        log.info("Reading addresses: {}", addresses);

        List<String> asyncQuery = new ArrayList<>();

        // client knows nothing. Full response required
        for (String address : addresses) {

            try {

                //                String value = knx.read(address);
                String value = knx.getCachedValue(address);

                if (value != null && !value.isEmpty()) {
                    jsonData.put(address, value);
                } else {
                    log.error("Address '" + address + "' not in cache. will query async.");
                    asyncQuery.add(address);
                }
            } catch (KnxServiceException ex) {
                log.warn("Skipping '" + address + "' due to read problem.", ex);
            }

        }
        jsonResponse.put(PARAM_DATA, jsonData);

        log.info("response: {}", jsonResponse.toJSONString());
        sse.sendMessage(null, null, jsonResponse.toJSONString());

        for (String async : asyncQuery) {
            pool.execute(new AsyncReadRunnable(sse, knx, async));
        }

        KnxServiceDataListener listener = new KnxServiceDataListener() {

            @Override
            public void onData(String ga, String value, KnxServiceDataListener.TYPE type) {
                if (type == TYPE.WRITE) {
                    JSONObject jsonResponse = new JSONObject();
                    JSONObject jsonData = new JSONObject();
                    jsonData.put(ga, value);
                    jsonResponse.put("data", jsonData);
                    log.info("response: {}", jsonResponse.toJSONString());
                    boolean trouble = sse.sendMessage(null, null, jsonResponse.toJSONString());
                    if (!trouble && requireUserSession) {
                        finalUserSessionId.renew();
                    }
                }
            }
        };

        try {

            for (String addr : addresses) {
                knx.registerListener(addr, listener);
            }

            log.info("Waiting for session closed for {}", userSessionID.getId());
            try {
                long lastCheck = System.currentTimeMillis();
                boolean heartbeatState = true;
                while (!sse.waitForTrouble(1000)) {
                    if (System.currentTimeMillis() - lastCheck > 1000) {
                        JSONObject r = new JSONObject();
                        JSONObject d = new JSONObject();
                        d.put("KAD.smartVISU.heartbeat", heartbeatState ? "1" : "0");
                        r.put(PARAM_DATA, d);
                        boolean trouble = sse.sendMessage(null, null, r.toJSONString());
                        log.trace("Sent keepalive for " + finalUserSessionId);
                        if (!trouble && requireUserSession) {
                            finalUserSessionId.renew();
                        }
                        heartbeatState = !heartbeatState; // toggle
                        lastCheck = System.currentTimeMillis();
                    }
                }
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            for (String addr : addresses) {
                knx.unregisterListener(addr, listener);
            }
        } catch (KnxServiceException ex) {
            ex.printStackTrace();
        }
        log.info("Session closed for {}", userSessionID.getId());
        return sse;
    }

    private Response handleFilter(IHTTPSession session) {
        return new Response("<html><body>FILTER: it works</body></html>");
    }

    private IResponse handleRrdfetch(IHTTPSession session) {
        Map<String, List<String>> params = getParams(session);
        log.info("rrdfetch params: {}", params);

        //        UserSessionID userSessionID = null;
        //        userSessionID = validateUserSessionInRequest(session);
        //        if (userSessionID==null) {
        //            return new Response(Status.UNAUTHORIZED, MIME_PLAINTEXT, "");
        //        }
        /**
         * Originally referenced the rrd-file on disk.
         * Ignore?
         */
        String rrd = session.getParms().get("rrd");

        /**
         * ds = datasource == name of data/entity to query?
         * 
         * Pro Graph gibt es eine DS. Und ein Graph ergibt einen HTTP Request!;aegpsu-
         */
        String ds = session.getParms().get("ds");

        /**
         * start of the time series. A time in seconds since epoch (1970-01-01)
         * is required. Negative numbers are relative to the current time. By
         * default, one day worth of data will be fetched. See also AT-STYLE
         * TIME SPECIFICATION section for a detailed explanation on ways to
         * specify the start time.
         */
        String start = session.getParms().get("start");

        /**
         * the end of the time series in seconds since epoch. See also AT-STYLE
         * TIME SPECIFICATION section for a detailed explanation of how to
         * specify the end time.
         */
        String end = session.getParms().get("end");

        /**
         * the interval you want the values to have (seconds per value).
         * rrdfetch will try to match your request, but it will return data even
         * if no absolute match is possible. NB . See note below.
         */
        String res = session.getParms().get("res");

        Response response = new Response("[" + "[1445869830000,[\"2.0700000000E01\"]],"
                + "[1445873874000,[\"2.0600000000E01\"]]," + "[1445881643000,[\"2.0480000000E01\"]],"
                + "[1445901039000,[\"2.0600000000E01\"]]," + "[1445901042000,[\"2.0500000000E01\"]],"
                + "[1445910320000,[\"2.0400000000E01\"]]," + "[1445919568000,[\"2.0300000000E01\"]],"
                + "[1445931781000,[\"2.0420000000E01\"]]," + "[1445941319000,[\"2.0520000000E01\"]],"
                + "[1445959136000,[\"2.0520000000E01\"]]," + "[1445959577000,[\"2.0520000000E01\"]],"
                + "[1445960204000,[\"2.0500000000E01\"]]," + "[1445965508000,[\"2.0420000000E01\"]],"
                + "[1445973246000,[\"2.0320000000E01\"]]," + "[1445976175000,[\"2.0420000000E01\"]],"
                + "[1445976479000,[\"2.0520000000E01\"]]," + "[1445978510000,[\"2.0620000000E01\"]],"
                + "[1445978935000,[\"2.0740000000E01\"]]," + "[1445984397000,[\"2.0640000000E01\"]],"
                + "[1445986888000,[\"2.0540000000E01\"]]," + "[1445990845000,[\"2.0640000000E01\"]],"
                + "[1445990847000,[\"2.0540000000E01\"]]," + "[1445992558000,[\"2.0450000000E01\"]],"
                + "[1445998861000,[\"2.0540000000E01\"]]," + "[1446020329000,[\"2.0450000000E01\"]],"
                + "[1446025375000,[\"2.0350000000E01\"]]," + "[1446039860000,[\"2.0450000000E01\"]],"
                + "[1446048227000,[\"2.0350000000E01\"]]," + "[1446053687000,[\"2.0250000000E01\"]],"
                + "[1446061039000,[\"2.0360000000E01\"]]," + "[1446063806000,[\"2.0460000000E01\"]],"
                + "[1446068456000,[\"2.0360000000E01\"]]," + "[1446071016000,[\"2.0260000000E01\"]],"
                + "[1446083019000,[\"2.0160000000E01\"]]," + "[1446087977000,[\"2.0270000000E01\"]],"
                + "[1446087980000,[\"2.0160000000E01\"]]," + "[1446096819000,[\"2.0060000000E01\"]],"
                + "[1446104753000,[\"2.0160000000E01\"]]," + "[1446109980000,[\"2.0280000000E01\"]]" + "]");
        response.addHeader("Access-Control-Allow-Origin", "*");

        return response;
    }

    private IResponse handleHook(IHTTPSession session) {

        Map<String, List<String>> params = getParams(session);
        log.debug("hook params: {}", params);
        String ga = null;
        String value = null;
        List<String> gaParam = params.get("ga");
        if (gaParam != null) {
            ga = gaParam.get(0);
        }

        List<String> valueParam = params.get("value");
        if (valueParam != null) {
            value = valueParam.get(0);
        }

        if (ga != null && value != null) {
            try {
                knx.write(ga, value);
                log.info("Sent hook: ga=[{}] value=[{}]", ga, value);
            } catch (KnxServiceException ex) {
                log.error("Problem sending hook data ga=[" + ga + "] value=[" + value + "]", ex);
            }
        } else {
            log.warn("hook data invalid: {}", params);
        }

        Response response = new Response("OK");
        response.addHeader("Access-Control-Allow-Origin", "*");
        return response;
    }

}