/* * Copyright 2011 Future Systems * * 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 org.krakenapps.webconsole.servlet; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.UnknownHostException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Invalidate; import org.apache.felix.ipojo.annotations.Requires; import org.apache.felix.ipojo.annotations.Validate; import org.krakenapps.httpd.HttpContext; import org.krakenapps.httpd.HttpService; import org.krakenapps.msgbus.AbstractSession; import org.krakenapps.msgbus.Message; import org.krakenapps.msgbus.Message.Type; import org.krakenapps.msgbus.MessageBus; import org.krakenapps.msgbus.Session; import org.krakenapps.webconsole.impl.KrakenMessageDecoder; import org.krakenapps.webconsole.impl.KrakenMessageEncoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component(name = "webconsole-msgbus-servlet") public class MsgbusServlet extends HttpServlet implements Runnable { private static final long serialVersionUID = 1L; private final Logger logger = LoggerFactory.getLogger(MsgbusServlet.class); /** * msgbus session id to waiting async context mappings */ private ConcurrentMap<String, AsyncContext> contexts; /** * msgbus session id to pending messages mappings */ private ConcurrentMap<String, Queue<String>> pendingQueues; /** * pending msgbus requests */ private ConcurrentMap<String, HttpServletResponse> pendingRequests; @Requires private MessageBus msgbus; @Requires private HttpService httpd; /** * periodic blocking checker */ private Thread t; private boolean doStop; public MsgbusServlet() { contexts = new ConcurrentHashMap<String, AsyncContext>(); pendingQueues = new ConcurrentHashMap<String, Queue<String>>(); pendingRequests = new ConcurrentHashMap<String, HttpServletResponse>(); } public void setMessageBus(MessageBus msgbus) { this.msgbus = msgbus; } public void setHttpService(HttpService httpd) { this.httpd = httpd; } @Validate public void start() { doStop = false; HttpContext ctx = httpd.ensureContext("webconsole"); ctx.addServlet("msgbus", this, "/msgbus/*"); t = new Thread(this, "Msgbus Push"); t.start(); } @Invalidate public void stop() { if (httpd != null) { HttpContext ctx = httpd.ensureContext("webconsole"); ctx.removeServlet("msgbus"); } doStop = true; t.interrupt(); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Session session = ensureSession(req); if (req.getPathInfo().equals("/trap")) { Queue<String> q = pendingQueues.get(session.getGuid()); if (q != null && !q.isEmpty()) { logger.trace("kraken webconsole: flush queued traps [session={}]", session.getGuid()); flushTraps(q, resp.getOutputStream()); } else { logger.trace("kraken webconsole: waiting msgbus response/trap [session={}]", session.getGuid()); AsyncContext aCtx = req.startAsync(); contexts.put(session.getGuid(), aCtx); } } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String sessionId = req.getSession().getId(); logger.debug("kraken webconsole: msgbus post from http session [{}], data [{}]", sessionId, formatSessionData(req.getSession())); if (req.getPathInfo().equals("/request")) { Session session = ensureSession(req); ByteArrayOutputStream os = new ByteArrayOutputStream(); byte[] b = new byte[4096]; while (true) { int readBytes = req.getInputStream().read(b); if (readBytes < 0) break; os.write(b, 0, readBytes); } String text = os.toString("utf-8"); if (text != null && text.trim().isEmpty()) { logger.debug("kraken webconsole: empty text from [{}], request [{}]", req.getRemoteAddr(), req.getRequestURI()); resp.sendError(500); return; } Message msg = KrakenMessageDecoder.decode(session, text); pendingRequests.putIfAbsent(msg.getGuid(), resp); msgbus.execute(session, msg); } } private Session ensureSession(HttpServletRequest req) throws UnknownHostException { String sessionId = req.getSession().getId(); logger.trace("kraken webconsole: using http session [{}]", sessionId); Session session = msgbus.getSession(sessionId); if (session == null) { session = new HttpMsgbusSession(sessionId, InetAddress.getByName(req.getRemoteAddr()), InetAddress.getByName(req .getLocalAddr())); msgbus.openSession(session); } return session; } @Override public void run() { try { logger.info("kraken webconsole: msgbus push thread started"); while (!doStop) { try { runOnce(); Thread.sleep(100); } catch (InterruptedException e) { logger.info("kraken webconsole: msgbus push thread interrupted"); } } } finally { logger.info("kraken webconsole: msgbus push thread stopped"); } } private void runOnce() throws InterruptedException { for (String sessionId : contexts.keySet()) { Queue<String> frames = pendingQueues.get(sessionId); if (frames == null || frames.size() == 0) continue; flushAsyncTraps(sessionId, frames); } } private void flushAsyncTraps(String sessionId, Queue<String> frames) { AsyncContext ctx = contexts.get(sessionId); if (ctx == null) return; try { synchronized (ctx) { OutputStream os = ctx.getResponse().getOutputStream(); flushTraps(frames, os); } } catch (IOException e) { logger.error("kraken webconsole: cannot send pending msg", e); } finally { ctx.complete(); contexts.remove(sessionId); } } private void flushTraps(Queue<String> frames, OutputStream os) throws IOException, UnsupportedEncodingException { os.write("[".getBytes()); int i = 0; while (true) { String frame = frames.poll(); if (frame == null) break; if (i != 0) os.write(",".getBytes()); logger.trace("kraken webconsole: trying to send pending frame [{}]", frame); os.write(frame.getBytes("utf-8")); i++; } os.write("]".getBytes()); } private class HttpMsgbusSession extends AbstractSession { private String sessionId; private InetAddress remoteAddr; private InetAddress localAddr; public HttpMsgbusSession(String sessionId, InetAddress remoteAddr, InetAddress localAddr) { this.sessionId = sessionId; this.remoteAddr = remoteAddr; this.localAddr = localAddr; } @Override public String getGuid() { return sessionId; } @Override public InetAddress getLocalAddress() { return localAddr; } @Override public InetAddress getRemoteAddress() { return remoteAddr; } @Override public void send(Message msg) { if (msg.getType() == Type.Trap) { pendingQueues.putIfAbsent(msg.getSession(), new ConcurrentLinkedQueue<String>()); Queue<String> frames = pendingQueues.get(msg.getSession()); String payload = KrakenMessageEncoder.encode(this, msg); frames.add(payload); if (contexts.containsKey(msg.getSession())) { logger.debug("kraken webconsole: sending trap immediately [session={}, payload={}]", msg.getSession(), payload); flushAsyncTraps(msg.getSession(), frames); } else logger.debug("kraken webconsole: queueing trap [session={}, payload={}]", msg.getSession(), payload); } else if (msg.getType() == Type.Response) { HttpServletResponse resp = pendingRequests.remove(msg.getRequestId()); if (resp != null) { Session session = msgbus.getSession(sessionId); if (session != null) { String payload = KrakenMessageEncoder.encode(session, msg); try { logger.trace("kraken webconsole: trying to send response [{}]", payload); resp.getOutputStream().write(payload.getBytes("utf-8")); } catch (IOException e) { logger.error("kraken webconsole: cannot send response [{}]", payload); } finally { try { resp.getOutputStream().close(); } catch (IOException e) { logger.error("kraken webconsole: cannot close http response stream", e); } } } else { logger.error("kraken webconsole: msgbus session [{}] not found", sessionId); } } else { logger.error("kraken webconsole: http response lost for msgbus req [{}]", msg.getRequestId()); } } } @Override public String toString() { return "msgbus session=" + sessionId + ", remote=" + getRemoteAddress(); } } private static String formatSessionData(HttpSession session) { String sessiondata = ""; if (session != null) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date since = new Date(session.getCreationTime()); Date lastAccess = new Date(session.getLastAccessedTime()); sessiondata = String.format("jsession=%s, since=%s, lastaccess=%s", session.getId(), dateFormat.format(since), dateFormat.format(lastAccess)); } return sessiondata; } @Override public String toString() { return "async contexts=" + contexts; } }