001/** 002 * Copyright 2014 Tampere University of Technology, Pori Department 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package core.tut.pori.context; 017 018import java.io.IOException; 019import java.util.Collections; 020import java.util.Date; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.Iterator; 024import java.util.Map; 025import java.util.Set; 026 027import javax.websocket.CloseReason; 028import javax.websocket.Endpoint; 029import javax.websocket.EndpointConfig; 030import javax.websocket.MessageHandler; 031import javax.websocket.Session; 032import javax.websocket.server.ServerApplicationConfig; 033import javax.websocket.server.ServerEndpointConfig; 034 035import org.apache.log4j.Logger; 036import org.springframework.beans.BeansException; 037import org.springframework.context.support.ClassPathXmlApplicationContext; 038 039import core.tut.pori.utils.StringUtils; 040import core.tut.pori.websocket.Definitions; 041import core.tut.pori.websocket.SocketService; 042 043/** 044 * WebSocket Service end-point handler. 045 * 046 * This class can be used to retrieve initialized instances of WebSocket end-point services. 047 * 048 * One should not initialize this handler directly, as an instantiated version is available from ServiceInitializer. 049 */ 050public class WebSocketHandler { 051 /** websocket service uri */ 052 public static final String SERVICE_URI = "/websocket/"; 053 private static final Logger LOGGER = Logger.getLogger(WebSocketHandler.class); 054 private static final String PARAMETER_END_POINT = "end_point"; 055 private static final String SERVLET_CONFIGURATION_FILE = "websocket-servlet.xml"; 056 private static WebSocketEndpoint END_POINT = null; 057 private ClassPathXmlApplicationContext _context = null; 058 private Map<String, SocketService> _websockets = null; 059 060 /** 061 * 062 */ 063 public WebSocketHandler(){ 064 END_POINT = new WebSocketEndpoint(); 065 initialize(); 066 } 067 068 /** 069 * 070 */ 071 public void close(){ 072 _context.close(); 073 _context = null; 074 _websockets = null; 075 END_POINT = null; 076 } 077 078 /** 079 * Do NOT close or cleanup the instances returned by this method, the initialization and destruction is handled automatically. 080 * 081 * @param cls 082 * @return the service or null if not found 083 */ 084 public <T extends SocketService> T getSocketService(Class<T> cls){ 085 try{ 086 for(T candidate : _context.getBeansOfType(cls).values()){ 087 if(candidate.getClass().equals(cls)){ 088 return candidate; 089 } 090 } 091 } catch (BeansException ex){ 092 LOGGER.warn(ex, ex); 093 } 094 return null; 095 } 096 097 /** 098 * 099 */ 100 private void initialize(){ 101 LOGGER.debug("Initializing handler..."); 102 Date started = new Date(); 103 _context = new ClassPathXmlApplicationContext(core.tut.pori.properties.SystemProperty.CONFIGURATION_FILE_PATH+SERVLET_CONFIGURATION_FILE); 104 105 LOGGER.debug("Class Path XML Context initialized in "+StringUtils.getDurationString(started, new Date())); 106 107 Map<String, SocketService> services = _context.getBeansOfType(SocketService.class); 108 int count = services.size(); 109 LOGGER.info("Found "+count+" service(s)."); 110 _websockets = new HashMap<>(count); 111 112 for(Iterator<SocketService> iter = services.values().iterator();iter.hasNext();){ 113 addService(iter.next()); 114 } 115 116 LOGGER.debug("Web Socket Handler initialized in "+StringUtils.getDurationString(started, new Date())); 117 } 118 119 /** 120 * 121 * @param service 122 * @throws IllegalArgumentException 123 */ 124 private void addService(SocketService service) throws IllegalArgumentException { 125 String name = service.getEndPointName(); 126 if(name == null){ 127 throw new IllegalArgumentException("Invalid service name for "+service.getClass().toString()); 128 }else if(_websockets.containsKey(name)){ 129 throw new IllegalArgumentException("Duplicate Web Socket end point name "+name+" for "+service.getClass().toString()); 130 }else{ 131 _websockets.put(name, service); 132 } 133 } 134 135 /** 136 * WebSocket server end point 137 */ 138 private class WebSocketEndpoint extends Endpoint { 139 140 /** 141 * 142 */ 143 public WebSocketEndpoint(){ 144 LOGGER.debug("End point initialized at "+SERVICE_URI); 145 } 146 147 @Override 148 public void onClose(Session session, CloseReason closeReason) { 149 String endPointName = session.getPathParameters().get(PARAMETER_END_POINT); 150 SocketService socket = _websockets.get(endPointName); 151 if(socket == null){ 152 LOGGER.debug("Ignoring close on non-existent end point, name: "+endPointName); 153 }else{ 154 try{ 155 socket.onClose(session, closeReason); 156 } catch (Throwable ex){ 157 LOGGER.error(ex, ex); 158 } 159 } 160 } 161 162 @Override 163 public void onError(Session session, Throwable throwable) { 164 SocketService socket = _websockets.get(session.getPathParameters().get(PARAMETER_END_POINT)); 165 try{ 166 socket.onError(session, throwable); 167 } catch (IllegalArgumentException ex){ 168 try { 169 session.close(Definitions.CLOSE_REASON_BAD_REQUEST); 170 } catch (IOException ex1) { 171 LOGGER.debug(ex, ex1); 172 } 173 } catch (Throwable ex){ 174 try { 175 session.close(Definitions.CLOSE_REASON_INTERNAL_SERVER_ERROR); 176 } catch (IOException ex1) { 177 LOGGER.debug(ex, ex1); 178 } 179 } 180 } 181 182 @Override 183 public void onOpen(final Session session, EndpointConfig config) { 184 String endPointName = session.getPathParameters().get(PARAMETER_END_POINT); 185 final SocketService socket = _websockets.get(endPointName); 186 if(socket == null){ 187 LOGGER.warn("Closing session to non-existent end point, name: "+endPointName); 188 try { 189 session.close(Definitions.CLOSE_REASON_NOT_FOUND); 190 } catch (IOException ex) { 191 LOGGER.debug(ex, ex); 192 } 193 return; 194 } 195 196 try{ 197 if(socket.accept(session)){ 198 session.addMessageHandler(new MessageHandler.Whole<String>() { 199 @Override 200 public void onMessage(String message) { 201 socket.received(session, message); 202 } 203 }); 204 }else{ 205 LOGGER.debug("Closing rejected session."); 206 try { 207 session.close(Definitions.CLOSE_REASON_UNAUTHORIZED); 208 } catch (IOException ex) { 209 LOGGER.debug(ex, ex); 210 } 211 } 212 } catch (IllegalArgumentException ex){ 213 try { 214 session.close(Definitions.CLOSE_REASON_BAD_REQUEST); 215 } catch (IOException ex1) { 216 LOGGER.debug(ex, ex1); 217 } 218 } catch (Throwable ex){ 219 try { 220 session.close(Definitions.CLOSE_REASON_INTERNAL_SERVER_ERROR); 221 } catch (IOException ex1) { 222 LOGGER.debug(ex, ex1); 223 } 224 } 225 } 226 } // class WebSocketEndpoint 227 228 /** 229 * Server End-point configurator. 230 * 231 * This class binds the WebSocketEndpoint to accessible uri on the server. 232 */ 233 public static class WebSocketHandlerConfigurator implements ServerApplicationConfig{ 234 235 @Override 236 public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> set) { 237 return Collections.emptySet(); 238 } 239 240 @Override 241 public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> set) { 242 Set<ServerEndpointConfig> configs = new HashSet<>(); 243 configs.add(ServerEndpointConfig.Builder. 244 create(WebSocketEndpoint.class, SERVICE_URI+"{"+PARAMETER_END_POINT+"}") 245 .configurator(new ServerEndpointConfig.Configurator(){ 246 @SuppressWarnings("unchecked") 247 @Override 248 public <T> T getEndpointInstance(Class<T> cls) throws InstantiationException { 249 return (T) END_POINT; 250 } 251 }) 252 .build() 253 ); 254 return configs; 255 } 256 } // class WebSocketHandlerConfigurator 257}