View Javadoc

1   package org.apache.log4j.chainsaw.zeroconf;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Component;
5   import java.awt.Container;
6   import java.awt.event.ActionEvent;
7   import java.awt.event.MouseAdapter;
8   import java.awt.event.MouseEvent;
9   import java.io.File;
10  import java.io.FileReader;
11  import java.io.FileWriter;
12  import java.util.HashMap;
13  import java.util.Iterator;
14  import java.util.Map;
15  
16  import javax.jmdns.JmDNS;
17  import javax.jmdns.ServiceEvent;
18  import javax.jmdns.ServiceInfo;
19  import javax.jmdns.ServiceListener;
20  import javax.swing.AbstractAction;
21  import javax.swing.Icon;
22  import javax.swing.ImageIcon;
23  import javax.swing.JFrame;
24  import javax.swing.JMenu;
25  import javax.swing.JMenuBar;
26  import javax.swing.JMenuItem;
27  import javax.swing.JPopupMenu;
28  import javax.swing.JScrollPane;
29  import javax.swing.JTabbedPane;
30  import javax.swing.JTable;
31  import javax.swing.JToolBar;
32  import javax.swing.SwingUtilities;
33  import javax.swing.event.TableModelEvent;
34  import javax.swing.event.TableModelListener;
35  
36  import org.apache.log4j.BasicConfigurator;
37  import org.apache.log4j.LogManager;
38  import org.apache.log4j.Logger;
39  import org.apache.log4j.chainsaw.SmallButton;
40  import org.apache.log4j.chainsaw.help.HelpManager;
41  import org.apache.log4j.chainsaw.icons.ChainsawIcons;
42  import org.apache.log4j.chainsaw.plugins.GUIPluginSkeleton;
43  import org.apache.log4j.chainsaw.prefs.SettingsManager;
44  import org.apache.log4j.net.SocketHubReceiver;
45  import org.apache.log4j.net.ZeroConfSocketHubAppender;
46  import org.apache.log4j.net.Zeroconf4log4j;
47  import org.apache.log4j.plugins.Plugin;
48  import org.apache.log4j.plugins.PluginEvent;
49  import org.apache.log4j.plugins.PluginListener;
50  import org.apache.log4j.spi.LoggerRepositoryEx;
51  
52  import com.thoughtworks.xstream.XStream;
53  import com.thoughtworks.xstream.io.xml.DomDriver;
54  
55  /***
56   * This plugin is designed to detect specific Zeroconf zones (Rendevouz/Bonjour,
57   * whatever people are calling it) and allow the user to double click on
58   * 'devices' to try and connect to them with no configuration needed.
59   * 
60   * TODO need to handle
61   * NON-log4j devices that may be broadcast in the interested zones 
62   * TODO add the
63   * default Zone, and the list of user-specified zones to a preferenceModel
64   * 
65   * To run this in trial mode, first run {@link ZeroConfSocketHubAppenderTestBed}, then
66   * run this class' main(..) method.
67   * 
68   * @author psmith
69   * 
70   */
71  public class ZeroConfPlugin extends GUIPluginSkeleton {
72  
73      private static final Logger LOG = Logger.getLogger(ZeroConfPlugin.class);
74  
75      private static final Icon DEVICE_DISCOVERED_ICON = new ImageIcon(ChainsawIcons.ANIM_RADIO_TOWER);
76  
77      private ZeroConfDeviceModel discoveredDevices = new ZeroConfDeviceModel();
78  
79      private JTable deviceTable = new JTable(discoveredDevices);
80      
81      private final JScrollPane scrollPane = new JScrollPane(deviceTable);
82  
83      private JmDNS jmDNS;
84  
85      private ZeroConfPreferenceModel preferenceModel;
86      
87      private Map serviceInfoToReceiveMap = new HashMap();
88  
89      private JMenu connectToMenu = new JMenu("Connect to");
90      private JMenuItem helpItem = new JMenuItem(new AbstractAction("Learn more about ZeroConf...",
91              ChainsawIcons.ICON_HELP) {
92  
93          public void actionPerformed(ActionEvent e) {
94              HelpManager.getInstance()
95                      .showHelpForClass(ZeroConfPlugin.class);
96          }
97      });  
98      
99      private JMenuItem nothingToConnectTo = new JMenuItem("No devices discovered");
100     
101     public ZeroConfPlugin() {
102         setName("Zeroconf");
103     }
104 
105     public void shutdown() {
106         Zeroconf4log4j.shutdown();
107         save();
108     }
109 
110     private void save() {
111         File fileLocation = getPreferenceFileLocation();
112         XStream stream = new XStream(new DomDriver());
113         try {
114             stream.toXML(preferenceModel, new FileWriter(fileLocation));
115         } catch (Exception e) {
116             LOG.error("Failed to save ZeroConfPlugin configuration file",e);
117         }
118     }
119 
120     private File getPreferenceFileLocation() {
121         return new File(SettingsManager.getInstance().getSettingsDirectory(), "zeroconfprefs.xml");
122     }
123 
124     public void activateOptions() {
125         setLayout(new BorderLayout());
126         jmDNS = Zeroconf4log4j.getInstance();
127 
128         jmDNS.addServiceListener(
129                 ZeroConfSocketHubAppender.DEFAULT_ZEROCONF_ZONE,
130                 new ZeroConfServiceListener());
131 
132         jmDNS.addServiceListener(ZeroConfSocketHubAppender.DEFAULT_ZEROCONF_ZONE, discoveredDevices);
133         
134         deviceTable.addMouseListener(new ConnectorMouseListener());
135 
136         
137         JToolBar toolbar = new JToolBar();
138         SmallButton helpButton = new SmallButton(helpItem.getAction());
139         helpButton.setText(helpItem.getText());
140         toolbar.add(helpButton);
141         toolbar.setFloatable(false);
142         add(toolbar, BorderLayout.NORTH);
143         add(scrollPane, BorderLayout.CENTER);
144         
145         injectMenu();
146         
147         ((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().addPluginListener(new PluginListener() {
148 
149             public void pluginStarted(PluginEvent e) {
150                 
151             }
152 
153             public void pluginStopped(PluginEvent e) {
154                 Plugin plugin = e.getPlugin();
155                 synchronized(serviceInfoToReceiveMap) {
156                     for (Iterator iter = serviceInfoToReceiveMap.entrySet().iterator(); iter.hasNext();) {
157                         Map.Entry entry = (Map.Entry) iter.next();
158                         if(entry.getValue() == plugin) {
159                                 iter.remove();
160                         }
161                     }
162                 }
163 //                 need to make sure that the menu item tracking this item has it's icon and enabled state updade
164                 discoveredDevices.fireTableDataChanged();
165             }});
166 
167         File fileLocation = getPreferenceFileLocation();
168         XStream stream = new XStream(new DomDriver());
169         if (fileLocation.exists()) {
170             try {
171                 this.preferenceModel = (ZeroConfPreferenceModel) stream
172                         .fromXML(new FileReader(fileLocation));
173             } catch (Exception e) {
174                 LOG.error("Failed to load ZeroConfPlugin configuration file",e);
175             }
176         }else {
177             this.preferenceModel = new ZeroConfPreferenceModel();
178         }
179         discoveredDevices.setZeroConfPreferenceModel(preferenceModel);
180         discoveredDevices.setZeroConfPluginParent(this);
181     }
182     
183     /***
184      * Sets the icon of this parent container (a JTabbedPane, we hope
185      *
186      */
187     private void setIconIfNeeded() {
188         Container container = this.getParent();
189         if(container instanceof JTabbedPane) {
190             JTabbedPane tabbedPane = (JTabbedPane) container;
191             Icon icon = discoveredDevices.getRowCount()==0?null:DEVICE_DISCOVERED_ICON;
192             tabbedPane.setIconAt(tabbedPane.indexOfTab(getName()), icon);
193         }else {
194             LOG.warn("Parent is not a TabbedPane, not setting icon: " + container.getClass().getName());
195         }
196     }
197 
198     /***
199      * Attempts to find a JFrame container as a parent,and addse a "Connect to" menu
200      *
201      */
202     private void injectMenu() {
203         
204         JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this);
205         if(frame == null) {
206             LOG.info("Could not locate parent JFrame to add menu to");
207         }else {
208             JMenuBar menuBar = frame.getJMenuBar();
209             if(menuBar==null ) {
210                 menuBar = new JMenuBar();
211                 frame.setJMenuBar(menuBar);
212             }
213             insertToLeftOfHelp(menuBar, connectToMenu);
214             connectToMenu.add(nothingToConnectTo);
215             
216             discoveredDevices.addTableModelListener(new TableModelListener (){
217 
218                 public void tableChanged(TableModelEvent e) {
219                     if(discoveredDevices.getRowCount()==0) {
220                         connectToMenu.add(nothingToConnectTo,0);
221                     }else if(discoveredDevices.getRowCount()>0) {
222                         connectToMenu.remove(nothingToConnectTo);
223                     }
224                     
225                 }});
226             
227             nothingToConnectTo.setEnabled(false);
228 
229             connectToMenu.addSeparator();
230             connectToMenu.add(helpItem);
231         }
232     }
233 
234     /***
235      * Hack method to locate the JMenu that is the Help menu, and inserts the new menu
236      * just to the left of it.
237      * @param menuBar
238      * @param item
239      */
240     private void insertToLeftOfHelp(JMenuBar menuBar, JMenu item) {
241         for (int i = 0; i < menuBar.getMenuCount(); i++) {
242             JMenu menu = menuBar.getMenu(i);
243             if(menu.getText().equalsIgnoreCase("help")) {
244                 menuBar.add(item, i-1);
245             }
246         }
247         LOG.warn("menu '" + item.getText() + "' was NOT added because the 'Help' menu could not be located");
248     }
249 
250     /***
251      * When a device is discovered, we create a menu item for it so it can be connected to via that
252      * GUI mechanism, and also if the device is one of the auto-connect devices then a background thread
253      * is created to connect the device.
254      * @param info
255      */
256     private void deviceDiscovered(final ServiceInfo info) {
257         final String name = info.getName();
258 //        TODO currently adding ALL devices to autoConnectlist
259 //        preferenceModel.addAutoConnectDevice(name);
260         
261         
262         JMenuItem connectToDeviceMenuItem = new JMenuItem(new AbstractAction(info.getName()) {
263 
264             public void actionPerformed(ActionEvent e) {
265                 connectTo(info);
266             }});
267         
268         if(discoveredDevices.getRowCount()>0) {
269             Component[] menuComponents = connectToMenu.getMenuComponents();
270             boolean located = false;
271             for (int i = 0; i < menuComponents.length; i++) {
272                 Component c = menuComponents[i];
273                 if (!(c instanceof JPopupMenu.Separator)) {
274                     JMenuItem item = (JMenuItem) menuComponents[i];
275                     if (item.getText().compareToIgnoreCase(name) < 0) {
276                         connectToMenu.insert(connectToDeviceMenuItem, i);
277                         located = true;
278                         break;
279                     }
280                 }
281             }
282             if(!located) {
283                 connectToMenu.insert(connectToDeviceMenuItem,0);
284             }
285         }else {
286             connectToMenu.insert(connectToDeviceMenuItem,0);
287         }
288 //         if the device name is one of the autoconnect devices, then connect immediately
289         if (preferenceModel.getAutoConnectDevices().contains(name)) {
290             new Thread(new Runnable() {
291 
292                 public void run() {
293                     LOG.info("Auto-connecting to " + name);
294                     connectTo(info);
295                 }
296             }).start();
297         }
298     }
299     
300     /***
301      * When a device is removed or disappears we need to remove any JMenu item associated with it.
302      * @param name
303      */
304     private void deviceRemoved(String name) {
305         Component[] menuComponents = connectToMenu.getMenuComponents();
306         for (int i = 0; i < menuComponents.length; i++) {
307             Component c = menuComponents[i];
308             if (!(c instanceof JPopupMenu.Separator)) {
309                 JMenuItem item = (JMenuItem) menuComponents[i];
310                 if (item.getText().compareToIgnoreCase(name) == 0) {
311                     connectToMenu.remove(item);
312                     break;
313                 }
314             }
315         }
316     }
317         
318     /***
319      * Listens out on the JmDNS/ZeroConf network for new devices that appear
320      * and adds/removes these device information from the list/model.
321      *
322      */
323     private class ZeroConfServiceListener implements ServiceListener {
324 
325         public void serviceAdded(final ServiceEvent event) {
326             LOG.info("Service Added: " + event);
327             /***
328              * it's not very clear whether we should do the resolving in a
329              * background thread or not.. All it says is to NOT do it in the AWT
330              * thread, so I'm thinking it probably should be a background thread
331              */
332             Runnable runnable = new Runnable() {
333                 public void run() {
334                     ZeroConfPlugin.this.jmDNS.requestServiceInfo(event
335                             .getType(), event.getName());
336                 }
337             };
338             Thread thread = new Thread(runnable,
339                     "ChainsawZeroConfRequestResolutionThread");
340             thread.setPriority(Thread.MIN_PRIORITY);
341             thread.start();
342         }
343 
344         public void serviceRemoved(ServiceEvent event) {
345             LOG.info("Service Removed: " + event);
346             deviceRemoved(event.getName());
347         }
348 
349         public void serviceResolved(ServiceEvent event) {
350             LOG.info("Service Resolved: " + event);
351             deviceDiscovered(event.getInfo());
352         }
353 
354     }
355 
356 
357     /***
358      * When the user double clicks on a row, then the device is connected to,
359      * the only exception is when clicking in the check box column for auto connect.
360      */
361     private class ConnectorMouseListener extends MouseAdapter {
362 
363         public void mouseClicked(MouseEvent e) {
364             if (e.getClickCount() == 2) {
365                 int row = deviceTable.rowAtPoint(e.getPoint());
366                 if(deviceTable.columnAtPoint(e.getPoint())==2) {
367                     return;
368                 }
369                 ServiceInfo info = discoveredDevices.getServiceInfoAtRow(row);
370                 
371                 if (!isConnectedTo(info)) {
372                     connectTo(info);
373                 } else {
374                     disconnectFrom(info);
375                 }
376             }
377         }
378 
379         public void mousePressed(MouseEvent e) {
380             /***
381              * This methodh handles when the user clicks the
382              * auto-connect
383              */
384 //            int index = listBox.locationToIndex(e.getPoint());
385 //
386 //            if (index != -1) {
387 ////                Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), )
388 //                Component c = SwingUtilities.getDeepestComponentAt(ZeroConfPlugin.this, e.getX(), e.getY());
389 //                if (c instanceof JCheckBox) {
390 //                    ServiceInfo info = (ServiceInfo) listBox.getModel()
391 //                            .getElementAt(index);
392 //                    String name = info.getName();
393 //                    if (preferenceModel.getAutoConnectDevices().contains(name)) {
394 //                        preferenceModel.removeAutoConnectDevice(name);
395 //                    } else {
396 //                        preferenceModel.addAutoConnectDevice(name);
397 //                    }
398 //                    discoveredDevices.fireContentsChanged();
399 //                    repaint();
400 //                }
401 //            }
402         }
403     }
404 
405     private void disconnectFrom(ServiceInfo info) {
406         if(!isConnectedTo(info)) {
407             return; // not connected, who cares
408         }
409         Plugin plugin;
410         synchronized (serviceInfoToReceiveMap) {
411             plugin = (Plugin) serviceInfoToReceiveMap.get(info);
412         }
413         ((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().stopPlugin(plugin.getName());
414         
415         JMenuItem item = locateMatchingMenuItem(info.getName());
416         if (item!=null) {
417             item.setIcon(null);
418             item.setEnabled(true);
419         }
420     }
421     /***
422      * returns true if the serviceInfo record already has a matching connected receiver
423      * @param info
424      * @return
425      */
426     boolean isConnectedTo(ServiceInfo info) {
427         return serviceInfoToReceiveMap.containsKey(info);
428     }
429     /***
430      * Starts a receiver to the appender referenced within the ServiceInfo
431      * @param info
432      */
433     private void connectTo(ServiceInfo info) {
434         LOG.info("Connection request for " + info);
435         int port = info.getPort();
436         String hostAddress = info.getHostAddress();
437        
438 //        TODO handle different receivers than just SocketHubReceiver
439         SocketHubReceiver receiver = new SocketHubReceiver();
440         receiver.setHost(hostAddress);
441         receiver.setPort(port);
442         receiver.setName(info.getName());
443         
444         ((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().addPlugin(receiver);
445         receiver.activateOptions();
446         LOG.info("Receiver '" + receiver.getName() + "' has been started");
447         
448         // ServiceInfo obeys equals() and hashCode() contracts, so this should be safe.
449         synchronized (serviceInfoToReceiveMap) {
450             serviceInfoToReceiveMap.put(info, receiver);
451         }
452         
453 //         this instance of the menu item needs to be disabled, and have an icon added
454         JMenuItem item = locateMatchingMenuItem(info.getName());
455         if (item!=null) {
456             item.setIcon(new ImageIcon(ChainsawIcons.ANIM_NET_CONNECT));
457             item.setEnabled(false);
458         }
459 //        // now notify the list model has changed, it needs redrawing of the receiver icon now it's connected
460 //        discoveredDevices.fireContentsChanged();
461     }
462 
463     /***
464      * Finds the matching JMenuItem based on name, may return null if there is no match.
465      * 
466      * @param name
467      * @return
468      */
469     private JMenuItem locateMatchingMenuItem(String name) {
470         Component[] menuComponents = connectToMenu.getMenuComponents();
471         for (int i = 0; i < menuComponents.length; i++) {
472             Component c = menuComponents[i];
473             if (!(c instanceof JPopupMenu.Separator)) {
474                 JMenuItem item = (JMenuItem) menuComponents[i];
475                 if (item.getText().compareToIgnoreCase(name) == 0) {
476                     return item;
477                 }
478             }
479         }
480         return null;
481     }
482 
483     public static void main(String[] args) throws InterruptedException {
484 
485         BasicConfigurator.resetConfiguration();
486         BasicConfigurator.configure();
487 
488         final ZeroConfPlugin plugin = new ZeroConfPlugin();
489 
490 
491         JFrame frame = new JFrame();
492         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
493 
494         frame.getContentPane().setLayout(new BorderLayout());
495         frame.getContentPane().add(plugin, BorderLayout.CENTER);
496 
497         // needs to be activated after being added to the JFrame for Menu injection to work
498         plugin.activateOptions();
499 
500         frame.pack();
501         frame.setVisible(true);
502 
503         Thread thread = new Thread(new Runnable() {
504             public void run() {
505                 plugin.shutdown();
506             }
507         });
508         Runtime.getRuntime().addShutdownHook(thread);
509     }
510 
511 }