package er.ajax; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WORequestHandler; import com.webobjects.appserver.WOResponse; import com.webobjects.appserver.WOSession; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; import er.extensions.appserver.ERXKeepAliveResponse; import er.extensions.foundation.ERXSelectorUtilities; /** * Request handler that offers push-style notifications. <br> * Gets registered under "/push/" on framework load.<br> * You should open an Ajax.Request, implement onInteractive: and the do * something useful when you get new data. Changes should be pushed with * push(sessionID, someString); * <h3>TODO:</h3> * <ul> * <li>currently the request stays open even when the client closed it (which is bad) * <li>implement a boundary scheme to tell when a "message" is complete. This * means we need a special Ajax.Request that does it. * <li>implement various client-side stuff to be actually useful (chats, EO * notifications). * <li>ask Frank about his EO layer * <li>use the request handler path as a "topic", so we can have more than one on a page. * </ul> * * @author ak */ public class AjaxPushRequestHandler extends WORequestHandler { public static final String AjaxCometRequestHandlerKey = "push"; private static ConcurrentHashMap<String, ConcurrentHashMap<String, ERXKeepAliveResponse>> responses = new ConcurrentHashMap<String, ConcurrentHashMap<String, ERXKeepAliveResponse>>(); public AjaxPushRequestHandler() { NSNotificationCenter.defaultCenter().addObserver(this, ERXSelectorUtilities.notificationSelector("sessionDidTimeOut"), WOSession.SessionDidTimeOutNotification, null); } /** * Remove stale responses when a session times out. * * @param n the session timeout notification */ public void sessionDidTimeOut(NSNotification n) { String id = (String) n.object(); ConcurrentHashMap<String, ERXKeepAliveResponse> sessionResponses = responses.get(id); if (sessionResponses != null) { for (ERXKeepAliveResponse response : sessionResponses.values()) { response.reset(); } responses.remove(id); } } /** * Get/Create the current request for the session and return it. * * @param request the request */ @Override public WOResponse handleRequest(WORequest request) { String sessionID = request.sessionID(); String name = request.requestHandlerPath(); ERXKeepAliveResponse response = responseForSessionIDNamed(sessionID, name); response.reset(); return response; } /** * Return or create the correct response for the session ID. * * @param sessionID the session id of the response * @param name the name of the response * @return response for ID */ private static ERXKeepAliveResponse responseForSessionIDNamed(String sessionID, String name) { ERXKeepAliveResponse response = null; if (sessionID != null) { if(name == null) { name = ""; } ConcurrentHashMap<String, ERXKeepAliveResponse> sessionResponses = responses.get(sessionID); if (sessionResponses == null) { ConcurrentHashMap<String, ERXKeepAliveResponse> newSessionResponses = new ConcurrentHashMap<>(); ConcurrentHashMap<String, ERXKeepAliveResponse> prevSessionResponses = responses.putIfAbsent(sessionID, newSessionResponses); sessionResponses = (prevSessionResponses == null) ? newSessionResponses : prevSessionResponses; } response = sessionResponses.get(name); if (response == null) { ERXKeepAliveResponse newResponse = new ERXKeepAliveResponse(); ERXKeepAliveResponse prevResponse = sessionResponses.putIfAbsent(name, newResponse); response = (prevResponse == null) ? newResponse : prevResponse; } } return response; } /** * Returns whether or not there is a response open for the given session id and name. * * @param sessionID the session id of the push response * @param name the name of the push response * @return whether or not there is still a response open */ public static boolean isResponseOpen(String sessionID, String name) { ERXKeepAliveResponse response = responseForSessionIDNamed(sessionID, name); return response != null; } /** * Push a string message to the client. At the moment, there is no boundary * handling, so be aware that you could get only half of a message. * * @param sessionID the session id of the push response * @param name the name of the push response */ public static void stop(String sessionID, String name) { Map<String, ERXKeepAliveResponse> sessionResponses = responses.get(sessionID); if (sessionResponses != null) { ERXKeepAliveResponse response = sessionResponses.get(name); if (response != null) { response.reset(); sessionResponses.remove(name); } // not going to do an empty check on sessionResponses, because we'd have to synchronize on // the top-level responses to do it safely } } /** * Push a string message to the client. At the moment, there is no boundary * handling, so be aware that you could get only half of a message. * * @param sessionID the session id of the push response * @param name the name of the push response * @param message the message to push */ public static void push(String sessionID, String name, String message) { ERXKeepAliveResponse response = responseForSessionIDNamed(sessionID, name); if (response != null) { StringBuilder sb = new StringBuilder(); sb.append(message.length()); sb.append(':'); response.push(sb.toString()); response.push(message); } } /** * Push a data message to the client. At the moment, there is no boundary * handling, so be aware that you could get only half of a message. * * @param sessionID the session id of the push response * @param name the name of the push response * @param message the message to push */ public static void push(String sessionID, String name, NSData message) { ERXKeepAliveResponse response = responseForSessionIDNamed(sessionID, name); if (response != null) { StringBuilder sb = new StringBuilder(); sb.append(message.length()); sb.append(':'); response.push(sb.toString()); response.push(message.bytes()); } } }