001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.jmx.gui;
018
019import java.awt.BorderLayout;
020import java.awt.Color;
021import java.awt.Component;
022import java.awt.Font;
023import java.awt.event.ActionEvent;
024import java.io.IOException;
025import java.io.PrintWriter;
026import java.io.StringWriter;
027import java.util.HashMap;
028import java.util.Map;
029
030import javax.management.InstanceNotFoundException;
031import javax.management.JMException;
032import javax.management.ListenerNotFoundException;
033import javax.management.MBeanServerDelegate;
034import javax.management.MBeanServerNotification;
035import javax.management.MalformedObjectNameException;
036import javax.management.Notification;
037import javax.management.NotificationFilterSupport;
038import javax.management.NotificationListener;
039import javax.management.ObjectName;
040import javax.management.remote.JMXConnector;
041import javax.management.remote.JMXConnectorFactory;
042import javax.management.remote.JMXServiceURL;
043import javax.swing.AbstractAction;
044import javax.swing.JFrame;
045import javax.swing.JOptionPane;
046import javax.swing.JPanel;
047import javax.swing.JScrollPane;
048import javax.swing.JTabbedPane;
049import javax.swing.JTextArea;
050import javax.swing.JToggleButton;
051import javax.swing.ScrollPaneConstants;
052import javax.swing.SwingUtilities;
053import javax.swing.UIManager;
054import javax.swing.UIManager.LookAndFeelInfo;
055
056import org.apache.logging.log4j.core.helpers.Assert;
057import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean;
058import org.apache.logging.log4j.core.jmx.Server;
059import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean;
060
061/**
062 * Swing GUI that connects to a Java process via JMX and allows the user to view
063 * and modify the Log4j 2 configuration, as well as monitor status logs.
064 * 
065 * @see <a href=
066 *      "http://docs.oracle.com/javase/6/docs/technotes/guides/management/jconsole.html"
067 *      >http://docs.oracle.com/javase/6/docs/technotes/guides/management/
068 *      jconsole.html</a >
069 */
070public class ClientGUI extends JPanel implements NotificationListener {
071    private static final long serialVersionUID = -253621277232291174L;
072    private static final int INITIAL_STRING_WRITER_SIZE = 1024;
073    private final Client client;
074    private Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<ObjectName, Component>();
075    private Map<ObjectName, JTextArea> statusLogTextAreaMap = new HashMap<ObjectName, JTextArea>();
076    private JTabbedPane tabbedPaneContexts;
077
078    public ClientGUI(final Client client) throws IOException, JMException {
079        this.client = Assert.isNotNull(client, "client");
080        createWidgets();
081        populateWidgets();
082
083        // register for Notifications if LoggerContext MBean was added/removed
084        ObjectName addRemoveNotifs = MBeanServerDelegate.DELEGATE_NAME;
085        NotificationFilterSupport filter = new NotificationFilterSupport();
086        filter.enableType(Server.DOMAIN); // only interested in Log4J2 MBeans
087        client.getConnection().addNotificationListener(addRemoveNotifs, this, null, null);
088    }
089
090    private void createWidgets() {
091        tabbedPaneContexts = new JTabbedPane();
092        this.setLayout(new BorderLayout());
093        this.add(tabbedPaneContexts, BorderLayout.CENTER);
094    }
095
096    private void populateWidgets() throws IOException, JMException {
097        for (final LoggerContextAdminMBean ctx : client.getLoggerContextAdmins()) {
098            addWidgetForLoggerContext(ctx);
099        }
100    }
101
102    private void addWidgetForLoggerContext(final LoggerContextAdminMBean ctx) throws MalformedObjectNameException,
103            IOException, InstanceNotFoundException {
104        JTabbedPane contextTabs = new JTabbedPane();
105        contextObjNameToTabbedPaneMap.put(ctx.getObjectName(), contextTabs);
106        tabbedPaneContexts.addTab("LoggerContext: " + ctx.getName(), contextTabs);
107
108        String contextName = ctx.getName();
109        StatusLoggerAdminMBean status = client.getStatusLoggerAdmin(contextName);
110        if (status != null) {
111            JTextArea text = createTextArea();
112            final String[] messages = status.getStatusDataHistory();
113            for (final String message : messages) {
114                text.append(message + "\n");
115            }
116            statusLogTextAreaMap.put(ctx.getObjectName(), text);
117            registerListeners(status);
118            JScrollPane scroll = scroll(text);
119            contextTabs.addTab("StatusLogger", scroll);
120        }
121
122        final ClientEditConfigPanel editor = new ClientEditConfigPanel(ctx);
123        contextTabs.addTab("Configuration", editor);
124    }
125
126    private void removeWidgetForLoggerContext(ObjectName loggerContextObjName) throws JMException, IOException {
127        Component tab = contextObjNameToTabbedPaneMap.get(loggerContextObjName);
128        if (tab != null) {
129            tabbedPaneContexts.remove(tab);
130        }
131        statusLogTextAreaMap.remove(loggerContextObjName);
132        final ObjectName objName = client.getStatusLoggerObjectName(loggerContextObjName);
133        try {
134            // System.out.println("Remove listener for " + objName);
135            client.getConnection().removeNotificationListener(objName, this);
136        } catch (ListenerNotFoundException ignored) {
137        }
138    }
139
140    private JTextArea createTextArea() {
141        JTextArea result = new JTextArea();
142        result.setEditable(false);
143        result.setBackground(this.getBackground());
144        result.setForeground(Color.black);
145        result.setFont(new Font("Monospaced", Font.PLAIN, result.getFont().getSize()));
146        result.setWrapStyleWord(true);
147        return result;
148    }
149
150    private JScrollPane scroll(final JTextArea text) {
151        final JToggleButton toggleButton = new JToggleButton();
152        toggleButton.setAction(new AbstractAction() {
153            private static final long serialVersionUID = -4214143754637722322L;
154
155            @Override
156            public void actionPerformed(final ActionEvent e) {
157                final boolean wrap = toggleButton.isSelected();
158                text.setLineWrap(wrap);
159            }
160        });
161        toggleButton.setToolTipText("Toggle line wrapping");
162        final JScrollPane scrollStatusLog = new JScrollPane(text, //
163                ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, //
164                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
165        scrollStatusLog.setCorner(ScrollPaneConstants.LOWER_RIGHT_CORNER, toggleButton);
166        return scrollStatusLog;
167    }
168
169    private void registerListeners(StatusLoggerAdminMBean status) throws InstanceNotFoundException,
170            MalformedObjectNameException, IOException {
171        final NotificationFilterSupport filter = new NotificationFilterSupport();
172        filter.enableType(StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE);
173        final ObjectName objName = status.getObjectName();
174        // System.out.println("Add listener for " + objName);
175        client.getConnection().addNotificationListener(objName, this, filter, status.getContextName());
176    }
177
178    @Override
179    public void handleNotification(final Notification notif, final Object paramObject) {
180        if (StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE.equals(notif.getType())) {
181            JTextArea text = statusLogTextAreaMap.get(paramObject);
182            if (text != null) {
183                text.append(notif.getMessage() + "\n");
184            }
185            return;
186        }
187        if (notif instanceof MBeanServerNotification) {
188            MBeanServerNotification mbsn = (MBeanServerNotification) notif;
189            ObjectName mbeanName = mbsn.getMBeanName();
190            if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notif.getType())) {
191                onMBeanRegistered(mbeanName);
192            } else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(notif.getType())) {
193                onMBeanUnregistered(mbeanName);
194            }
195        }
196    }
197
198    /**
199     * Called every time a Log4J2 MBean was registered in the MBean server.
200     * 
201     * @param mbeanName ObjectName of the registered Log4J2 MBean
202     */
203    private void onMBeanRegistered(ObjectName mbeanName) {
204        if (client.isLoggerContext(mbeanName)) {
205            try {
206                LoggerContextAdminMBean ctx = client.getLoggerContextAdmin(mbeanName);
207                addWidgetForLoggerContext(ctx);
208            } catch (Exception ex) {
209                handle("Could not add tab for new MBean " + mbeanName, ex);
210            }
211        }
212    }
213
214    /**
215     * Called every time a Log4J2 MBean was unregistered from the MBean server.
216     * 
217     * @param mbeanName ObjectName of the unregistered Log4J2 MBean
218     */
219    private void onMBeanUnregistered(ObjectName mbeanName) {
220        if (client.isLoggerContext(mbeanName)) {
221            try {
222                removeWidgetForLoggerContext(mbeanName);
223            } catch (Exception ex) {
224                handle("Could not remove tab for " + mbeanName, ex);
225            }
226        }
227    }
228
229    private void handle(String msg, Exception ex) {
230        System.err.println(msg);
231        ex.printStackTrace();
232
233        StringWriter sw = new StringWriter(INITIAL_STRING_WRITER_SIZE);
234        ex.printStackTrace(new PrintWriter(sw));
235        JOptionPane.showMessageDialog(this, sw.toString(), msg, JOptionPane.ERROR_MESSAGE);
236    }
237
238    /**
239     * Connects to the specified location and shows this panel in a window.
240     * <p>
241     * Useful links:
242     * http://www.componative.com/content/controller/developer/insights
243     * /jconsole3/
244     * 
245     * @param args must have at least one parameter, which specifies the
246     *            location to connect to. Must be of the form {@code host:port}
247     *            or {@code service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi}
248     *            or
249     *            {@code service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi}
250     * @throws Exception if anything goes wrong
251     */
252    public static void main(final String[] args) throws Exception {
253        if (args.length < 1) {
254            usage();
255            return;
256        }
257        String serviceUrl = args[0];
258        if (!serviceUrl.startsWith("service:jmx")) {
259            serviceUrl = "service:jmx:rmi:///jndi/rmi://" + args[0] + "/jmxrmi";
260        }
261        final JMXServiceURL url = new JMXServiceURL(serviceUrl);
262        final Map<String, String> paramMap = new HashMap<String, String>();
263        for (final Object objKey : System.getProperties().keySet()) {
264            final String key = (String) objKey;
265            paramMap.put(key, System.getProperties().getProperty(key));
266        }
267        final JMXConnector connector = JMXConnectorFactory.connect(url, paramMap);
268        final Client client = new Client(connector);
269        final String title = "Log4j JMX Client - " + url;
270
271        SwingUtilities.invokeLater(new Runnable() {
272            @Override
273            public void run() {
274                installLookAndFeel();
275                try {
276                    final ClientGUI gui = new ClientGUI(client);
277                    final JFrame frame = new JFrame(title);
278                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
279                    frame.getContentPane().add(gui, BorderLayout.CENTER);
280                    frame.pack();
281                    frame.setVisible(true);
282                } catch (final Exception ex) {
283                    // if console is visible, print error so that
284                    // the stack trace remains visible after error dialog is
285                    // closed
286                    ex.printStackTrace();
287
288                    // show error in dialog: there may not be a console window
289                    // visible
290                    final StringWriter sr = new StringWriter();
291                    ex.printStackTrace(new PrintWriter(sr));
292                    JOptionPane.showMessageDialog(null, sr.toString(), "Error", JOptionPane.ERROR_MESSAGE);
293                }
294            }
295        });
296    }
297
298    private static void usage() {
299        final String me = ClientGUI.class.getName();
300        System.err.println("Usage: java " + me + " <host>:<port>");
301        System.err.println("   or: java " + me + " service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi");
302        final String longAdr = " service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi";
303        System.err.println("   or: java " + me + longAdr);
304    }
305
306    private static void installLookAndFeel() {
307        try {
308            for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
309                if ("Nimbus".equals(info.getName())) {
310                    UIManager.setLookAndFeel(info.getClassName());
311                    return;
312                }
313            }
314        } catch (final Exception ex) {
315            ex.printStackTrace();
316        }
317        try {
318            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
319        } catch (final Exception e) {
320            e.printStackTrace();
321        }
322    }
323}