/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2014 Wisdom Framework * %% * 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. * #L% */ package org.wisdom.router; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.NullNode; import org.apache.felix.ipojo.annotations.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.wisdom.api.Controller; import org.wisdom.api.annotations.Closed; import org.wisdom.api.annotations.OnMessage; import org.wisdom.api.annotations.Opened; import org.wisdom.api.concurrent.ManagedExecutorService; import org.wisdom.api.content.ContentEngine; import org.wisdom.api.content.ParameterFactories; import org.wisdom.api.http.websockets.Publisher; import org.wisdom.api.http.websockets.WebSocketDispatcher; import org.wisdom.api.http.websockets.WebSocketListener; import org.wisdom.api.router.RouteUtils; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; /** * Component handling web socket frame routing. */ @Component(immediate = true) @Provides(specifications = Publisher.class) @Instantiate(name = "WebSocketRouter") public class WebSocketRouter implements WebSocketListener, Publisher { private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketRouter.class); @Requires WebSocketDispatcher[] dispatchers; Set<DefaultWebSocketCallback> opens = new LinkedHashSet<>(); Set<DefaultWebSocketCallback> closes = new LinkedHashSet<>(); Set<OnMessageWebSocketCallback> listeners = new LinkedHashSet<>(); @Requires(optional = true) private ContentEngine contentEngine; @Requires(optional = true) ParameterFactories converter; @Requires(filter = "(name=" + ManagedExecutorService.SYSTEM + ")") ManagedExecutorService executor; /** * @return the logger. */ public static Logger getLogger() { return LOGGER; } /** * Registers the current router in the dispatcher. */ @Bind(aggregate = true) public void bindDispatcher(WebSocketDispatcher dispatcher) { dispatcher.register(this); } /** * Unregisters the current router in the dispatcher. */ @Unbind public void unbindDispatcher(WebSocketDispatcher dispatcher) { dispatcher.unregister(this); } @Invalidate public void stop() { for (WebSocketDispatcher dispatcher : dispatchers) { dispatcher.unregister(this); } } /** * Binds a new controller. * * @param controller the new controller */ @Bind(aggregate = true) public synchronized void bindController(Controller controller) { analyze(controller); } /** * @return the parameter converter. */ public ParameterFactories converter() { return converter; } /** * @return the content engine. */ public ContentEngine engine() { return contentEngine; } /** * Extracts all the annotations from the controller's method. * * @param controller the controller to analyze */ private void analyze(Controller controller) { String prefix = RouteUtils.getPath(controller); Method[] methods = controller.getClass().getMethods(); for (Method method : methods) { Opened open = method.getAnnotation(Opened.class); Closed close = method.getAnnotation(Closed.class); OnMessage on = method.getAnnotation(OnMessage.class); if (open != null) { DefaultWebSocketCallback callback = new DefaultWebSocketCallback(controller, method, RouteUtils.getPrefixedUri(prefix, open.value()), this); if (callback.check()) { opens.add(callback); } } if (close != null) { DefaultWebSocketCallback callback = new DefaultWebSocketCallback(controller, method, RouteUtils.getPrefixedUri(prefix, close.value()), this); if (callback.check()) { closes.add(callback); } } if (on != null) { OnMessageWebSocketCallback callback = new OnMessageWebSocketCallback(controller, method, RouteUtils.getPrefixedUri(prefix, on.value()), this); if (callback.check()) { listeners.add(callback); } } } } /** * Unbinds the controller. * * @param controller the leaving controller */ @Unbind public synchronized void unbindController(Controller controller) { List<DefaultWebSocketCallback> toRemove = new ArrayList<>(); for (DefaultWebSocketCallback open : opens) { if (open.getController() == controller) { toRemove.add(open); } } opens.removeAll(toRemove); toRemove.clear(); for (DefaultWebSocketCallback close : closes) { if (close.getController() == controller) { toRemove.add(close); } } closes.removeAll(toRemove); toRemove.clear(); for (DefaultWebSocketCallback callback : listeners) { if (callback.getController() == controller) { toRemove.add(callback); } } listeners.removeAll(toRemove); //NOSONAR type is correct here. } /** * Handles the reception of a message. * * @param uri the url of the web socket * @param from the client having sent the message (octal id). * @param content the received content */ @Override public void received(final String uri, final String from, final byte[] content) { for (final OnMessageWebSocketCallback listener : listeners) { if (listener.matches(uri)) { executor.submit(new Callable<Void>() { @Override public Void call() throws Exception { try { listener.invoke(uri, from, content); } catch (InvocationTargetException e) { //NOSONAR LOGGER.error("An error occurred in the @OnMessage callback {}#{} : {}", listener.getController().getClass().getName(), listener.getMethod().getName (), e.getTargetException().getMessage(), e.getTargetException() ); } catch (Exception e) { LOGGER.error("An error occurred in the @OnMessage callback {}#{} : {}", listener.getController().getClass().getName(), listener.getMethod().getName(), e.getMessage(), e); } return null; } }); } } } /** * Handles the registration of a new client on the web socket. * * @param uri the url of the web socket * @param client the client id */ @Override public void opened(String uri, String client) { for (DefaultWebSocketCallback open : opens) { if (open.matches(uri)) { try { open.invoke(uri, client, null); } catch (InvocationTargetException e) { //NOSONAR LOGGER.error("An error occurred in the @Open callback {}#{} : {}", open.getController().getClass().getName(), open.getMethod().getName (), e.getTargetException().getMessage(), e.getTargetException() ); } catch (Exception e) { LOGGER.error("An error occurred in the @Open callback {}#{} : {}", open.getController().getClass().getName(), open.getMethod().getName(), e.getMessage(), e); } } } } /** * Handles the disconnection of a client from the web socket. * * @param uri the url of the web socket * @param client the client id */ @Override public void closed(String uri, String client) { for (DefaultWebSocketCallback close : closes) { if (close.matches(uri)) { try { close.invoke(uri, client, null); } catch (InvocationTargetException e) { //NOSONAR LOGGER.error("An error occurred in the @Close callback {}#{} : {}", close.getController().getClass().getName(), close.getMethod().getName (), e.getTargetException().getMessage(), e.getTargetException() ); } catch (Exception e) { LOGGER.error("An error occurred in the @Close callback {}#{} : {}", close.getController().getClass().getName(), close.getMethod().getName(), e.getMessage(), e); } } } } /** * Publishes a text message on the web socket. * All client's connected to the given websocket receive the message. * * @param uri the websocket's url * @param message the message (text) */ @Override public void publish(String uri, String message) { if (message == null) { LOGGER.warn("Cannot send websocket message on {}, the message is null", uri); return; } for (WebSocketDispatcher dispatcher : dispatchers) { dispatcher.publish(uri, message); } } /** * Publishes a binary message on the web socket. * All client's connected to the given websocket receive the message. * * @param uri the websocket's url * @param message the data (binary) */ @Override public void publish(String uri, byte[] message) { if (message == null) { LOGGER.warn("Cannot send websocket message on {}, the message is null", uri); return; } for (WebSocketDispatcher dispatcher : dispatchers) { dispatcher.publish(uri, message); } } /** * Publishes a JSON message on the web socket. * All client's connected to the given websocket receive the message. * * @param uri the websocket's url * @param message the data (JSON) */ @Override public void publish(String uri, JsonNode message) { for (WebSocketDispatcher dispatcher : dispatchers) { if (message == null) { dispatcher.publish(uri, NullNode.getInstance().toString()); } else { dispatcher.publish(uri, message.toString()); } } } /** * Sends the given text message to the identified client. If the client is not connected on the web socket, * nothing happens. * * @param uri the websocket's url * @param client the client that is going to receive the message * @param message the data (text) */ @Override public void send(String uri, String client, String message) { if (message == null || client == null) { LOGGER.warn("Cannot send websocket message on {}, either the client id is null ({}) of the message is " + "null ({})", uri, client, message); return; } for (WebSocketDispatcher dispatcher : dispatchers) { dispatcher.send(uri, client, message); } } /** * Sends the given json message to the identified client. If the client is not connected on the web socket, * nothing happens. * * @param uri the websocket's url * @param client the client that is going to receive the message * @param message the data (json) */ @Override public void send(String uri, String client, JsonNode message) { for (WebSocketDispatcher dispatcher : dispatchers) { if (message == null) { dispatcher.send(uri, client, NullNode.getInstance().toString()); } else { dispatcher.send(uri, client, message.toString()); } } } /** * Sends the given binary message to the identified client. If the client is not connected on the web socket, * nothing happens. * * @param uri the websocket's url * @param client the client that is going to receive the message * @param message the data (binary) */ @Override public void send(String uri, String client, byte[] message) { if (message == null || client == null) { LOGGER.warn("Cannot send websocket message on {}, either the client id is null ({}) of the message is " + "null ({})", uri, client, message); return; } for (WebSocketDispatcher dispatcher : dispatchers) { dispatcher.send(uri, client, message); } } }