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
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
259
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
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
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402 }
403 }
404
405 private void disconnectFrom(ServiceInfo info) {
406 if(!isConnectedTo(info)) {
407 return;
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
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
449 synchronized (serviceInfoToReceiveMap) {
450 serviceInfoToReceiveMap.put(info, receiver);
451 }
452
453
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
460
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
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 }