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}