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