/* * Copyright 2011 Future Systems, Inc * * 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. */ package org.krakenapps.msgbus.impl; import java.lang.reflect.InvocationTargetException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Invalidate; import org.apache.felix.ipojo.annotations.Provides; import org.apache.felix.ipojo.annotations.Requires; import org.apache.felix.ipojo.annotations.Validate; import org.krakenapps.confdb.Config; import org.krakenapps.confdb.ConfigDatabase; import org.krakenapps.confdb.ConfigService; import org.krakenapps.cron.PeriodicJob; import org.krakenapps.msgbus.Message; import org.krakenapps.msgbus.MessageBus; import org.krakenapps.msgbus.MessageHandler; import org.krakenapps.msgbus.MsgbusConfig; import org.krakenapps.msgbus.MsgbusException; import org.krakenapps.msgbus.MessageListener; import org.krakenapps.msgbus.PermissionChecker; import org.krakenapps.msgbus.Request; import org.krakenapps.msgbus.ResourceApi; import org.krakenapps.msgbus.ResourceHandler; import org.krakenapps.msgbus.Response; import org.krakenapps.msgbus.Session; import org.krakenapps.msgbus.SessionEventHandler; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import org.osgi.util.tracker.ServiceTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component(name = "msgbus") @Provides @PeriodicJob("* * * * *") public class MessageBusImpl extends ServiceTracker implements MessageBus, Runnable { private final Logger logger = LoggerFactory.getLogger(MessageBusImpl.class.getName()); private static Integer defaultSessionTimeout = 30; private int timeout; private ConcurrentMap<String, Set<String>> pluginMessageMap; private ConcurrentMap<String, Session> sessionMap; private Set<SessionEventHandler> sessionEventListeners; private ConcurrentMap<String, Set<MessageHandler>> messageHandlerMap; private Set<PermissionChecker> permissionCheckers = Collections .newSetFromMap(new ConcurrentHashMap<PermissionChecker, Boolean>()); private final ExecutorService threadPool; private CopyOnWriteArraySet<MessageListener> listeners; @Requires private ResourceApi resourceApi; @Requires private ConfigService conf; public MessageBusImpl(BundleContext bc) { super(bc, PermissionChecker.class.getName(), null); timeout = 0; pluginMessageMap = new ConcurrentHashMap<String, Set<String>>(); sessionMap = new ConcurrentHashMap<String, Session>(); sessionEventListeners = Collections.newSetFromMap(new ConcurrentHashMap<SessionEventHandler, Boolean>()); messageHandlerMap = new ConcurrentHashMap<String, Set<MessageHandler>>(); listeners = new CopyOnWriteArraySet<MessageListener>(); threadPool = Executors.newCachedThreadPool(); } @Validate public void start() { ConfigDatabase db = conf.ensureDatabase("kraken-msgbus"); Config c = db.findOne(MsgbusConfig.class, null); MsgbusConfig msgbusConfig = ensureMsgbusConfig(db, c); timeout = msgbusConfig.getTimeout() == null ? defaultSessionTimeout : msgbusConfig.getTimeout(); super.open(); } @Invalidate public void stop() { super.close(); } @Override public void run() { Collection<Session> sessions = getSessions(); Date currentTime = new Date(); for (Session s : sessions) { long diff = (currentTime.getTime() - s.getLastAccessTime().getTime()) / (1000 * 60); if (logger.isDebugEnabled()) logger.debug("kraken msgbus: session [{}] killed by timeout checker, diff time [{}], now [{}], access [{}]", new Object[] { s.getGuid(), diff, currentTime, s.getLastAccessTime() }); if (diff > timeout) { closeSession(s); logger.info("kraken msgbus: session timeout, session guid [{}], timeout [{}]", s.getGuid(), getSessionTimeout()); } } } @Override public boolean checkPermission(Session session, String group, String code) { for (PermissionChecker checker : permissionCheckers) if (!checker.check(session, group, code)) return false; return true; } @Override public Object addingService(ServiceReference reference) { PermissionChecker checker = (PermissionChecker) super.addingService(reference); permissionCheckers.add(checker); logger.trace("kraken msgbus: new permission checker installed, " + checker); return checker; } @Override public void removedService(ServiceReference reference, Object service) { PermissionChecker checker = (PermissionChecker) service; permissionCheckers.remove(checker); logger.trace("kraken msgbus: permission checker uninstalled, " + checker); super.removedService(reference, service); } public Collection<String> getPluginNames() { Set<String> set = new HashSet<String>(); for (String pluginName : pluginMessageMap.keySet()) set.add(pluginName); return set; } public Collection<String> getMethodNames(String pluginName) { Set<String> methodTypes = pluginMessageMap.get(pluginName); if (methodTypes == null) return null; Set<String> copySet = new HashSet<String>(); for (String methodType : methodTypes) { copySet.add(methodType); } return copySet; } public Collection<Session> getSessions() { Set<Session> sessions = new HashSet<Session>(); for (String key : sessionMap.keySet()) { Session session = sessionMap.get(key); if (session != null) sessions.add(session); } return sessions; } @Override public Session getSession(String guid) { Session session = sessionMap.get(guid); if (session == null) return null; session.setLastAccessTime(); return session; } @Override public Session getSession(int id) { return getSession(Integer.toString(id)); } @Override public Message execute(Session session, Message message) { if (session == null) throw new IllegalArgumentException("session should be not null"); if (message == null) throw new IllegalArgumentException("message should be not null"); logger.trace("kraken msgbus: executing method [{}]", message.getMethod()); Set<MessageHandler> handlers = messageHandlerMap.get(message.getMethod()); if (handlers == null || handlers.size() == 0) { session.send(Message.createError(session, message, "msgbus-handler-not-found", message.getMethod())); logger.warn("kraken msgbus: handler not found. discarded [{}]", message.getMethod()); throw new IllegalStateException("msgbus-handler-not-found"); } TaskRunner runner = new TaskRunner(session, message, handlers.iterator().next()); runner.run(); return runner.respondMessage; } private void triggerListener(Session session, Message message) { for (MessageListener listener : listeners) { try { listener.onMessage(session, message); } catch (Throwable t) { logger.warn("kraken msgbus: msgbus call listener should not throw any exception", t); } } } @Override public void dispatch(Session session, Message message) { logger.trace("kraken msgbus: dispatching message [{}]", message.getMethod()); Set<MessageHandler> handlers = messageHandlerMap.get(message.getMethod()); if (handlers == null || handlers.size() == 0) { session.send(Message.createError(session, message, "msgbus-handler-not-found", message.getMethod())); logger.warn("kraken msgbus: handler not found. discarded [{}]", message.getMethod()); return; } for (MessageHandler handler : handlers) { threadPool.execute(new TaskRunner(session, message, handler)); } triggerListener(session, message); } @SuppressWarnings("unchecked") public void send(Message message) { Session session = sessionMap.get(message.getSession()); if (session == null) { logger.warn("kraken msgbus: session not found. [{}] message will be discarded.", message.getMethod()); return; } logger.debug("kraken msgbus: sending message [{}] to session [{}]", message.getMethod(), message.getSession()); Map<String, Object> m = message.getParameters(); message.setParameters((Map<String, Object>) convert(m)); session.send(message); message.setParameters(m); } private Object convert(Object value) { if (value == null) return null; if (value instanceof Date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ"); return sdf.format((Date) value); } else if (value instanceof Map) { Map<?, ?> m = (Map<?, ?>) value; Map<Object, Object> mm = new HashMap<Object, Object>(); for (Object key : m.keySet()) mm.put(convert(key), convert(m.get(key))); return mm; } else if (value instanceof Collection) { return convertList((Collection<?>) value); } else if (value.getClass().isArray()) { try { return convertList(Arrays.asList((Object[]) value)); } catch (ClassCastException e) { return value; } } return value; } private Object convertList(Collection<?> value) { List<Object> list = new ArrayList<Object>(); for (Object obj : value) list.add(convert(obj)); return list; } public void openSession(Session session) { sessionMap.put(session.getGuid(), session); for (SessionEventHandler handler : sessionEventListeners) { handler.sessionOpened(session); } } public void closeSession(Session session) { if (session == null) return; sessionMap.remove(session.getGuid()); logger.trace("kraken msgbus: session={}, org domain={}, admin login name={} closed", new Object[] { session, session.getOrgDomain(), session.getAdminLoginName() }); for (SessionEventHandler handler : sessionEventListeners) { handler.sessionClosed(session); } } public void register(MessageHandler handler) { pluginMessageMap.put(handler.getClassName(), new HashSet<String>(handler.getMethodNames())); for (String methodName : handler.getMethodNames()) { logger.trace("kraken msgbus: {} subscribed.", methodName); Set<MessageHandler> handlers = messageHandlerMap.get(methodName); if (handlers == null) { handlers = Collections.newSetFromMap(new ConcurrentHashMap<MessageHandler, Boolean>()); messageHandlerMap.put(methodName, handlers); } handlers.add(handler); } } public void unregister(MessageHandler handler) { pluginMessageMap.remove(handler.getClassName()); for (String messageType : messageHandlerMap.keySet()) { Set<MessageHandler> handlers = messageHandlerMap.get(messageType); handlers.remove(handler); } } public void register(SessionEventHandler hanlder) { sessionEventListeners.add(hanlder); } public void unregister(SessionEventHandler handler) { sessionEventListeners.remove(handler); } private void reduceStackTrace(InvocationTargetException e) { Throwable t = e.getTargetException(); int limit = 0; boolean found = false; for (StackTraceElement el : t.getStackTrace()) { if (el.getClassName().equals("org.krakenapps.msgbus.handler.MsgbusPluginHandler")) { found = true; break; } limit++; } if (found) { // remove reflection related call stacks limit -= 4; t.setStackTrace(Arrays.copyOf(t.getStackTrace(), limit)); } } class TaskRunner implements Runnable { private Session session; private Message message; private MessageHandler handler; private Message respondMessage; public TaskRunner(Session session, Message message, MessageHandler handler) { this.session = session; this.message = message; this.handler = handler; } @Override public void run() { invokeMessageHandler(session, message, handler); } @SuppressWarnings("unchecked") private void invokeMessageHandler(Session session, Message message, MessageHandler handler) { try { respondMessage = Message.createResponse(session, message); Request request = new Request(session, message); Response response = new Response(); handler.handleMessage(request, response); respondMessage.setParameters(response); } catch (SecurityException e) { logger.warn("kraken msgbus: security violation [domain={}, admin_login_name={}, method={}]", new Object[] { session.getOrgDomain(), session.getAdminLoginName(), message.getMethod() }); logger.debug("kraken msgbus: security violation stacktrace", e); respondMessage = Message.createError(session, message, "security", "Security Violation"); } catch (IllegalArgumentException e) { respondMessage = Message.createError(session, message, "invalid-method-signature", "invalid msgbus method signature"); logger.error("kraken msgbus: illegal msgbus method signature", e); } catch (IllegalAccessException e) { respondMessage = Message.createError(session, message, "invalid-access", "invalid msgbus access"); logger.error("kraken msgbus: invalid msgbus access", e); } catch (InvocationTargetException e) { if (e.getCause() instanceof MsgbusException) { MsgbusException wce = (MsgbusException) e.getCause(); String groupId = wce.getGroupId(); String key = wce.getErrorCode(); String lang = session.getString("lang"); if (lang == null) lang = "en"; String errorCode = wce.getErrorCode(); ResourceHandler resourceHandler = resourceApi.getResourceHandler(groupId); String errorMessage = null; if (resourceHandler != null) errorMessage = resourceHandler.formatText(errorCode, new Locale(lang), wce.getParameters()); if (errorMessage == null) { final String templateErrorMessage = "kraken msgbus: error message template [group_id={}, message_id={}, lang={}] not found"; logger.warn(templateErrorMessage, new Object[] { groupId, key, lang }); } respondMessage = Message.createError(session, message, errorCode, errorMessage); respondMessage.setParameters(wce.getParameters()); } else { respondMessage = Message.createError(session, message, "general-error", "invocation target exception"); } reduceStackTrace(e); logger.error("kraken msgbus: message handler failed", e); } catch (Exception e) { respondMessage = Message.createError(session, message, "unknown", "unknown exception"); logger.error("kraken msgbus: message handler failed", e); } finally { Map<String, Object> m = respondMessage.getParameters(); respondMessage.setParameters((Map<String, Object>) convert(m)); session.send(respondMessage); respondMessage.setParameters(m); } } } @Override public void setSessionTimeout(int minutes) { ConfigDatabase db = conf.ensureDatabase("kraken-msgbus"); Config c = db.findOne(MsgbusConfig.class, null); MsgbusConfig msgbusConfig = ensureMsgbusConfig(db, c); msgbusConfig.setTimeout(minutes); db.update(c, msgbusConfig); this.timeout = minutes; } @Override public int getSessionTimeout() { return timeout; } private MsgbusConfig ensureMsgbusConfig(ConfigDatabase db, Config c) { MsgbusConfig msgbusConfig; if (c == null) { msgbusConfig = new MsgbusConfig(); msgbusConfig.setTimeout(30); db.add(msgbusConfig); } else { msgbusConfig = c.getDocument(MsgbusConfig.class); } return msgbusConfig; } @Override public void addMessageListener(MessageListener listener) { listeners.add(listener); } @Override public void removeMessageListener(MessageListener listener) { listeners.remove(listener); } }