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     */
017    package org.apache.logging.log4j.jmx.gui;
018    
019    import java.awt.BorderLayout;
020    import java.awt.Color;
021    import java.awt.Component;
022    import java.awt.Font;
023    import java.awt.event.ActionEvent;
024    import java.io.IOException;
025    import java.io.PrintWriter;
026    import java.io.StringWriter;
027    import java.util.HashMap;
028    import java.util.Map;
029    
030    import javax.management.InstanceNotFoundException;
031    import javax.management.JMException;
032    import javax.management.ListenerNotFoundException;
033    import javax.management.MBeanServerDelegate;
034    import javax.management.MBeanServerNotification;
035    import javax.management.MalformedObjectNameException;
036    import javax.management.Notification;
037    import javax.management.NotificationFilterSupport;
038    import javax.management.NotificationListener;
039    import javax.management.ObjectName;
040    import javax.management.remote.JMXConnector;
041    import javax.management.remote.JMXConnectorFactory;
042    import javax.management.remote.JMXServiceURL;
043    import javax.swing.AbstractAction;
044    import javax.swing.JFrame;
045    import javax.swing.JOptionPane;
046    import javax.swing.JPanel;
047    import javax.swing.JScrollPane;
048    import javax.swing.JTabbedPane;
049    import javax.swing.JTextArea;
050    import javax.swing.JToggleButton;
051    import javax.swing.ScrollPaneConstants;
052    import javax.swing.SwingUtilities;
053    import javax.swing.UIManager;
054    import javax.swing.UIManager.LookAndFeelInfo;
055    
056    import org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean;
057    import org.apache.logging.log4j.core.jmx.Server;
058    import org.apache.logging.log4j.core.jmx.StatusLoggerAdminMBean;
059    import org.apache.logging.log4j.core.util.Assert;
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     */
070    public 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 final Map<ObjectName, Component> contextObjNameToTabbedPaneMap = new HashMap<ObjectName, Component>();
075        private final 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.requireNonNull(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            SwingUtilities.invokeLater(new Runnable() {
181                @Override
182                public void run() { // LOG4J2-538
183                    handleNotificationInAwtEventThread(notif, paramObject);
184                }
185            });
186        }
187    
188        private void handleNotificationInAwtEventThread(final Notification notif, final Object paramObject) {
189            if (StatusLoggerAdminMBean.NOTIF_TYPE_MESSAGE.equals(notif.getType())) {
190                JTextArea text = statusLogTextAreaMap.get(paramObject);
191                if (text != null) {
192                    text.append(notif.getMessage() + '\n');
193                }
194                return;
195            }
196            if (notif instanceof MBeanServerNotification) {
197                MBeanServerNotification mbsn = (MBeanServerNotification) notif;
198                ObjectName mbeanName = mbsn.getMBeanName();
199                if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notif.getType())) {
200                    onMBeanRegistered(mbeanName);
201                } else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(notif.getType())) {
202                    onMBeanUnregistered(mbeanName);
203                }
204            }
205        }
206    
207        /**
208         * Called every time a Log4J2 MBean was registered in the MBean server.
209         *
210         * @param mbeanName ObjectName of the registered Log4J2 MBean
211         */
212        private void onMBeanRegistered(ObjectName mbeanName) {
213            if (client.isLoggerContext(mbeanName)) {
214                try {
215                    LoggerContextAdminMBean ctx = client.getLoggerContextAdmin(mbeanName);
216                    addWidgetForLoggerContext(ctx);
217                } catch (Exception ex) {
218                    handle("Could not add tab for new MBean " + mbeanName, ex);
219                }
220            }
221        }
222    
223        /**
224         * Called every time a Log4J2 MBean was unregistered from the MBean server.
225         *
226         * @param mbeanName ObjectName of the unregistered Log4J2 MBean
227         */
228        private void onMBeanUnregistered(ObjectName mbeanName) {
229            if (client.isLoggerContext(mbeanName)) {
230                try {
231                    removeWidgetForLoggerContext(mbeanName);
232                } catch (Exception ex) {
233                    handle("Could not remove tab for " + mbeanName, ex);
234                }
235            }
236        }
237    
238        private void handle(String msg, Exception ex) {
239            System.err.println(msg);
240            ex.printStackTrace();
241    
242            StringWriter sw = new StringWriter(INITIAL_STRING_WRITER_SIZE);
243            ex.printStackTrace(new PrintWriter(sw));
244            JOptionPane.showMessageDialog(this, sw.toString(), msg, JOptionPane.ERROR_MESSAGE);
245        }
246    
247        /**
248         * Connects to the specified location and shows this panel in a window.
249         * <p>
250         * Useful links:
251         * http://www.componative.com/content/controller/developer/insights
252         * /jconsole3/
253         *
254         * @param args must have at least one parameter, which specifies the
255         *            location to connect to. Must be of the form {@code host:port}
256         *            or {@code service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi}
257         *            or
258         *            {@code service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi}
259         * @throws Exception if anything goes wrong
260         */
261        public static void main(final String[] args) throws Exception {
262            if (args.length < 1) {
263                usage();
264                return;
265            }
266            String serviceUrl = args[0];
267            if (!serviceUrl.startsWith("service:jmx")) {
268                serviceUrl = "service:jmx:rmi:///jndi/rmi://" + args[0] + "/jmxrmi";
269            }
270            final JMXServiceURL url = new JMXServiceURL(serviceUrl);
271            final Map<String, String> paramMap = new HashMap<String, String>();
272            for (final Object objKey : System.getProperties().keySet()) {
273                final String key = (String) objKey;
274                paramMap.put(key, System.getProperties().getProperty(key));
275            }
276            final JMXConnector connector = JMXConnectorFactory.connect(url, paramMap);
277            final Client client = new Client(connector);
278            final String title = "Log4j JMX Client - " + url;
279    
280            SwingUtilities.invokeLater(new Runnable() {
281                @Override
282                public void run() {
283                    installLookAndFeel();
284                    try {
285                        final ClientGui gui = new ClientGui(client);
286                        final JFrame frame = new JFrame(title);
287                        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
288                        frame.getContentPane().add(gui, BorderLayout.CENTER);
289                        frame.pack();
290                        frame.setVisible(true);
291                    } catch (final Exception ex) {
292                        // if console is visible, print error so that
293                        // the stack trace remains visible after error dialog is
294                        // closed
295                        ex.printStackTrace();
296    
297                        // show error in dialog: there may not be a console window
298                        // visible
299                        final StringWriter sr = new StringWriter();
300                        ex.printStackTrace(new PrintWriter(sr));
301                        JOptionPane.showMessageDialog(null, sr.toString(), "Error", JOptionPane.ERROR_MESSAGE);
302                    }
303                }
304            });
305        }
306    
307        private static void usage() {
308            final String me = ClientGui.class.getName();
309            System.err.println("Usage: java " + me + " <host>:<port>");
310            System.err.println("   or: java " + me + " service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi");
311            final String longAdr = " service:jmx:rmi://<host>:<port>/jndi/rmi://<host>:<port>/jmxrmi";
312            System.err.println("   or: java " + me + longAdr);
313        }
314    
315        private static void installLookAndFeel() {
316            try {
317                for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
318                    if ("Nimbus".equals(info.getName())) {
319                        UIManager.setLookAndFeel(info.getClassName());
320                        return;
321                    }
322                }
323            } catch (final Exception ex) {
324                ex.printStackTrace();
325            }
326            try {
327                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
328            } catch (final Exception e) {
329                e.printStackTrace();
330            }
331        }
332    }