blob: 92ca032df693ff4b34fc03a4c2cb50a62012409e [file] [log] [blame]
package org.apache.log4j.chainsaw.zeroconf;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.chainsaw.ModifiableListModel;
import org.apache.log4j.chainsaw.SmallButton;
import org.apache.log4j.chainsaw.help.HelpManager;
import org.apache.log4j.chainsaw.icons.ChainsawIcons;
import org.apache.log4j.chainsaw.plugins.GUIPluginSkeleton;
import org.apache.log4j.chainsaw.prefs.SettingsManager;
import org.apache.log4j.net.SocketHubReceiver;
import org.apache.log4j.net.ZeroConfSocketHubAppender;
import org.apache.log4j.net.Zeroconf4log4j;
import org.apache.log4j.plugins.Plugin;
import org.apache.log4j.plugins.PluginEvent;
import org.apache.log4j.plugins.PluginListener;
import org.apache.log4j.spi.LoggerRepositoryEx;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
/**
* This plugin is designed to detect specific Zeroconf zones (Rendevouz/Bonjour,
* whatever people are calling it) and allow the user to double click on
* 'devices' to try and connect to them with no configuration needed.
*
* TODO add autoConnect visuals, and save it in a model
* TODO need to handle
* NON-log4j devices that may be broadcast in the interested zones
* TODO add the
* default Zone, and the list of user-specified zones to a preferenceModel
*
* To run this in trial mode, first run {@link ZeroConfSocketHubAppenderTestBed}, then
* run this class' main(..) method.
*
* @author psmith
*
*/
public class ZeroConfPlugin extends GUIPluginSkeleton {
private static final Logger LOG = Logger.getLogger(ZeroConfPlugin.class);
private static final Icon DEVICE_DISCOVERED_ICON = new ImageIcon(ChainsawIcons.ANIM_RADIO_TOWER);
private ModifiableListModel discoveredDevices = new ModifiableListModel();
private final JList listBox = new JList(discoveredDevices);
private final JScrollPane scrollPane = new JScrollPane(listBox);
private JmDNS jmDNS;
private ZeroConfPreferenceModel preferenceModel;
private Map serviceInfoToReceiveMap = new HashMap();
private JMenu connectToMenu = new JMenu("Connect to");
private JMenuItem helpItem = new JMenuItem(new AbstractAction("Learn more about ZeroConf...",
ChainsawIcons.ICON_HELP) {
public void actionPerformed(ActionEvent e) {
HelpManager.getInstance()
.showHelpForClass(ZeroConfPlugin.class);
}
});
private JMenuItem nothingToConnectTo = new JMenuItem("No devices discovered");
public ZeroConfPlugin() {
setName("Zeroconf");
}
public void shutdown() {
Zeroconf4log4j.shutdown();
save();
}
private void save() {
File fileLocation = getPreferenceFileLocation();
XStream stream = new XStream(new DomDriver());
try {
stream.toXML(preferenceModel, new FileWriter(fileLocation));
} catch (Exception e) {
LOG.error("Failed to save ZeroConfPlugin configuration file",e);
}
}
private File getPreferenceFileLocation() {
return new File(SettingsManager.getInstance().getSettingsDirectory(), "zeroconfprefs.xml");
}
public void activateOptions() {
setLayout(new BorderLayout());
jmDNS = Zeroconf4log4j.getInstance();
jmDNS.addServiceListener(
ZeroConfSocketHubAppender.DEFAULT_ZEROCONF_ZONE,
new ZeroConfServiceListener());
listBox.setCellRenderer(new ServiceInfoListCellRenderer());
listBox.setLayoutOrientation(JList.HORIZONTAL_WRAP);
listBox.setFixedCellHeight(75);
listBox.setFixedCellWidth(200);
listBox.setVisibleRowCount(-1);
listBox.addMouseListener(new ConnectorMouseListener());
JToolBar toolbar = new JToolBar();
SmallButton helpButton = new SmallButton(helpItem.getAction());
helpButton.setText(helpItem.getText());
toolbar.add(helpButton);
toolbar.setFloatable(false);
add(toolbar, BorderLayout.NORTH);
add(scrollPane, BorderLayout.CENTER);
injectMenu();
((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().addPluginListener(new PluginListener() {
public void pluginStarted(PluginEvent e) {
}
public void pluginStopped(PluginEvent e) {
Plugin plugin = e.getPlugin();
synchronized(serviceInfoToReceiveMap) {
for (Iterator iter = serviceInfoToReceiveMap.entrySet().iterator(); iter.hasNext();) {
Map.Entry entry = (Map.Entry) iter.next();
if(entry.getValue() == plugin) {
serviceInfoToReceiveMap.remove(entry.getKey());
}
}
}
// need to make sure that the menu item tracking this item has it's icon and enabled state updade
JMenuItem item = locateMatchingMenuItem(plugin.getName());
if (item!=null) {
item.setEnabled(true);
item.setIcon(null);
}
discoveredDevices.fireContentsChanged();
}});
File fileLocation = getPreferenceFileLocation();
XStream stream = new XStream(new DomDriver());
if (fileLocation.exists()) {
try {
this.preferenceModel = (ZeroConfPreferenceModel) stream
.fromXML(new FileReader(fileLocation));
} catch (Exception e) {
LOG.error("Failed to load ZeroConfPlugin configuration file",e);
}
}else {
this.preferenceModel = new ZeroConfPreferenceModel();
}
discoveredDevices.addListDataListener(new ListDataListener() {
public void intervalAdded(ListDataEvent e) {
setIconIfNeeded();
}
public void intervalRemoved(ListDataEvent e) {
setIconIfNeeded();
}
public void contentsChanged(ListDataEvent e) {
setIconIfNeeded();
}});
}
/**
* Sets the icon of this parent container (a JTabbedPane, we hope
*
*/
private void setIconIfNeeded() {
Container container = this.getParent();
if(container instanceof JTabbedPane) {
JTabbedPane tabbedPane = (JTabbedPane) container;
Icon icon = discoveredDevices.getSize()==0?null:DEVICE_DISCOVERED_ICON;
tabbedPane.setIconAt(tabbedPane.indexOfTab(getName()), icon);
}else {
LOG.warn("Parent is not a TabbedPane, not setting icon: " + container.getClass().getName());
}
}
/**
* Attempts to find a JFrame container as a parent,and addse a "Connect to" menu
*
*/
private void injectMenu() {
JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this);
if(frame == null) {
LOG.info("Could not locate parent JFrame to add menu to");
}else {
JMenuBar menuBar = frame.getJMenuBar();
if(menuBar==null ) {
menuBar = new JMenuBar();
frame.setJMenuBar(menuBar);
}
insertToLeftOfHelp(menuBar, connectToMenu);
connectToMenu.add(nothingToConnectTo);
discoveredDevices.addListDataListener(new ListDataListener() {
public void intervalAdded(ListDataEvent e) {
if(discoveredDevices.getSize()>0) {
connectToMenu.remove(nothingToConnectTo);
}
}
public void intervalRemoved(ListDataEvent e) {
if(discoveredDevices.getSize()==0) {
connectToMenu.add(nothingToConnectTo,0);
}
}
public void contentsChanged(ListDataEvent e) {
}});
nothingToConnectTo.setEnabled(false);
connectToMenu.addSeparator();
connectToMenu.add(helpItem);
}
}
private void insertToLeftOfHelp(JMenuBar menuBar, JMenu item) {
for (int i = 0; i < menuBar.getMenuCount(); i++) {
JMenu menu = menuBar.getMenu(i);
if(menu.getText().equalsIgnoreCase("help")) {
menuBar.add(item, i-1);
}
}
LOG.warn("menu '" + item.getText() + "' was NOT added because the 'Help' menu could not be located");
}
private void deviceDiscovered(final ServiceInfo info) {
final String name = info.getName();
// TODO currently adding ALL devices to autoConnectlist
// preferenceModel.addAutoConnectDevice(name);
JMenuItem connectToDeviceMenuItem = new JMenuItem(new AbstractAction(info.getName()) {
public void actionPerformed(ActionEvent e) {
connectTo(info);
}});
if(discoveredDevices.getSize()>0) {
for (int i = 0; i < discoveredDevices.getSize(); i++) {
if (name.compareToIgnoreCase(((ServiceInfo) discoveredDevices
.elementAt(i)).getName()) < 0) {
discoveredDevices.insertElementAt(info, i);
}
}
Component[] menuComponents = connectToMenu.getMenuComponents();
boolean located = false;
for (int i = 0; i < menuComponents.length; i++) {
Component c = menuComponents[i];
if (!(c instanceof JPopupMenu.Separator)) {
JMenuItem item = (JMenuItem) menuComponents[i];
if (item.getText().compareToIgnoreCase(name) < 0) {
connectToMenu.insert(connectToDeviceMenuItem, i);
located = true;
break;
}
}
}
if(!located) {
connectToMenu.insert(connectToDeviceMenuItem,0);
}
}else {
discoveredDevices.addElement(info);
connectToMenu.insert(connectToDeviceMenuItem,0);
}
// if the device name is one of the autoconnect devices, then connect immediately
if (preferenceModel.getAutoConnectDevices().contains(name)) {
new Thread(new Runnable() {
public void run() {
LOG.info("Auto-connecting to " + name);
connectTo(info);
}
}).start();
}
}
private void deviceRemoved(String name) {
for (int i = 0; i < discoveredDevices.getSize(); i++) {
if (name.compareToIgnoreCase(((ServiceInfo) discoveredDevices
.elementAt(i)).getName()) == 0) {
discoveredDevices.remove(i);
}
}
Component[] menuComponents = connectToMenu.getMenuComponents();
for (int i = 0; i < menuComponents.length; i++) {
Component c = menuComponents[i];
if (!(c instanceof JPopupMenu.Separator)) {
JMenuItem item = (JMenuItem) menuComponents[i];
if (item.getText().compareToIgnoreCase(name) == 0) {
connectToMenu.remove(item);
break;
}
}
}
}
private class ZeroConfServiceListener implements ServiceListener {
public void serviceAdded(final ServiceEvent event) {
LOG.info("Service Added: " + event);
/**
* it's not very clear whether we should do the resolving in a
* background thread or not.. All it says is to NOT do it in the AWT
* thread, so I'm thinking it probably should be a background thread
*/
Runnable runnable = new Runnable() {
public void run() {
ZeroConfPlugin.this.jmDNS.requestServiceInfo(event
.getType(), event.getName());
}
};
Thread thread = new Thread(runnable,
"ChainsawZeroConfRequestResolutionThread");
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
}
public void serviceRemoved(ServiceEvent event) {
LOG.info("Service Removed: " + event);
deviceRemoved(event.getName());
}
public void serviceResolved(ServiceEvent event) {
LOG.info("Service Resolved: " + event);
deviceDiscovered(event.getInfo());
}
}
private class ServiceInfoListCellRenderer implements
ListCellRenderer {
private JPanel panel = new JPanel(new BorderLayout(15, 15));
private final ImageIcon ICON = new ImageIcon(
ChainsawIcons.ANIM_RADIO_TOWER);
private JLabel iconLabel = new JLabel(ICON);
private JLabel nameLabel = new JLabel();
private JLabel detailLabel = new JLabel();
private JCheckBox autoConnect = new JCheckBox();
private Box southBox = Box.createVerticalBox();
private JCheckBox checkBox = new JCheckBox();
private ServiceInfoListCellRenderer() {
Font font = nameLabel.getFont();
font = font.deriveFont(font.getSize() + 6);
nameLabel.setFont(font);
panel.setLayout(new BorderLayout());
panel.add(iconLabel, BorderLayout.WEST);
JPanel centerPanel = new JPanel(new BorderLayout(3, 3));
centerPanel.add(nameLabel, BorderLayout.CENTER);
centerPanel.add(southBox, BorderLayout.SOUTH);
panel.add(centerPanel, BorderLayout.CENTER);
southBox.add(detailLabel);
Box hBox = Box.createHorizontalBox();
hBox.add(Box.createHorizontalGlue());
hBox.add(new JLabel("Auto-connect:"));
hBox.add(checkBox);
southBox.add(hBox);
panel.setBorder(BorderFactory.createEtchedBorder());
}
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
if (isSelected) {
panel.setBackground(list.getSelectionBackground());
panel.setForeground(list.getSelectionForeground());
} else {
panel.setBackground(list.getBackground());
panel.setForeground(list.getForeground());
}
ServiceInfo info = (ServiceInfo) value;
nameLabel.setText(info.getName());
detailLabel.setText(info.getHostAddress() + ":" + info.getPort());
iconLabel.setIcon(isConnectedTo(info)?ICON:null);
checkBox.setSelected(preferenceModel.getAutoConnectDevices().contains(info.getName()));
return panel;
}
}
private class ConnectorMouseListener extends MouseAdapter {
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
int index = listBox.locationToIndex(e.getPoint());
ListModel dlm = discoveredDevices;
ServiceInfo info = (ServiceInfo) dlm.getElementAt(index);
listBox.ensureIndexIsVisible(index);
if (!isConnectedTo(info)) {
connectTo(info);
} else {
disconnectFrom(info);
}
}
}
public void mousePressed(MouseEvent e) {
/**
* This methodh handles when the user clicks the
* auto-connect
*/
int index = listBox.locationToIndex(e.getPoint());
if (index != -1) {
// Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), )
Component c = SwingUtilities.getDeepestComponentAt(ZeroConfPlugin.this, e.getX(), e.getY());
if (c instanceof JCheckBox) {
ServiceInfo info = (ServiceInfo) listBox.getModel()
.getElementAt(index);
String name = info.getName();
if (preferenceModel.getAutoConnectDevices().contains(name)) {
preferenceModel.removeAutoConnectDevice(name);
} else {
preferenceModel.addAutoConnectDevice(name);
}
discoveredDevices.fireContentsChanged();
repaint();
}
}
}
}
private void disconnectFrom(ServiceInfo info) {
if(!isConnectedTo(info)) {
return; // not connected, who cares
}
Plugin plugin;
synchronized (serviceInfoToReceiveMap) {
plugin = (Plugin) serviceInfoToReceiveMap.get(info);
}
((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().stopPlugin(plugin.getName());
}
/**
* returns true if the serviceInfo record already has a matching connected receiver
* @param info
* @return
*/
private boolean isConnectedTo(ServiceInfo info) {
return serviceInfoToReceiveMap.containsKey(info);
}
/**
* Starts a receiver to the appender referenced within the ServiceInfo
* @param info
*/
private void connectTo(ServiceInfo info) {
LOG.info("Connection request for " + info);
int port = info.getPort();
String hostAddress = info.getHostAddress();
// TODO handle different receivers than just SocketHubReceiver
SocketHubReceiver receiver = new SocketHubReceiver();
receiver.setHost(hostAddress);
receiver.setPort(port);
receiver.setName(info.getName());
((LoggerRepositoryEx)LogManager.getLoggerRepository()).getPluginRegistry().addPlugin(receiver);
receiver.activateOptions();
LOG.info("Receiver '" + receiver.getName() + "' has been started");
// ServiceInfo obeys equals() and hashCode() contracts, so this should be safe.
synchronized (serviceInfoToReceiveMap) {
serviceInfoToReceiveMap.put(info, receiver);
}
// this instance of the menu item needs to be disabled, and have an icon added
JMenuItem item = locateMatchingMenuItem(info.getName());
if (item!=null) {
item.setIcon(new ImageIcon(ChainsawIcons.ANIM_NET_CONNECT));
item.setEnabled(false);
}
// now notify the list model has changed, it needs redrawing of the receiver icon now it's connected
discoveredDevices.fireContentsChanged();
}
/**
* Finds the matching JMenuItem based on name, may return null if there is no match.
*
* @param name
* @return
*/
private JMenuItem locateMatchingMenuItem(String name) {
Component[] menuComponents = connectToMenu.getMenuComponents();
for (int i = 0; i < menuComponents.length; i++) {
Component c = menuComponents[i];
if (!(c instanceof JPopupMenu.Separator)) {
JMenuItem item = (JMenuItem) menuComponents[i];
if (item.getText().compareToIgnoreCase(name) == 0) {
return item;
}
}
}
return null;
}
public static void main(String[] args) throws InterruptedException {
BasicConfigurator.resetConfiguration();
BasicConfigurator.configure();
final ZeroConfPlugin plugin = new ZeroConfPlugin();
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().setLayout(new BorderLayout());
frame.getContentPane().add(plugin, BorderLayout.CENTER);
// needs to be activated after being added to the JFrame for Menu injection to work
plugin.activateOptions();
frame.pack();
frame.setVisible(true);
Thread thread = new Thread(new Runnable() {
public void run() {
plugin.shutdown();
}
});
Runtime.getRuntime().addShutdownHook(thread);
}
}