View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  package org.apache.log4j.chainsaw.zeroconf;
    18
    19  import java.awt.BorderLayout;
    20  import java.awt.Component;
    21  import java.awt.event.ActionEvent;
    22  import java.awt.event.MouseAdapter;
    23  import java.awt.event.MouseEvent;
    24  import java.io.File;
    25  import java.io.FileReader;
    26  import java.io.FileWriter;
    27  import java.util.HashMap;
    28  import java.util.HashSet;
    29  import java.util.Iterator;
    30  import java.util.Map;
    31  import java.util.Set;
    32
    33  import javax.jmdns.JmDNS;
    34  import javax.jmdns.ServiceEvent;
    35  import javax.jmdns.ServiceInfo;
    36  import javax.jmdns.ServiceListener;
    37  import javax.swing.AbstractAction;
    38  import javax.swing.ImageIcon;
    39  import javax.swing.JFrame;
    40  import javax.swing.JMenu;
    41  import javax.swing.JMenuBar;
    42  import javax.swing.JMenuItem;
    43  import javax.swing.JPopupMenu;
    44  import javax.swing.JScrollPane;
    45  import javax.swing.JTable;
    46  import javax.swing.JToolBar;
    47  import javax.swing.SwingUtilities;
    48  import javax.swing.event.TableModelEvent;
    49  import javax.swing.event.TableModelListener;
    50
    51  import org.apache.log4j.BasicConfigurator;
    52  import org.apache.log4j.LogManager;
    53  import org.apache.log4j.Logger;
    54  import org.apache.log4j.chainsaw.ChainsawConstants;
    55  import org.apache.log4j.chainsaw.LogFilePatternLayoutBuilder;
    56  import org.apache.log4j.chainsaw.SmallButton;
    57  import org.apache.log4j.chainsaw.help.HelpManager;
    58  import org.apache.log4j.chainsaw.icons.ChainsawIcons;
    59  import org.apache.log4j.chainsaw.plugins.GUIPluginSkeleton;
    60  import org.apache.log4j.chainsaw.prefs.SettingsManager;
    61  import org.apache.log4j.chainsaw.vfs.VFSLogFilePatternReceiver;
    62  import org.apache.log4j.helpers.LogLog;
    63  import org.apache.log4j.net.MulticastReceiver;
    64  import org.apache.log4j.net.SocketHubReceiver;
    65  import org.apache.log4j.net.SocketReceiver;
    66  import org.apache.log4j.net.UDPReceiver;
    67  import org.apache.log4j.net.XMLSocketReceiver;
    68  import org.apache.log4j.net.ZeroConfSupport;
    69  import org.apache.log4j.plugins.Plugin;
    70  import org.apache.log4j.plugins.PluginEvent;
    71  import org.apache.log4j.plugins.PluginListener;
    72  import org.apache.log4j.plugins.Receiver;
    73  import org.apache.log4j.spi.LoggerRepositoryEx;
    74
    75  import com.thoughtworks.xstream.XStream;
    76  import com.thoughtworks.xstream.io.xml.DomDriver;
    77
    78  /**
    79   * This plugin is designed to detect specific Zeroconf zones (Rendevouz/Bonjour,
    80   * whatever people are calling it) and allow the user to double click on
    81   * 'devices' to try and connect to them with no configuration needed.
    82   *
    83   * TODO need to handle
    84   * NON-log4j devices that may be broadcast in the interested zones
    85   * TODO add the
    86   * default Zone, and the list of user-specified zones to a preferenceModel
    87   *
    88   * @author psmith
    89   *
    90   */
    91  public class ZeroConfPlugin extends GUIPluginSkeleton {
    92
    93      private static final Logger LOG = Logger.getLogger(ZeroConfPlugin.class);
    94
    95      private ZeroConfDeviceModel discoveredDevices = new ZeroConfDeviceModel();
    96
    97      private JTable deviceTable = new JTable(discoveredDevices);
    98
    99      private final JScrollPane scrollPane = new JScrollPane(deviceTable);
    100
    101     private ZeroConfPreferenceModel preferenceModel;
    102
    103     private Map serviceInfoToReceiveMap = new HashMap();
    104
    105     private JMenu connectToMenu = new JMenu("Connect to");
    106     private JMenuItem helpItem = new JMenuItem(new AbstractAction("Learn more about ZeroConf...",
    107             ChainsawIcons.ICON_HELP) {
    108
    109         public void actionPerformed(ActionEvent e) {
    110             HelpManager.getInstance()
    111                     .showHelpForClass(ZeroConfPlugin.class);
    112         }
    113     });
    114
    115     private JMenuItem nothingToConnectTo = new JMenuItem("No devices discovered");
    116     private static final String MULTICAST_APPENDER_SERVICE_NAME = "_log4j_xml_mcast_appender.local.";
    117     private static final String UDP_APPENDER_SERVICE_NAME = "_log4j_xml_udp_appender.local.";
    118     private static final String XML_SOCKET_APPENDER_SERVICE_NAME = "_log4j_xml_tcpconnect_appender.local.";
    119     private static final String SOCKET_APPENDER_SERVICE_NAME = "_log4j_obj_tcpconnect_appender.local.";
    120     private static final String SOCKETHUB_APPENDER_SERVICE_NAME = "_log4j_obj_tcpaccept_appender.local.";
    121     private static final String TCP_APPENDER_SERVICE_NAME = "_log4j._tcp.local.";
    122     private static final String NEW_UDP_APPENDER_SERVICE_NAME = "_log4j._udp.local.";
    123
    124     private JmDNS jmDNS;
    125
    126     public ZeroConfPlugin() {
    127         setName("Zeroconf");
    128         deviceTable.setRowHeight(ChainsawConstants.DEFAULT_ROW_HEIGHT);
    129     }
    130
    131     public void shutdown() {
    132         if (jmDNS != null) {
    133             try {
    134                 jmDNS.close();
    135             } catch (Exception e) {
    136                 LOG.error("Unable to close JMDNS", e);
    137             }
    138         }
    139         save();
    140     }
    141
    142     private void save() {
    143         File fileLocation = getPreferenceFileLocation();
    144         XStream stream = new XStream(new DomDriver());
    145         try {
    146             stream.toXML(preferenceModel, new FileWriter(fileLocation));
    147         } catch (Exception e) {
    148             LOG.error("Failed to save ZeroConfPlugin configuration file",e);
    149         }
    150     }
    151
    152     private File getPreferenceFileLocation() {
    153         return new File(SettingsManager.getInstance().getSettingsDirectory(), "zeroconfprefs.xml");
    154     }
    155
    156     public void activateOptions() {
    157         setLayout(new BorderLayout());
    158         jmDNS = (JmDNS) ZeroConfSupport.getJMDNSInstance();
    159
    160         registerServiceListenersForAppenders();
    161
    162         deviceTable.addMouseListener(new ConnectorMouseListener());
    163
    164
    165         JToolBar toolbar = new JToolBar();
    166         SmallButton helpButton = new SmallButton(helpItem.getAction());
    167         helpButton.setText(helpItem.getText());
    168         toolbar.add(helpButton);
    169         toolbar.setFloatable(false);
    170         add(toolbar, BorderLayout.NORTH);
    171         add(scrollPane, BorderLayout.CENTER);
    172
    173         injectMenu();
    174
    175         ((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().addPluginListener(new PluginListener() {
    176
    177             public void pluginStarted(PluginEvent e) {
    178
    179             }
    180
    181             public void pluginStopped(PluginEvent e) {
    182                 Plugin plugin = e.getPlugin();
    183                 synchronized(serviceInfoToReceiveMap) {
    184                     for (Iterator iter = serviceInfoToReceiveMap.entrySet().iterator(); iter.hasNext();) {
    185                         Map.Entry entry = (Map.Entry) iter.next();
    186                         if(entry.getValue() == plugin) {
    187                                 iter.remove();
    188                         }
    189                     }
    190                 }
    191 //                 need to make sure that the menu item tracking this item has it's icon and enabled state updade
    192                 discoveredDevices.fireTableDataChanged();
    193             }});
    194
    195         File fileLocation = getPreferenceFileLocation();
    196         XStream stream = new XStream(new DomDriver());
    197         if (fileLocation.exists()) {
    198             try {
    199                 this.preferenceModel = (ZeroConfPreferenceModel) stream
    200                         .fromXML(new FileReader(fileLocation));
    201             } catch (Exception e) {
    202                 LOG.error("Failed to load ZeroConfPlugin configuration file",e);
    203             }
    204         }else {
    205             this.preferenceModel = new ZeroConfPreferenceModel();
    206         }
    207         discoveredDevices.setZeroConfPreferenceModel(preferenceModel);
    208         discoveredDevices.setZeroConfPluginParent(this);
    209     }
    210
    211     private void registerServiceListenersForAppenders()
    212     {
    213         Set serviceNames = new HashSet();
    214         serviceNames.add(MULTICAST_APPENDER_SERVICE_NAME);
    215         serviceNames.add(SOCKET_APPENDER_SERVICE_NAME);
    216         serviceNames.add(SOCKETHUB_APPENDER_SERVICE_NAME);
    217         serviceNames.add(UDP_APPENDER_SERVICE_NAME);
    218         serviceNames.add(XML_SOCKET_APPENDER_SERVICE_NAME);
    219         serviceNames.add(TCP_APPENDER_SERVICE_NAME);
    220         serviceNames.add(NEW_UDP_APPENDER_SERVICE_NAME);
    221
    222         for (Iterator iter = serviceNames.iterator(); iter.hasNext();) {
    223             String serviceName = iter.next().toString();
    224             jmDNS.addServiceListener(
    225                     serviceName,
    226                     new ZeroConfServiceListener());
    227
    228             jmDNS.addServiceListener(serviceName, discoveredDevices);
    229         }
    230
    231         //now add each appender constant
    232     }
    233
    234     /**
    235      * Attempts to find a JFrame container as a parent,and addse a "Connect to" menu
    236      *
    237      */
    238     private void injectMenu() {
    239
    240         JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this);
    241         if(frame == null) {
    242             LOG.info("Could not locate parent JFrame to add menu to");
    243         }else {
    244             JMenuBar menuBar = frame.getJMenuBar();
    245             if(menuBar==null ) {
    246                 menuBar = new JMenuBar();
    247                 frame.setJMenuBar(menuBar);
    248             }
    249             insertToLeftOfHelp(menuBar, connectToMenu);
    250             connectToMenu.add(nothingToConnectTo);
    251
    252             discoveredDevices.addTableModelListener(new TableModelListener (){
    253
    254                 public void tableChanged(TableModelEvent e) {
    255                     if(discoveredDevices.getRowCount()==0) {
    256                         connectToMenu.add(nothingToConnectTo,0);
    257                     }else if(discoveredDevices.getRowCount()>0) {
    258                         connectToMenu.remove(nothingToConnectTo);
    259                     }
    260
    261                 }});
    262
    263             nothingToConnectTo.setEnabled(false);
    264
    265             connectToMenu.addSeparator();
    266             connectToMenu.add(helpItem);
    267         }
    268     }
    269
    270     /**
    271      * Hack method to locate the JMenu that is the Help menu, and inserts the new menu
    272      * just to the left of it.
    273      * @param menuBar
    274      * @param item
    275      */
    276     private void insertToLeftOfHelp(JMenuBar menuBar, JMenu item) {
    277         for (int i = 0; i < menuBar.getMenuCount(); i++) {
    278             JMenu menu = menuBar.getMenu(i);
    279             if(menu.getText().equalsIgnoreCase("help")) {
    280                 menuBar.add(item, i-1);
    281             }
    282         }
    283         LOG.warn("menu '" + item.getText() + "' was NOT added because the 'Help' menu could not be located");
    284     }
    285
    286     /**
    287      * When a device is discovered, we create a menu item for it so it can be connected to via that
    288      * GUI mechanism, and also if the device is one of the auto-connect devices then a background thread
    289      * is created to connect the device.
    290      * @param info
    291      */
    292     private void deviceDiscovered(final ServiceInfo info) {
    293         final String name = info.getName();
    294 //        TODO currently adding ALL devices to autoConnectlist
    295 //        preferenceModel.addAutoConnectDevice(name);
    296
    297
    298         JMenuItem connectToDeviceMenuItem = new JMenuItem(new AbstractAction(info.getName()) {
    299
    300             public void actionPerformed(ActionEvent e) {
    301                 connectTo(info);
    302             }});
    303
    304         if(discoveredDevices.getRowCount()>0) {
    305             Component[] menuComponents = connectToMenu.getMenuComponents();
    306             boolean located = false;
    307             for (int i = 0; i < menuComponents.length; i++) {
    308                 Component c = menuComponents[i];
    309                 if (!(c instanceof JPopupMenu.Separator)) {
    310                     JMenuItem item = (JMenuItem) menuComponents[i];
    311                     if (item.getText().compareToIgnoreCase(name) < 0) {
    312                         connectToMenu.insert(connectToDeviceMenuItem, i);
    313                         located = true;
    314                         break;
    315                     }
    316                 }
    317             }
    318             if(!located) {
    319                 connectToMenu.insert(connectToDeviceMenuItem,0);
    320             }
    321         }else {
    322             connectToMenu.insert(connectToDeviceMenuItem,0);
    323         }
    324 //         if the device name is one of the autoconnect devices, then connect immediately
    325         if (preferenceModel != null && preferenceModel.getAutoConnectDevices() != null && preferenceModel.getAutoConnectDevices().contains(name)) {
    326             new Thread(new Runnable() {
    327
    328                 public void run() {
    329                     LOG.info("Auto-connecting to " + name);
    330                     connectTo(info);
    331                 }
    332             }).start();
    333         }
    334     }
    335
    336     /**
    337      * When a device is removed or disappears we need to remove any JMenu item associated with it.
    338      * @param name
    339      */
    340     private void deviceRemoved(String name) {
    341         Component[] menuComponents = connectToMenu.getMenuComponents();
    342         for (int i = 0; i < menuComponents.length; i++) {
    343             Component c = menuComponents[i];
    344             if (!(c instanceof JPopupMenu.Separator)) {
    345                 JMenuItem item = (JMenuItem) menuComponents[i];
    346                 if (item.getText().compareToIgnoreCase(name) == 0) {
    347                     connectToMenu.remove(item);
    348                     break;
    349                 }
    350             }
    351         }
    352     }
    353
    354     /**
    355      * Listens out on the JmDNS/ZeroConf network for new devices that appear
    356      * and adds/removes these device information from the list/model.
    357      *
    358      */
    359     private class ZeroConfServiceListener implements ServiceListener {
    360
    361         public void serviceAdded(final ServiceEvent event) {
    362             LOG.info("Service Added: " + event);
    363             /**
    364              * it's not very clear whether we should do the resolving in a
    365              * background thread or not.. All it says is to NOT do it in the AWT
    366              * thread, so I'm thinking it probably should be a background thread
    367              */
    368             Runnable runnable = new Runnable() {
    369                 public void run() {
    370                     ZeroConfPlugin.this.jmDNS.requestServiceInfo(event
    371                             .getType(), event.getName());
    372                 }
    373             };
    374             Thread thread = new Thread(runnable,
    375                     "ChainsawZeroConfRequestResolutionThread");
    376             thread.setPriority(Thread.MIN_PRIORITY);
    377             thread.start();
    378         }
    379
    380         public void serviceRemoved(ServiceEvent event) {
    381             LOG.info("Service Removed: " + event);
    382             deviceRemoved(event.getName());
    383         }
    384
    385         public void serviceResolved(ServiceEvent event) {
    386             LOG.info("Service Resolved: " + event);
    387             deviceDiscovered(event.getInfo());
    388         }
    389
    390     }
    391
    392
    393     /**
    394      * When the user double clicks on a row, then the device is connected to,
    395      * the only exception is when clicking in the check box column for auto connect.
    396      */
    397     private class ConnectorMouseListener extends MouseAdapter {
    398
    399         public void mouseClicked(MouseEvent e) {
    400             if (e.getClickCount() == 2) {
    401                 int row = deviceTable.rowAtPoint(e.getPoint());
    402                 if(deviceTable.columnAtPoint(e.getPoint())==2) {
    403                     return;
    404                 }
    405                 ServiceInfo info = discoveredDevices.getServiceInfoAtRow(row);
    406
    407                 if (!isConnectedTo(info)) {
    408                     connectTo(info);
    409                 } else {
    410                     disconnectFrom(info);
    411                 }
    412             }
    413         }
    414
    415         public void mousePressed(MouseEvent e) {
    416             /**
    417              * This methodh handles when the user clicks the
    418              * auto-connect
    419              */
    420 //            int index = listBox.locationToIndex(e.getPoint());
    421 //
    422 //            if (index != -1) {
    423 ////                Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), )
    424 //                Component c = SwingUtilities.getDeepestComponentAt(ZeroConfPlugin.this, e.getX(), e.getY());
    425 //                if (c instanceof JCheckBox) {
    426 //                    ServiceInfo info = (ServiceInfo) listBox.getModel()
    427 //                            .getElementAt(index);
    428 //                    String name = info.getName();
    429 //                    if (preferenceModel.getAutoConnectDevices().contains(name)) {
    430 //                        preferenceModel.removeAutoConnectDevice(name);
    431 //                    } else {
    432 //                        preferenceModel.addAutoConnectDevice(name);
    433 //                    }
    434 //                    discoveredDevices.fireContentsChanged();
    435 //                    repaint();
    436 //                }
    437 //            }
    438         }
    439     }
    440
    441     private void disconnectFrom(ServiceInfo info) {
    442         if(!isConnectedTo(info)) {
    443             return; // not connected, who cares
    444         }
    445         Plugin plugin;
    446         synchronized (serviceInfoToReceiveMap) {
    447             plugin = (Plugin) serviceInfoToReceiveMap.get(info);
    448         }
    449         ((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().stopPlugin(plugin.getName());
    450
    451         JMenuItem item = locateMatchingMenuItem(info.getName());
    452         if (item!=null) {
    453             item.setIcon(null);
    454             item.setEnabled(true);
    455         }
    456     }
    457     /**
    458      * returns true if the serviceInfo record already has a matching connected receiver
    459      * @param info
    460      * @return
    461      */
    462     boolean isConnectedTo(ServiceInfo info) {
    463         return serviceInfoToReceiveMap.containsKey(info);
    464     }
    465     /**
    466      * Starts a receiver to the appender referenced within the ServiceInfo
    467      * @param info
    468      */
    469     private void connectTo(ServiceInfo info) {
    470         LOG.info("Connection request for " + info);
    471         //Chainsaw can construct receivers from discovered appenders
    472         Receiver receiver = getReceiver(info);
    473         //if null, unable to resolve the service name..no-op
    474         if (receiver == null) {
    475             return;
    476         }
    477         ((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().addPlugin(receiver);
    478         receiver.activateOptions();
    479         LOG.info("Receiver '" + receiver.getName() + "' has been started");
    480
    481         // ServiceInfo obeys equals() and hashCode() contracts, so this should be safe.
    482         synchronized (serviceInfoToReceiveMap) {
    483             serviceInfoToReceiveMap.put(info, receiver);
    484         }
    485
    486 //         this instance of the menu item needs to be disabled, and have an icon added
    487         JMenuItem item = locateMatchingMenuItem(info.getName());
    488         if (item!=null) {
    489             item.setIcon(new ImageIcon(ChainsawIcons.ANIM_NET_CONNECT));
    490             item.setEnabled(false);
    491         }
    492 //        // now notify the list model has changed, it needs redrawing of the receiver icon now it's connected
    493 //        discoveredDevices.fireContentsChanged();
    494     }
    495
    496     private Receiver getReceiver(ServiceInfo info) {
    497         String zone = info.getType();
    498         int port = info.getPort();
    499         String hostAddress = info.getHostAddress();
    500         String name = info.getName();
    501         String decoderClass = info.getPropertyString("decoder");
    502
    503         if (NEW_UDP_APPENDER_SERVICE_NAME.equals(zone))
    504         {
    505             UDPReceiver receiver = new UDPReceiver();
    506             receiver.setPort(port);
    507             receiver.setName(name + "-receiver");
    508             return receiver;
    509         }
    510         //FileAppender or socketappender
    511         //TODO: add more checks (actual layout format, etc)
    512         if (TCP_APPENDER_SERVICE_NAME.equals(zone)) {
    513             //CHECK content type
    514             //application/octet-stream = SocketReceiver
    515             //text/plain = VFSLogFilePatternReceiver (if structured=false)
    516             String contentType = info.getPropertyString("contentType").toLowerCase();
    517             //won't work with log4j2, as Chainsaw depends on log4j1.x
    518             if ("application/octet-stream".equals(contentType))
    519             {
    520                 SocketReceiver receiver = new SocketReceiver();
    521                 receiver.setPort(port);
    522                 receiver.setName(name + "-receiver");
    523                 return receiver;
    524             }
    525             //this will work - regular text log files are fine
    526             if ("text/plain".equals(contentType))
    527             {
    528                 VFSLogFilePatternReceiver receiver = new VFSLogFilePatternReceiver();
    529                 receiver.setAppendNonMatches(true);
    530                 receiver.setFileURL(info.getPropertyString("fileURI"));
    531                 receiver.setLogFormat(LogFilePatternLayoutBuilder.getLogFormatFromPatternLayout(info.getPropertyString("format")));
    532                 receiver.setTimestampFormat(LogFilePatternLayoutBuilder.getTimeStampFormat(info.getPropertyString("format")));
    533                 receiver.setName(name + "-receiver");
    534                 receiver.setTailing(true);
    535                 return receiver;
    536             }
    537         }
    538
    539         //MulticastAppender
    540         if (MULTICAST_APPENDER_SERVICE_NAME.equals(zone)) {
    541             MulticastReceiver receiver = new MulticastReceiver();
    542             //this needs to be a multicast address, not the host address, so we need to use a property
    543             receiver.setAddress(info.getPropertyString("multicastAddress"));
    544             receiver.setPort(port);
    545             receiver.setName(name + "-receiver");
    546             if (decoderClass != null && !decoderClass.equals("")) {
    547                 receiver.setDecoder(decoderClass);
    548             }
    549
    550             return receiver;
    551         }
    552         //UDPAppender
    553         if (UDP_APPENDER_SERVICE_NAME.equals(zone)) {
    554             UDPReceiver receiver = new UDPReceiver();
    555             receiver.setPort(port);
    556             receiver.setName(name + "-receiver");
    557             if (decoderClass != null && !decoderClass.equals("")) {
    558                 receiver.setDecoder(decoderClass);
    559             }
    560             return receiver;
    561         }
    562
    563         //non-log4j XML-based socketappender
    564         if (XML_SOCKET_APPENDER_SERVICE_NAME.equals(zone)) {
    565             XMLSocketReceiver receiver = new XMLSocketReceiver();
    566             receiver.setPort(port);
    567             receiver.setName(name + "-receiver");
    568             if (decoderClass != null && !decoderClass.equals("")) {
    569                 receiver.setDecoder(decoderClass);
    570             }
    571             return receiver;
    572         }
    573
    574         //SocketAppender
    575         if (SOCKET_APPENDER_SERVICE_NAME.equals(zone)) {
    576             SocketReceiver receiver = new SocketReceiver();
    577             receiver.setPort(port);
    578             receiver.setName(name + "-receiver");
    579             return receiver;
    580         }
    581
    582         //SocketHubAppender
    583         if (SOCKETHUB_APPENDER_SERVICE_NAME.equals(zone)) {
    584             SocketHubReceiver receiver = new SocketHubReceiver();
    585             receiver.setHost(hostAddress);
    586             receiver.setPort(port);
    587             receiver.setName(name + "-receiver");
    588             return receiver;
    589         }
    590         //not recognized
    591         LogLog.debug("Unable to find receiver for appender with service name: " + zone);
    592         return null;
    593     }
    594
    595     /**
    596      * Finds the matching JMenuItem based on name, may return null if there is no match.
    597      *
    598      * @param name
    599      * @return
    600      */
    601     private JMenuItem locateMatchingMenuItem(String name) {
    602         Component[] menuComponents = connectToMenu.getMenuComponents();
    603         for (int i = 0; i < menuComponents.length; i++) {
    604             Component c = menuComponents[i];
    605             if (!(c instanceof JPopupMenu.Separator)) {
    606                 JMenuItem item = (JMenuItem) menuComponents[i];
    607                 if (item.getText().compareToIgnoreCase(name) == 0) {
    608                     return item;
    609                 }
    610             }
    611         }
    612         return null;
    613     }
    614
    615     public static void main(String[] args) throws InterruptedException {
    616
    617         BasicConfigurator.resetConfiguration();
    618         BasicConfigurator.configure();
    619
    620         final ZeroConfPlugin plugin = new ZeroConfPlugin();
    621
    622
    623         JFrame frame = new JFrame();
    624         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    625
    626         frame.getContentPane().setLayout(new BorderLayout());
    627         frame.getContentPane().add(plugin, BorderLayout.CENTER);
    628
    629         // needs to be activated after being added to the JFrame for Menu injection to work
    630         plugin.activateOptions();
    631
    632         frame.pack();
    633         frame.setVisible(true);
    634
    635         Thread thread = new Thread(new Runnable() {
    636             public void run() {
    637                 plugin.shutdown();
    638             }
    639         });
    640         Runtime.getRuntime().addShutdownHook(thread);
    641     }
    642
    643 }