Java tutorial
/*- ******************************************************************************* * Copyright (c) 2011, 2016 Diamond Light Source Ltd. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Matthew Gerring - initial API and implementation and/or initial documentation *******************************************************************************/ package org.eclipse.scanning.event; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EventListener; import java.util.EventObject; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.MessageListener; import javax.jms.TextMessage; import javax.jms.Topic; import org.apache.commons.lang.ClassUtils; import org.eclipse.scanning.api.INameable; import org.eclipse.scanning.api.event.EventException; import org.eclipse.scanning.api.event.IEventConnectorService; import org.eclipse.scanning.api.event.IdBean; import org.eclipse.scanning.api.event.alive.HeartbeatBean; import org.eclipse.scanning.api.event.alive.HeartbeatEvent; import org.eclipse.scanning.api.event.alive.IHeartbeatListener; import org.eclipse.scanning.api.event.bean.BeanEvent; import org.eclipse.scanning.api.event.bean.IBeanClassListener; import org.eclipse.scanning.api.event.bean.IBeanListener; import org.eclipse.scanning.api.event.core.ISubscriber; import org.eclipse.scanning.api.event.scan.DeviceState; import org.eclipse.scanning.api.event.scan.IScanListener; import org.eclipse.scanning.api.event.scan.ScanBean; import org.eclipse.scanning.api.event.scan.ScanEvent; import org.eclipse.scanning.api.event.status.Status; import org.eclipse.scanning.api.scan.event.ILocationListener; import org.eclipse.scanning.api.scan.event.Location; import org.eclipse.scanning.api.scan.event.LocationEvent; import org.eclipse.scanning.event.util.JsonUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @SuppressWarnings("rawtypes") class SubscriberImpl<T extends EventListener> extends AbstractConnection implements ISubscriber<T> { private static final Logger logger = LoggerFactory.getLogger(SubscriberImpl.class); private static String DEFAULT_KEY = UUID.randomUUID().toString(); // Does not really matter what key is used for the default collection. private Map<String, Collection<T>> slisteners; // Scan listeners private Map<Class, DiseminateHandler> dMap; private BlockingQueue<DiseminateEvent> queue; private MessageConsumer scanConsumer, hearbeatConsumer; private boolean synchronous = true; public SubscriberImpl(URI uri, String topic, IEventConnectorService service) { super(uri, topic, service); slisteners = new ConcurrentHashMap<String, Collection<T>>(31); // Concurrent overkill? dMap = createDiseminateHandlers(); } @Override public void addListener(T listener) throws EventException { addListener(DEFAULT_KEY, listener); } @Override public void addListener(String scanID, T listener) throws EventException { setConnected(true); if (isSynchronous()) createDiseminateThread(); registerListener(scanID, listener, slisteners); if (scanConsumer == null) { try { Class<?> beanClass = listener instanceof IBeanClassListener ? ((IBeanClassListener) listener).getBeanClass() : null; scanConsumer = createConsumer(getTopicName(), beanClass); } catch (JMSException e) { throw new EventException("Cannot subscribe to topic " + getTopicName() + " with URI " + uri, e); } } } private MessageConsumer createConsumer(final String topicName, final Class<?> beanClass) throws JMSException { Topic topic = super.createTopic(topicName); final MessageConsumer consumer = session.createConsumer(topic); MessageListener listener = new MessageListener() { public void onMessage(Message message) { TextMessage txt = (TextMessage) message; try { String json = txt.getText(); json = JsonUtil.removeProperties(json, properties); try { Object bean = service.unmarshal(json, beanClass); schedule(new DiseminateEvent(bean)); } catch (Exception ne) { logger.error("Error processing message {} on topic {} with beanClass {}", message, topicName, beanClass, ne); ne.printStackTrace(); // Unit tests without log4j config show this one. } } catch (JMSException ne) { logger.error("Cannot get text from message " + txt, ne); } } }; consumer.setMessageListener(listener); return consumer; } private void schedule(DiseminateEvent event) { if (isSynchronous()) { if (queue != null) queue.add(event); } else { if (event == DiseminateEvent.STOP) return; // TODO FIXME Might not be right... final Thread thread = new Thread("Execute event " + getTopicName()) { public void run() { diseminate(event); // Use this JMS thread directly to do work. } }; thread.setDaemon(true); thread.setPriority(Thread.NORM_PRIORITY + 1); thread.start(); } } private void createDiseminateThread() { if (!isSynchronous()) return; // If asynch we do not run events in order and wait until they return. if (queue != null) return; queue = new LinkedBlockingQueue<>(); // Small, if they do work and things back-up, exceptions will occur. final Thread despachter = new Thread(new Runnable() { public void run() { while (isConnected()) { try { DiseminateEvent event = queue.take(); if (event == DiseminateEvent.STOP) return; diseminate(event); } catch (RuntimeException e) { e.printStackTrace(); logger.error("RuntimeException occured despatching event", e); continue; } catch (Exception e) { e.printStackTrace(); logger.error("Stopping event despatch thread ", e); return; } } System.out.println(Thread.currentThread().getName() + " disconnecting events."); } }, "Submitter despatch thread " + getSubmitQueueName()); despachter.setDaemon(true); despachter.setPriority(Thread.NORM_PRIORITY + 1); despachter.start(); } private final static class DiseminateEvent { public static final DiseminateEvent STOP = new DiseminateEvent("STOP"); protected final Object bean; public DiseminateEvent(Object bean) { this.bean = bean; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((bean == null) ? 0 : bean.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; DiseminateEvent other = (DiseminateEvent) obj; if (bean == null) { if (other.bean != null) return false; } else if (!bean.equals(other.bean)) return false; return true; } } private void diseminate(DiseminateEvent event) { Object bean = event.bean; diseminate(bean, slisteners.get(DEFAULT_KEY)); // general listeners if (bean instanceof IdBean) { IdBean idBean = (IdBean) bean; diseminate(bean, slisteners.get(idBean.getUniqueId())); // scan specific listeners, if any } else if (bean instanceof INameable) { INameable namedBean = (INameable) bean; diseminate(bean, slisteners.get(namedBean.getName())); // scan specific listeners, if any } } private boolean diseminate(Object bean, Collection<T> listeners) { if (listeners == null) return false; if (listeners.isEmpty()) return false; final EventListener[] ls = listeners.toArray(new EventListener[listeners.size()]); boolean ret = true; for (EventListener listener : ls) { @SuppressWarnings("unchecked") List<Class<?>> types = getAllInterfaces(listener.getClass()); boolean diseminated = false; for (Class<?> type : types) { DiseminateHandler handler = dMap.get(type); if (handler == null) continue; handler.diseminate(bean, listener); diseminated = true; } ret = ret && diseminated; } return ret; } private Map<Class<? extends EventListener>, List<Class<?>>> interfaces; /** * Important to cache the interfaces. Getting them caused a bug where scannable * values were slow to transmit to the client during a scan. * * @param class1 * @return */ private List<Class<?>> getAllInterfaces(Class<? extends EventListener> class1) { if (interfaces == null) interfaces = new HashMap<>(); if (!interfaces.containsKey(class1)) { interfaces.put(class1, ClassUtils.getAllInterfaces(class1)); } return interfaces.get(class1); } private Map<Class, DiseminateHandler> createDiseminateHandlers() { Map<Class, DiseminateHandler> ret = Collections.synchronizedMap(new HashMap<Class, DiseminateHandler>(3)); ret.put(IScanListener.class, new DiseminateHandler() { public void diseminate(Object bean, EventListener e) { if (!(bean instanceof ScanBean)) return; // This listener must be used with events publishing ScanBean // If your scan does not publish ScanBean events then you // may listen to it with a standard IBeanListener. // Used casting because generics got silly ScanBean sbean = (ScanBean) bean; IScanListener l = (IScanListener) e; DeviceState now = sbean.getDeviceState(); DeviceState was = sbean.getPreviousDeviceState(); if (now != null && now != was) { execute(new DespatchEvent(l, new ScanEvent(sbean), true)); return; } else { Status snow = sbean.getStatus(); Status swas = sbean.getPreviousStatus(); if (snow != null && snow != swas && swas != null) { execute(new DespatchEvent(l, new ScanEvent(sbean), true)); return; } } execute(new DespatchEvent(l, new ScanEvent(sbean), false)); } }); ret.put(IHeartbeatListener.class, new DiseminateHandler() { public void diseminate(Object bean, EventListener e) { // Used casting because generics got silly HeartbeatBean hbean = (HeartbeatBean) bean; IHeartbeatListener l = (IHeartbeatListener) e; execute(new DespatchEvent(l, new HeartbeatEvent(hbean))); } }); ret.put(IBeanListener.class, new DiseminateHandler() { public void diseminate(Object bean, EventListener e) { // Used casting because generics got silly @SuppressWarnings("unchecked") IBeanListener<Object> l = (IBeanListener<Object>) e; execute(new DespatchEvent(l, new BeanEvent<Object>(bean))); } }); ret.put(ILocationListener.class, new DiseminateHandler() { public void diseminate(Object bean, EventListener e) { // Used casting because generics got silly ILocationListener l = (ILocationListener) e; execute(new DespatchEvent(l, new LocationEvent((Location) bean))); } }); return ret; } private interface DiseminateHandler { public void diseminate(Object bean, EventListener listener) throws ClassCastException; } private void registerListener(String key, T listener, Map<String, Collection<T>> listeners) { Collection<T> ls = listeners.get(key); if (ls == null) { ls = new LinkedHashSet<T>(3); listeners.put(key.toString(), ls); } ls.add(listener); } @Override public void removeListener(T listener) { removeListener(DEFAULT_KEY, listener); } @Override public void removeListener(String id, T listener) { if (slisteners.containsKey(id)) { slisteners.get(id).remove(listener); } } @Override public void removeListeners(String id) { slisteners.remove(id); } @Override public void clear() { slisteners.clear(); } @Override public void disconnect() throws EventException { try { clear(); if (scanConsumer != null) scanConsumer.close(); if (hearbeatConsumer != null) hearbeatConsumer.close(); super.disconnect(); } catch (JMSException ne) { throw new EventException("Internal error - unable to close connection!", ne); } finally { scanConsumer = null; hearbeatConsumer = null; setConnected(false); } super.disconnect(); schedule(DiseminateEvent.STOP); } protected boolean isListenersEmpty() { return slisteners.isEmpty(); } private boolean connected; private void execute(DespatchEvent event) { if (event.listener instanceof IHeartbeatListener) ((IHeartbeatListener) event.listener).heartbeatPerformed((HeartbeatEvent) event.object); if (event.listener instanceof IBeanListener) ((IBeanListener) event.listener).beanChangePerformed((BeanEvent) event.object); if (event.listener instanceof ILocationListener) ((ILocationListener) event.listener).locationPerformed((LocationEvent) event.object); if (event.listener instanceof IScanListener) { IScanListener l = (IScanListener) event.listener; ScanEvent e = (ScanEvent) event.object; if (event.isStateChange()) { l.scanStateChanged(e); } else { l.scanEventPerformed(e); } } } /** * Immutable event for queue. * * @author fcp94556 * */ private static class DespatchEvent { protected final EventListener listener; protected final EventObject object; protected final boolean isStateChange; public DespatchEvent(EventListener listener, EventObject object) { this(listener, object, false); } public DespatchEvent(EventListener listener2, EventObject object2, boolean b) { this.listener = listener2; this.object = object2; this.isStateChange = b; } public boolean isStateChange() { return isStateChange; } } public boolean isConnected() { return connected; } private void setConnected(boolean connected) { this.connected = connected; } public boolean isSynchronous() { return synchronous; } public void setSynchronous(boolean synchronous) { this.synchronous = synchronous; } private List<String> properties; @Override public void addProperty(String name, FilterAction... actions) { for (FilterAction fa : actions) if (fa != FilterAction.DELETE) throw new IllegalArgumentException( "It is only possible to remove properties from the subscribed json right now"); if (properties == null) properties = new ArrayList<>(7); properties.add(name); } @Override public void removeProperty(String name) { if (properties == null) return; properties.remove(name); } @Override public List<String> getProperties() { return properties; } }