All Downloads are FREE. Search and download functionalities are using the official Maven repository.

osgi.enroute.easse.simple.adapter.ServerSideEventImpl Maven / Gradle / Ivy

Go to download

Provides a mapping from Event Admin events to Javascript Server Side Events (SSE). This bundle registers under /sse/1, the remaining path is treated as the topic. It will then send all matching events to the browser through SSE. The type of the event is org.osgi.service.eventadmin;topic=%s, the data payload is a JSON representation of the event properties.

The newest version!
package osgi.enroute.easse.simple.adapter;

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import javax.servlet.Servlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.osgi.service.http.whiteboard.HttpWhiteboardConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import aQute.lib.json.JSONCodec;
import osgi.enroute.http.capabilities.RequireHttpImplementation;

/**
 * This component provides a servlet that allows javascript clients to see the
 * Event Admin events. It blocks the request thread until it gets killed.
 * 

* The request path is treated as an EventAdmin topic filter, it is the * EVENT_TOPIC service property on an Event Handler service. *

* If the client registers with an {@code instance=} then the request thread * can be killed from another request by specifying {@code abort=}. This was * necessary because automatic closing did not work very well. *

* For IE-9, this article was very helpfull: Comet Streaming in Explorer with XMLHttpRequest and XDomainRequest */ @RequireHttpImplementation @Component(name = "osgi.eventadmin.sse", property = HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN + "=/sse/1/*", service = Servlet.class, configurationPolicy = ConfigurationPolicy.OPTIONAL) public class ServerSideEventImpl extends HttpServlet { final static Logger log = LoggerFactory.getLogger(ServerSideEventImpl.class); private static final long serialVersionUID = 1L; private static JSONCodec codec = new JSONCodec(); private static byte[] prelude; private static Random random = new SecureRandom(); final Map threads = new ConcurrentHashMap(); BundleContext context; @Activate void activate(BundleContext context) { this.context = context; } @Deactivate void deactivate() { for (Thread out : threads.values()) { out.interrupt(); } } @Override public void doGet(HttpServletRequest rq, HttpServletResponse rsp) throws IOException { // // First some house cleaning. The caller can abort // a previous connection. The request then passes abort=instanceId. // String instanceId = rq.getParameter("abort"); if (instanceId != null) { kill(instanceId); rsp.setStatus(HttpServletResponse.SC_OK); return; } // // Now we need to get an instance id, if it does not // exist than we create a random one. // instanceId = rq.getParameter("instance"); if (instanceId == null) instanceId = random.nextLong() + ""; else kill(instanceId); String path = rq.getPathInfo(); if (path == null || path.isEmpty()) { rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Incorrect path " + path); return; } final String topic = path.substring(1); // skip leading / // // Access-Control-Allow-Origin header, seems crucial for IE-9 although // I do not know why?? // rsp.setHeader("Access-Control-Allow-Origin", "*"); rsp.setContentType("text/event-stream;charset=utf-8"); final Thread thread = Thread.currentThread(); try (OutputStream out = rsp.getOutputStream()) { // // We need to clean up old connections from this process // The caller gives us an instance id, which it calculates per // JS/page. If this same page reconnects, we kill the old // connection // threads.put(instanceId, thread); final PrintStream pout = new PrintStream(out); final BlockingQueue eventQueue = new LinkedBlockingQueue(20); final AtomicReference ref = new AtomicReference(out); ServiceRegistration registration = register(topic, eventQueue, instanceId, ref, thread); try { // // The 'programmers' at M$ implemented streaming but forgot // about a // lower layer's buffering. So to make streaming work, we must // send // a 2k prelude // String userAgent = rq.getHeader("User-Agent"); if (userAgent != null && userAgent.contains("MSIE 9.")) { out.write(getPrelude()); out.flush(); } pout.printf(": welcome\n\n"); pout.flush(); while (true) { Event event = eventQueue.poll(2, TimeUnit.SECONDS); if (event == null) { pout.print(":\n\n"); } else { Map props = new HashMap(); for (String name : event.getPropertyNames()) { props.put(name, event.getProperty(name)); } pout.printf("type: org.osgi.service.eventadmin;topic=%s\n", topic); String json = codec.enc() .put(props) .toString(); pout.printf("data: %s\n\n", json); } pout.flush(); } } catch (InterruptedException ie) { rsp.setStatus(HttpServletResponse.SC_OK); } catch (Exception e) { log.info("Quiting {}", topic, e); // time to close ... } finally { threads.remove(instanceId); registration.unregister(); if (ref.getAndSet(null) == null) { // // A little grace period since we could be interrupted // and do not want to kill the next request try { Thread.sleep(10); } catch (InterruptedException e) { // OK, we we're hoping for it } } } } } /** * @param topic * @param eventQueue * @param instanceId * @param out * @return */ private ServiceRegistration register(final String topic, final BlockingQueue eventQueue, String instanceId, final AtomicReference out, final Thread thread) { Hashtable p = new Hashtable(); p.put(EventConstants.EVENT_TOPIC, topic); p.put("instance.id", instanceId); ServiceRegistration registration = context.registerService(EventHandler.class.getName(), new EventHandler() { @Override public synchronized void handleEvent(Event event) { if (eventQueue.offer(event)) return; // // Our queue is filling up, this is likely caused by // a dead SSE thread (browser closed without warning // us. So we kill it // try (Closeable o = out.getAndSet(null)) { if (o == null) // // Already killed // return; log.warn("Killing orphaned GUI thread beause queue is full"); // // First interrupt it so we kill it nicely // try { thread.interrupt(); // // Then the hammer to kill for real // o.close(); } catch (IOException e) {} } catch (IOException e1) { throw new RuntimeException(e1); } } }, p); return registration; } /** * Kill a running instance * * @param instanceId */ private void kill(String instanceId) { Thread previous = threads.get(instanceId); if (previous != null) { previous.interrupt(); } } /** * Get the MSIE-9 prelude. There is an puny chance this method would * initialize multiple times, but that is ok,. */ private static byte[] getPrelude() { if (prelude == null) { prelude = new byte[2048]; prelude[0] = ':'; for (int i = 1; i < prelude.length - 1; i++) prelude[i] = ' '; prelude[prelude.length - 1] = '\n'; } return prelude; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy