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  
18  package org.apache.log4j.chainsaw;
19  
20  import org.apache.log4j.*;
21  import org.apache.log4j.chainsaw.color.RuleColorizer;
22  import org.apache.log4j.chainsaw.dnd.FileDnDTarget;
23  import org.apache.log4j.chainsaw.help.HelpManager;
24  import org.apache.log4j.chainsaw.help.Tutorial;
25  import org.apache.log4j.chainsaw.helper.SwingHelper;
26  import org.apache.log4j.chainsaw.icons.ChainsawIcons;
27  import org.apache.log4j.chainsaw.icons.LineIconFactory;
28  import org.apache.log4j.chainsaw.messages.MessageCenter;
29  import org.apache.log4j.chainsaw.osx.OSXIntegration;
30  import org.apache.log4j.chainsaw.plugins.PluginClassLoaderFactory;
31  import org.apache.log4j.chainsaw.prefs.*;
32  import org.apache.log4j.chainsaw.receivers.ReceiversHelper;
33  import org.apache.log4j.chainsaw.receivers.ReceiversPanel;
34  import org.apache.log4j.chainsaw.vfs.VFSLogFilePatternReceiver;
35  import org.apache.log4j.net.SocketNodeEventListener;
36  import org.apache.log4j.plugins.*;
37  import org.apache.log4j.rewrite.PropertyRewritePolicy;
38  import org.apache.log4j.rewrite.RewriteAppender;
39  import org.apache.log4j.rule.ExpressionRule;
40  import org.apache.log4j.rule.Rule;
41  import org.apache.log4j.spi.Decoder;
42  import org.apache.log4j.spi.LoggerRepository;
43  import org.apache.log4j.spi.LoggerRepositoryEx;
44  import org.apache.log4j.spi.LoggingEvent;
45  import org.apache.log4j.xml.DOMConfigurator;
46  import org.apache.log4j.xml.XMLDecoder;
47  
48  import javax.swing.*;
49  import javax.swing.event.ChangeEvent;
50  import javax.swing.event.ChangeListener;
51  import javax.swing.event.EventListenerList;
52  import javax.swing.event.HyperlinkEvent;
53  import java.awt.*;
54  import java.awt.event.*;
55  import java.beans.PropertyChangeListener;
56  import java.io.File;
57  import java.io.IOException;
58  import java.lang.reflect.Method;
59  import java.net.MalformedURLException;
60  import java.net.URISyntaxException;
61  import java.net.URL;
62  import java.security.*;
63  import java.util.*;
64  import java.util.List;
65  
66  
67  /**
68   * The main entry point for Chainsaw, this class represents the first frame
69   * that is used to display a Welcome panel, and any other panels that are
70   * generated because Logging Events are streamed via a Receiver, or other
71   * mechanism.
72   * <p>
73   * NOTE: Some of Chainsaw's application initialization should be performed prior
74   * to activating receivers and the logging framework used to perform self-logging.
75   * <p>
76   * DELAY as much as possible the logging framework initialization process,
77   * currently initialized by the creation of a ChainsawAppenderHandler.
78   *
79   * @author Scott Deboy &lt;sdeboy@apache.org&gt;
80   * @author Paul Smith  &lt;psmith@apache.org&gt;
81   */
82  public class LogUI extends JFrame implements ChainsawViewer, SettingsListener {
83      private static final String MAIN_WINDOW_HEIGHT = "main.window.height";
84      private static final String MAIN_WINDOW_WIDTH = "main.window.width";
85      private static final String MAIN_WINDOW_Y = "main.window.y";
86      private static final String MAIN_WINDOW_X = "main.window.x";
87      private static ChainsawSplash splash;
88      private static final double DEFAULT_MAIN_RECEIVER_SPLIT_LOCATION = 0.85d;
89      private final JFrame preferencesFrame = new JFrame();
90      private boolean noReceiversDefined;
91      private ReceiversPanel receiversPanel;
92      private ChainsawTabbedPane tabbedPane;
93      private JToolBar toolbar;
94      private ChainsawStatusBar statusBar;
95      private ApplicationPreferenceModel applicationPreferenceModel;
96      private ApplicationPreferenceModelPanel applicationPreferenceModelPanel;
97      private final Map tableModelMap = new HashMap();
98      private final Map tableMap = new HashMap();
99      private final List<String> filterableColumns = new ArrayList<>();
100     private final Map<String, Component> panelMap = new HashMap<>();
101     ChainsawAppenderHandler handler;
102     private ChainsawToolBarAndMenus tbms;
103     private ChainsawAbout aboutBox;
104     private final SettingsManager sm = SettingsManager.getInstance();
105     private final JFrame tutorialFrame = new JFrame("Chainsaw Tutorial");
106     private JSplitPane mainReceiverSplitPane;
107     private double lastMainReceiverSplitLocation = DEFAULT_MAIN_RECEIVER_SPLIT_LOCATION;
108     private final List<LogPanel> identifierPanels = new ArrayList<>();
109     private int dividerSize;
110     private int cyclicBufferSize;
111     private static Logger logger;
112     private static String configurationURLAppArg;
113 
114     /**
115      * Set to true, if and only if the GUI has completed it's full
116      * initialization. Any logging events that come in must wait until this is
117      * true, and if it is false, should wait on the initializationLock object
118      * until notified.
119      */
120     private boolean isGUIFullyInitialized = false;
121     private final Object initializationLock = new Object();
122 
123     /**
124      * The shutdownAction is called when the user requests to exit Chainsaw, and
125      * by default this exits the VM, but a developer may replace this action with
126      * something that better suits their needs
127      */
128     private Action shutdownAction = null;
129 
130     /**
131      * Clients can register a ShutdownListener to be notified when the user has
132      * requested Chainsaw to exit.
133      */
134     private EventListenerList shutdownListenerList = new EventListenerList();
135     private WelcomePanel welcomePanel;
136 
137     private static final Object repositorySelectorGuard = new Object();
138     private static final LoggerRepositoryExImpl repositoryExImpl = new LoggerRepositoryExImpl(LogManager.getLoggerRepository());
139 
140     private PluginRegistry pluginRegistry;
141     //map of tab names to rulecolorizers
142     private Map<String, RuleColorizer> allColorizers = new HashMap<>();
143     private ReceiverConfigurationPanel receiverConfigurationPanel = new ReceiverConfigurationPanel();
144 
145     /**
146      * Constructor which builds up all the visual elements of the frame including
147      * the Menu bar
148      */
149     public LogUI() {
150         super("Chainsaw");
151         setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
152 
153         if (ChainsawIcons.WINDOW_ICON != null) {
154             setIconImage(new ImageIcon(ChainsawIcons.WINDOW_ICON).getImage());
155         }
156     }
157 
158     private static final void showSplash(Frame owner) {
159         splash = new ChainsawSplash(owner);
160         SwingHelper.centerOnScreen(splash);
161         splash.setVisible(true);
162     }
163 
164     private static final void removeSplash() {
165         if (splash != null) {
166             splash.setVisible(false);
167             splash.dispose();
168         }
169     }
170 
171     /**
172      * Registers a ShutdownListener with this calss so that it can be notified
173      * when the user has requested that Chainsaw exit.
174      *
175      * @param l
176      */
177     public void addShutdownListener(ShutdownListener l) {
178         shutdownListenerList.add(ShutdownListener.class, l);
179     }
180 
181     /**
182      * Removes the registered ShutdownListener so that the listener will not be
183      * notified on a shutdown.
184      *
185      * @param l
186      */
187     public void removeShutdownListener(ShutdownListener l) {
188         shutdownListenerList.remove(ShutdownListener.class, l);
189     }
190 
191     /**
192      * Starts Chainsaw by attaching a new instance to the Log4J main root Logger
193      * via a ChainsawAppender, and activates itself
194      *
195      * @param args
196      */
197     public static void main(String[] args) {
198         if (args.length > 0) {
199             configurationURLAppArg = args[0];
200         }
201 
202         if (OSXIntegration.IS_OSX) {
203             System.setProperty("apple.laf.useScreenMenuBar", "true");
204         }
205 
206 
207         LogManager.setRepositorySelector(() -> repositoryExImpl, repositorySelectorGuard);
208 
209         final ApplicationPreferenceModel model = new ApplicationPreferenceModel();
210 
211         SettingsManager.getInstance().configure(new ApplicationPreferenceModelSaver(model));
212         //if a configuration URL param was provided, set the configuration URL field to null
213         if (configurationURLAppArg != null) {
214             model.setBypassConfigurationURL(configurationURLAppArg);
215         }
216 
217         EventQueue.invokeLater(() -> {
218             String lookAndFeelClassName = model.getLookAndFeelClassName();
219             if (lookAndFeelClassName == null || lookAndFeelClassName.trim().equals("")) {
220                 String osName = System.getProperty("os.name");
221                 if (osName.toLowerCase(Locale.ENGLISH).startsWith("mac")) {
222                     //no need to assign look and feel
223                 } else if (osName.toLowerCase(Locale.ENGLISH).startsWith("windows")) {
224                     lookAndFeelClassName = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";
225                     model.setLookAndFeelClassName(lookAndFeelClassName);
226                 } else if (osName.toLowerCase(Locale.ENGLISH).startsWith("linux")) {
227                     lookAndFeelClassName = "com.sun.java.swing.plaf.gtk.GTKLookAndFeel";
228                     model.setLookAndFeelClassName(lookAndFeelClassName);
229                 }
230             }
231 
232             if (lookAndFeelClassName != null && !(lookAndFeelClassName.trim().equals(""))) {
233                 loadLookAndFeelUsingPluginClassLoader(lookAndFeelClassName);
234             }
235             createChainsawGUI(model, null);
236         });
237     }
238 
239     /**
240      * Creates, activates, and then shows the Chainsaw GUI, optionally showing
241      * the splash screen, and using the passed shutdown action when the user
242      * requests to exit the application (if null, then Chainsaw will exit the vm)
243      *
244      * @param model
245      * @param newShutdownAction DOCUMENT ME!
246      */
247     public static void createChainsawGUI(
248         ApplicationPreferenceModel model, Action newShutdownAction) {
249 
250         if (model.isOkToRemoveSecurityManager()) {
251             MessageCenter
252                 .getInstance()
253                 .addMessage(
254                     "User has authorised removal of Java Security Manager via preferences");
255             System.setSecurityManager(null);
256             // this SHOULD set the Policy/Permission stuff for any
257             // code loaded from our custom classloader.  
258             // crossing fingers...
259             Policy.setPolicy(new Policy() {
260 
261                 public void refresh() {
262                 }
263 
264                 public PermissionCollection getPermissions(CodeSource codesource) {
265                     Permissions perms = new Permissions();
266                     perms.add(new AllPermission());
267                     return (perms);
268                 }
269             });
270         }
271 
272         final LogUI logUI = new LogUI();
273         logUI.applicationPreferenceModel = model;
274 
275         if (model.isShowSplash()) {
276             showSplash(logUI);
277         }
278         logUI.cyclicBufferSize = model.getCyclicBufferSize();
279         logUI.pluginRegistry = repositoryExImpl.getPluginRegistry();
280 
281         logUI.handler = new ChainsawAppenderHandler();
282         logUI.handler.addEventBatchListener(logUI.new NewTabEventBatchReceiver());
283 
284         /**
285          * TODO until we work out how JoranConfigurator might be able to have
286          * configurable class loader, if at all.  For now we temporarily replace the
287          * TCCL so that Plugins that need access to resources in
288          * the Plugins directory can find them (this is particularly
289          * important for the Web start version of Chainsaw
290          */
291         //configuration initialized here
292         logUI.ensureChainsawAppenderHandlerAdded();
293         logger = LogManager.getLogger(LogUI.class);
294 
295         //set hostname, application and group properties which will cause Chainsaw and other apache-generated
296         //logging events to route (by default) to a tab named 'chainsaw-log'
297         PropertyRewritePolicy policy = new PropertyRewritePolicy();
298         policy.setProperties("hostname=chainsaw,application=log,group=chainsaw");
299 
300         RewriteAppender rewriteAppender = new RewriteAppender();
301         rewriteAppender.setRewritePolicy(policy);
302 
303         Enumeration appenders = Logger.getLogger("org.apache").getAllAppenders();
304         if (!appenders.hasMoreElements()) {
305             appenders = Logger.getRootLogger().getAllAppenders();
306         }
307         while (appenders.hasMoreElements()) {
308             Appender nextAppender = (Appender) appenders.nextElement();
309             rewriteAppender.addAppender(nextAppender);
310         }
311         Logger.getLogger("org.apache").removeAllAppenders();
312         Logger.getLogger("org.apache").addAppender(rewriteAppender);
313         Logger.getLogger("org.apache").setAdditivity(false);
314 
315         //commons-vfs uses httpclient for http filesystem support, route this to the chainsaw-log tab as well
316         appenders = Logger.getLogger("httpclient").getAllAppenders();
317         if (!appenders.hasMoreElements()) {
318             appenders = Logger.getRootLogger().getAllAppenders();
319         }
320         while (appenders.hasMoreElements()) {
321             Appender nextAppender = (Appender) appenders.nextElement();
322             rewriteAppender.addAppender(nextAppender);
323         }
324         Logger.getLogger("httpclient").removeAllAppenders();
325         Logger.getLogger("httpclient").addAppender(rewriteAppender);
326         Logger.getLogger("httpclient").setAdditivity(false);
327 
328         //set the commons.vfs.cache logger to info, since it can contain password information
329         Logger.getLogger("org.apache.commons.vfs.cache").setLevel(Level.INFO);
330 
331         Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
332             e.printStackTrace();
333             logger.error("Uncaught exception in thread " + t, e);
334         });
335 
336         String config = configurationURLAppArg;
337         if (config != null) {
338             logger.info("Command-line configuration arg provided (overriding auto-configuration URL) - using: " + config);
339         } else {
340             config = model.getConfigurationURL();
341         }
342 
343         if (config != null && (!config.trim().equals(""))) {
344             config = config.trim();
345             try {
346                 URL configURL = new URL(config);
347                 logger.info("Using '" + config + "' for auto-configuration");
348                 logUI.loadConfigurationUsingPluginClassLoader(configURL);
349             } catch (MalformedURLException e) {
350                 logger.error("Initial configuration - failed to convert config string to url", e);
351             } catch (IOException e) {
352                 logger.error("Unable to access auto-configuration URL: " + config);
353             }
354         }
355 
356         //register a listener to load the configuration when it changes (avoid having to restart Chainsaw when applying a new configuration)
357         //this doesn't remove receivers from receivers panel, it just triggers DOMConfigurator.configure.
358         model.addPropertyChangeListener("configurationURL", evt -> {
359             String newConfiguration = evt.getNewValue().toString();
360             if (newConfiguration != null && !(newConfiguration.trim().equals(""))) {
361                 newConfiguration = newConfiguration.trim();
362                 try {
363                     logger.info("loading updated configuration: " + newConfiguration);
364                     URL newConfigurationURL = new URL(newConfiguration);
365                     File file = new File(newConfigurationURL.toURI());
366                     if (file.exists()) {
367                         logUI.loadConfigurationUsingPluginClassLoader(newConfigurationURL);
368                     } else {
369                         logger.info("Updated configuration but file does not exist");
370                     }
371                 } catch (MalformedURLException | URISyntaxException e) {
372                     logger.error("Updated configuration - failed to convert config string to URL", e);
373                 }
374             }
375         });
376 
377         LogManager.getRootLogger().setLevel(Level.TRACE);
378         EventQueue.invokeLater(logUI::activateViewer);
379 
380         logger.info("SecurityManager is now: " + System.getSecurityManager());
381 
382         if (newShutdownAction != null) {
383             logUI.setShutdownAction(newShutdownAction);
384         } else {
385             logUI.setShutdownAction(
386                 new AbstractAction() {
387                     public void actionPerformed(ActionEvent e) {
388                         System.exit(0);
389                     }
390                 });
391         }
392     }
393 
394     /**
395      * Allow Chainsaw v2 to be ran in-process (configured as a ChainsawAppender)
396      * NOTE: Closing Chainsaw will NOT stop the application generating the events.
397      *
398      * @param appender
399      */
400     public void activateViewer(ChainsawAppender appender) {
401 
402         if (OSXIntegration.IS_OSX) {
403             System.setProperty("apple.laf.useScreenMenuBar", "true");
404         }
405 
406         LogManager.setRepositorySelector(() -> repositoryExImpl, repositorySelectorGuard);
407 
408         //if Chainsaw is launched as an appender, ensure the root logger level is TRACE
409         LogManager.getRootLogger().setLevel(Level.TRACE);
410 
411         final ApplicationPreferenceModel model = new ApplicationPreferenceModel();
412         SettingsManager.getInstance().configure(new ApplicationPreferenceModelSaver(model));
413 
414         cyclicBufferSize = model.getCyclicBufferSize();
415 
416         handler = new ChainsawAppenderHandler(appender);
417         handler.addEventBatchListener(new NewTabEventBatchReceiver());
418 
419         logger = LogManager.getLogger(LogUI.class);
420 
421         setShutdownAction(
422             new AbstractAction() {
423                 public void actionPerformed(ActionEvent e) {
424                 }
425             });
426 
427         applicationPreferenceModel = new ApplicationPreferenceModel();
428 
429         SettingsManager.getInstance().configure(new ApplicationPreferenceModelSaver(model));
430 
431         EventQueue.invokeLater(() -> {
432             loadLookAndFeelUsingPluginClassLoader(model.getLookAndFeelClassName());
433             createChainsawGUI(model, null);
434             getApplicationPreferenceModel().apply(model);
435             activateViewer();
436         });
437     }
438 
439     /**
440      * Initialises the menu's and toolbars, but does not actually create any of
441      * the main panel components.
442      */
443     private void initGUI() {
444 
445         setupHelpSystem();
446         statusBar = new ChainsawStatusBar(this);
447         setupReceiverPanel();
448 
449         setToolBarAndMenus(new ChainsawToolBarAndMenus(this));
450         toolbar = getToolBarAndMenus().getToolbar();
451         setJMenuBar(getToolBarAndMenus().getMenubar());
452 
453         setTabbedPane(new ChainsawTabbedPane());
454         getSettingsManager().addSettingsListener(getTabbedPane());
455         getSettingsManager().configure(getTabbedPane());
456 
457         /**
458          * This adds Drag & Drop capability to Chainsaw
459          */
460         FileDnDTarget dnDTarget = new FileDnDTarget(tabbedPane);
461         dnDTarget.addPropertyChangeListener("fileList", evt -> {
462             final List fileList = (List) evt.getNewValue();
463 
464             Thread thread = new Thread(() -> {
465                 logger.debug("Loading files: " + fileList);
466                 for (Object aFileList : fileList) {
467                     File file = (File) aFileList;
468                     final Decoder decoder = new XMLDecoder();
469                     try {
470                         getStatusBar().setMessage("Loading " + file.getAbsolutePath() + "...");
471                         FileLoadAction.importURL(handler, decoder, file
472                             .getName(), file.toURI().toURL());
473                     } catch (Exception e) {
474                         String errorMsg = "Failed to import a file";
475                         logger.error(errorMsg, e);
476                         getStatusBar().setMessage(errorMsg);
477                     }
478                 }
479 
480             });
481 
482             thread.setPriority(Thread.MIN_PRIORITY);
483             thread.start();
484 
485         });
486 
487         applicationPreferenceModelPanel = new ApplicationPreferenceModelPanel(applicationPreferenceModel);
488 
489         applicationPreferenceModelPanel.setOkCancelActionListener(
490             e -> preferencesFrame.setVisible(false));
491         KeyStroke escape = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
492         Action closeAction = new AbstractAction() {
493             public void actionPerformed(ActionEvent e) {
494                 preferencesFrame.setVisible(false);
495             }
496         };
497         preferencesFrame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escape, "ESCAPE");
498         preferencesFrame.getRootPane().
499             getActionMap().put("ESCAPE", closeAction);
500 
501         OSXIntegration.init(this);
502 
503     }
504 
505     private void initPlugins(PluginRegistry pluginRegistry) {
506         pluginRegistry.addPluginListener(
507             new PluginListener() {
508                 public void pluginStarted(PluginEvent e) {
509                     if (e.getPlugin() instanceof JComponent) {
510                         JComponent plugin = (JComponent) e.getPlugin();
511                         getTabbedPane().addANewTab(plugin.getName(), plugin, null, null);
512                         getPanelMap().put(plugin.getName(), plugin);
513                     }
514                 }
515 
516                 public void pluginStopped(PluginEvent e) {
517                     //TODO remove the plugin from the gui
518                 }
519             });
520 
521         // TODO this should all be in a config file
522 //    ChainsawCentral cc = new ChainsawCentral();
523 //    pluginRegistry.addPlugin(cc);
524 //    cc.activateOptions();
525 
526         try {
527             Class<? extends Plugin> pluginClass = Class.forName("org.apache.log4j.chainsaw.zeroconf.ZeroConfPlugin").asSubclass(Plugin.class);
528             Plugin plugin = pluginClass.newInstance();
529             pluginRegistry.addPlugin(plugin);
530             plugin.activateOptions();
531             MessageCenter.getInstance().getLogger().info("Looks like ZeroConf is available... WooHoo!");
532         } catch (Throwable e) {
533             MessageCenter.getInstance().getLogger().error("Doesn't look like ZeroConf is available", e);
534         }
535     }
536 
537     private void setupReceiverPanel() {
538         receiversPanel = new ReceiversPanel();
539         receiversPanel.addPropertyChangeListener(
540             "visible",
541             evt -> getApplicationPreferenceModel().setReceivers(
542                 (Boolean) evt.getNewValue()));
543     }
544 
545     /**
546      * Initialises the Help system and the WelcomePanel
547      */
548     private void setupHelpSystem() {
549         welcomePanel = new WelcomePanel();
550 
551         JToolBar tb = welcomePanel.getToolbar();
552 
553 
554         tb.add(
555             new SmallButton(
556                 new AbstractAction("Tutorial", new ImageIcon(ChainsawIcons.HELP)) {
557                     public void actionPerformed(ActionEvent e) {
558                         setupTutorial();
559                     }
560                 }));
561         tb.addSeparator();
562 
563         final Action exampleConfigAction =
564             new AbstractAction("View example Receiver configuration") {
565                 public void actionPerformed(ActionEvent e) {
566                     HelpManager.getInstance().setHelpURL(
567                         ChainsawConstants.EXAMPLE_CONFIG_URL);
568                 }
569             };
570 
571         exampleConfigAction.putValue(
572             Action.SHORT_DESCRIPTION,
573             "Displays an example Log4j configuration file with several Receivers defined.");
574 
575         JButton exampleButton = new SmallButton(exampleConfigAction);
576         tb.add(exampleButton);
577 
578         tb.add(Box.createHorizontalGlue());
579 
580         /**
581          * Setup a listener on the HelpURL property and automatically change the WelcomePages URL
582          * to it.
583          */
584         HelpManager.getInstance().addPropertyChangeListener(
585             "helpURL",
586             evt -> {
587                 URL newURL = (URL) evt.getNewValue();
588 
589                 if (newURL != null) {
590                     welcomePanel.setURL(newURL);
591                     ensureWelcomePanelVisible();
592                 }
593             });
594     }
595 
596     private void ensureWelcomePanelVisible() {
597         // ensure that the Welcome Panel is made visible
598         if (!getTabbedPane().containsWelcomePanel()) {
599             addWelcomePanel();
600         }
601         getTabbedPane().setSelectedComponent(welcomePanel);
602     }
603 
604     /**
605      * Given the load event, configures the size/location of the main window etc
606      * etc.
607      *
608      * @param event DOCUMENT ME!
609      */
610     public void loadSettings(LoadSettingsEvent event) {
611         setLocation(
612             event.asInt(LogUI.MAIN_WINDOW_X), event.asInt(LogUI.MAIN_WINDOW_Y));
613         int width = event.asInt(LogUI.MAIN_WINDOW_WIDTH);
614         int height = event.asInt(LogUI.MAIN_WINDOW_HEIGHT);
615         if (width == -1 && height == -1) {
616             width = Toolkit.getDefaultToolkit().getScreenSize().width;
617             height = Toolkit.getDefaultToolkit().getScreenSize().height;
618             setSize(width, height);
619             setExtendedState(getExtendedState() | MAXIMIZED_BOTH);
620         } else {
621             setSize(width, height);
622         }
623 
624         getToolBarAndMenus().stateChange();
625         RuleColorizer colorizer = new RuleColorizer();
626         colorizer.loadColorSettings(ChainsawConstants.DEFAULT_COLOR_RULE_NAME);
627         allColorizers.put(ChainsawConstants.DEFAULT_COLOR_RULE_NAME, colorizer);
628         if (event.getSetting("SavedConfig.logFormat") != null) {
629             receiverConfigurationPanel.getModel().setLogFormat(event.getSetting("SavedConfig.logFormat"));
630         }
631     }
632 
633     /**
634      * Ensures the location/size of the main window is stored with the settings
635      *
636      * @param event DOCUMENT ME!
637      */
638     public void saveSettings(SaveSettingsEvent event) {
639         event.saveSetting(LogUI.MAIN_WINDOW_X, (int) getLocation().getX());
640         event.saveSetting(LogUI.MAIN_WINDOW_Y, (int) getLocation().getY());
641 
642         event.saveSetting(LogUI.MAIN_WINDOW_WIDTH, getWidth());
643         event.saveSetting(LogUI.MAIN_WINDOW_HEIGHT, getHeight());
644         RuleColorizer colorizer = allColorizers.get(ChainsawConstants.DEFAULT_COLOR_RULE_NAME);
645         colorizer.saveColorSettings(ChainsawConstants.DEFAULT_COLOR_RULE_NAME);
646         if (receiverConfigurationPanel.getModel().getLogFormat() != null) {
647             event.saveSetting("SavedConfig.logFormat", receiverConfigurationPanel.getModel().getLogFormat());
648         }
649     }
650 
651     /**
652      * Activates itself as a viewer by configuring Size, and location of itself,
653      * and configures the default Tabbed Pane elements with the correct layout,
654      * table columns, and sets itself viewable.
655      */
656     public void activateViewer() {
657         LoggerRepository repo = LogManager.getLoggerRepository();
658         if (repo instanceof LoggerRepositoryEx) {
659             this.pluginRegistry = ((LoggerRepositoryEx) repo).getPluginRegistry();
660         }
661         initGUI();
662 
663         initPrefModelListeners();
664 
665         /**
666          * We add a simple appender to the MessageCenter logger
667          * so that each message is displayed in the Status bar
668          */
669         MessageCenter.getInstance().getLogger().addAppender(
670             new AppenderSkeleton() {
671                 protected void append(LoggingEvent event) {
672                     getStatusBar().setMessage(event.getMessage().toString());
673                 }
674 
675                 public void close() {
676                 }
677 
678                 public boolean requiresLayout() {
679                     return false;
680                 }
681             });
682 
683 
684         initSocketConnectionListener();
685 
686         if (pluginRegistry.getPlugins(Receiver.class).size() == 0) {
687             noReceiversDefined = true;
688         }
689 
690         getFilterableColumns().add(ChainsawConstants.LEVEL_COL_NAME);
691         getFilterableColumns().add(ChainsawConstants.LOGGER_COL_NAME);
692         getFilterableColumns().add(ChainsawConstants.THREAD_COL_NAME);
693         getFilterableColumns().add(ChainsawConstants.NDC_COL_NAME);
694         getFilterableColumns().add(ChainsawConstants.PROPERTIES_COL_NAME);
695         getFilterableColumns().add(ChainsawConstants.CLASS_COL_NAME);
696         getFilterableColumns().add(ChainsawConstants.METHOD_COL_NAME);
697         getFilterableColumns().add(ChainsawConstants.FILE_COL_NAME);
698         getFilterableColumns().add(ChainsawConstants.NONE_COL_NAME);
699 
700         JPanel panePanel = new JPanel();
701         panePanel.setLayout(new BorderLayout(2, 2));
702 
703         getContentPane().setLayout(new BorderLayout());
704 
705         getTabbedPane().addChangeListener(getToolBarAndMenus());
706         getTabbedPane().addChangeListener(e -> {
707             LogPanel thisLogPanel = getCurrentLogPanel();
708             if (thisLogPanel != null) {
709                 thisLogPanel.updateStatusBar();
710             }
711         });
712 
713         KeyStroke ksRight =
714             KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
715         KeyStroke ksLeft =
716             KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
717         KeyStroke ksGotoLine =
718             KeyStroke.getKeyStroke(KeyEvent.VK_G, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
719 
720         getTabbedPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
721             ksRight, "MoveRight");
722         getTabbedPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
723             ksLeft, "MoveLeft");
724         getTabbedPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
725             ksGotoLine, "GotoLine");
726 
727         Action moveRight =
728             new AbstractAction() {
729                 public void actionPerformed(ActionEvent e) {
730                     int temp = getTabbedPane().getSelectedIndex();
731                     ++temp;
732 
733                     if (temp != getTabbedPane().getTabCount()) {
734                         getTabbedPane().setSelectedTab(temp);
735                     }
736                 }
737             };
738 
739         Action moveLeft =
740             new AbstractAction() {
741                 public void actionPerformed(ActionEvent e) {
742                     int temp = getTabbedPane().getSelectedIndex();
743                     --temp;
744 
745                     if (temp > -1) {
746                         getTabbedPane().setSelectedTab(temp);
747                     }
748                 }
749             };
750 
751         Action gotoLine =
752             new AbstractAction() {
753                 public void actionPerformed(ActionEvent e) {
754                     String inputLine = JOptionPane.showInputDialog(LogUI.this, "Enter the line number to go:", "Goto Line", JOptionPane.PLAIN_MESSAGE);
755                     try {
756                         int lineNumber = Integer.parseInt(inputLine);
757                         int row = getCurrentLogPanel().setSelectedEvent(lineNumber);
758                         if (row == -1) {
759                             JOptionPane.showMessageDialog(LogUI.this, "You have entered an invalid line number", "Error", JOptionPane.ERROR_MESSAGE);
760                         }
761                     } catch (NumberFormatException nfe) {
762                         JOptionPane.showMessageDialog(LogUI.this, "You have entered an invalid line number", "Error", JOptionPane.ERROR_MESSAGE);
763                     }
764                 }
765             };
766 
767 
768         getTabbedPane().getActionMap().put("MoveRight", moveRight);
769         getTabbedPane().getActionMap().put("MoveLeft", moveLeft);
770         getTabbedPane().getActionMap().put("GotoLine", gotoLine);
771 
772         /**
773          * We listen for double clicks, and auto-undock currently selected Tab if
774          * the mouse event location matches the currently selected tab
775          */
776         getTabbedPane().addMouseListener(
777             new MouseAdapter() {
778                 public void mouseClicked(MouseEvent e) {
779                     super.mouseClicked(e);
780 
781                     if (
782                         (e.getClickCount() > 1)
783                             && ((e.getModifiers() & InputEvent.BUTTON1_MASK) > 0)) {
784                         int tabIndex = getTabbedPane().getSelectedIndex();
785 
786                         if (
787                             (tabIndex != -1)
788                                 && (tabIndex == getTabbedPane().getSelectedIndex())) {
789                             LogPanel logPanel = getCurrentLogPanel();
790 
791                             if (logPanel != null) {
792                                 logPanel.undock();
793                             }
794                         }
795                     }
796                 }
797             });
798 
799         panePanel.add(getTabbedPane());
800         addWelcomePanel();
801 
802         getContentPane().add(toolbar, BorderLayout.NORTH);
803         getContentPane().add(statusBar, BorderLayout.SOUTH);
804 
805         mainReceiverSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panePanel, receiversPanel);
806         dividerSize = mainReceiverSplitPane.getDividerSize();
807         mainReceiverSplitPane.setDividerLocation(-1);
808 
809         getContentPane().add(mainReceiverSplitPane, BorderLayout.CENTER);
810 
811         /**
812          * We need to make sure that all the internal GUI components have been added to the
813          * JFrame so that any plugns that get activated during initPlugins(...) method
814          * have access to inject menus
815          */
816         initPlugins(pluginRegistry);
817 
818         mainReceiverSplitPane.setResizeWeight(1.0);
819         addWindowListener(
820             new WindowAdapter() {
821                 public void windowClosing(WindowEvent event) {
822                     exit();
823                 }
824             });
825         preferencesFrame.setTitle("'Application-wide Preferences");
826         preferencesFrame.setIconImage(
827             ((ImageIcon) ChainsawIcons.ICON_PREFERENCES).getImage());
828         preferencesFrame.getContentPane().add(applicationPreferenceModelPanel);
829 
830         preferencesFrame.setSize(750, 520);
831 
832         Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize();
833         preferencesFrame.setLocation(
834             new Point(
835                 (screenDimension.width / 2) - (preferencesFrame.getSize().width / 2),
836                 (screenDimension.height / 2) - (preferencesFrame.getSize().height / 2)));
837 
838         pack();
839 
840         final JPopupMenu tabPopup = new JPopupMenu();
841         final Action hideCurrentTabAction =
842             new AbstractAction("Hide") {
843                 public void actionPerformed(ActionEvent e) {
844                     Component selectedComp = getTabbedPane().getSelectedComponent();
845                     if (selectedComp instanceof LogPanel) {
846                         displayPanel(getCurrentLogPanel().getIdentifier(), false);
847                         tbms.stateChange();
848                     } else {
849                         getTabbedPane().remove(selectedComp);
850                     }
851                 }
852             };
853 
854         final Action hideOtherTabsAction =
855             new AbstractAction("Hide Others") {
856                 public void actionPerformed(ActionEvent e) {
857                     Component selectedComp = getTabbedPane().getSelectedComponent();
858                     String currentName;
859                     if (selectedComp instanceof LogPanel) {
860                         currentName = getCurrentLogPanel().getIdentifier();
861                     } else if (selectedComp instanceof WelcomePanel) {
862                         currentName = ChainsawTabbedPane.WELCOME_TAB;
863                     } else {
864                         currentName = ChainsawTabbedPane.ZEROCONF;
865                     }
866 
867                     int count = getTabbedPane().getTabCount();
868                     int index = 0;
869 
870                     for (int i = 0; i < count; i++) {
871                         String name = getTabbedPane().getTitleAt(index);
872 
873                         if (
874                             getPanelMap().keySet().contains(name)
875                                 && !name.equals(currentName)) {
876                             displayPanel(name, false);
877                             tbms.stateChange();
878                         } else {
879                             index++;
880                         }
881                     }
882                 }
883             };
884 
885         Action showHiddenTabsAction =
886             new AbstractAction("Show All Hidden") {
887                 public void actionPerformed(ActionEvent e) {
888                     for (Object o : getPanels().entrySet()) {
889                         Map.Entry entry = (Map.Entry) o;
890                         Boolean docked = (Boolean) entry.getValue();
891                         if (docked) {
892                             String identifier = (String) entry.getKey();
893                             int count = getTabbedPane().getTabCount();
894                             boolean found = false;
895 
896                             for (int i = 0; i < count; i++) {
897                                 String name = getTabbedPane().getTitleAt(i);
898 
899                                 if (name.equals(identifier)) {
900                                     found = true;
901 
902                                     break;
903                                 }
904                             }
905 
906                             if (!found) {
907                                 displayPanel(identifier, true);
908                                 tbms.stateChange();
909                             }
910                         }
911                     }
912                 }
913             };
914 
915         tabPopup.add(hideCurrentTabAction);
916         tabPopup.add(hideOtherTabsAction);
917         tabPopup.addSeparator();
918         tabPopup.add(showHiddenTabsAction);
919 
920         final PopupListener tabPopupListener = new PopupListener(tabPopup);
921         getTabbedPane().addMouseListener(tabPopupListener);
922 
923         this.handler.addPropertyChangeListener(
924             "dataRate",
925             evt -> {
926                 double dataRate = (Double) evt.getNewValue();
927                 statusBar.setDataRate(dataRate);
928             });
929 
930         getSettingsManager().addSettingsListener(this);
931         getSettingsManager().addSettingsListener(MRUFileListPreferenceSaver.getInstance());
932         getSettingsManager().addSettingsListener(receiversPanel);
933         try {
934             //if an uncaught exception is thrown, allow the UI to continue to load
935             getSettingsManager().loadSettings();
936         } catch (Exception e) {
937             e.printStackTrace();
938         }
939         //app preferences have already been loaded (and configuration url possibly set to blank if being overridden)
940         //but we need a listener so the settings will be saved on exit (added after loadsettings was called)
941         getSettingsManager().addSettingsListener(new ApplicationPreferenceModelSaver(applicationPreferenceModel));
942 
943         setVisible(true);
944 
945         if (applicationPreferenceModel.isReceivers()) {
946             showReceiverPanel();
947         } else {
948             hideReceiverPanel();
949         }
950 
951         removeSplash();
952 
953         synchronized (initializationLock) {
954             isGUIFullyInitialized = true;
955             initializationLock.notifyAll();
956         }
957 
958         if (
959             noReceiversDefined
960                 && applicationPreferenceModel.isShowNoReceiverWarning()) {
961             SwingHelper.invokeOnEDT(this::showReceiverConfigurationPanel);
962         }
963 
964         Container container = tutorialFrame.getContentPane();
965         final JEditorPane tutorialArea = new JEditorPane();
966         tutorialArea.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
967         tutorialArea.setEditable(false);
968         container.setLayout(new BorderLayout());
969 
970         try {
971             tutorialArea.setPage(ChainsawConstants.TUTORIAL_URL);
972             JTextComponentFormatter.applySystemFontAndSize(tutorialArea);
973 
974             container.add(new JScrollPane(tutorialArea), BorderLayout.CENTER);
975         } catch (Exception e) {
976             MessageCenter.getInstance().getLogger().error(
977                 "Error occurred loading the Tutorial", e);
978         }
979 
980         tutorialFrame.setIconImage(new ImageIcon(ChainsawIcons.HELP).getImage());
981         tutorialFrame.setSize(new Dimension(640, 480));
982 
983         final Action startTutorial =
984             new AbstractAction(
985                 "Start Tutorial", new ImageIcon(ChainsawIcons.ICON_RESUME_RECEIVER)) {
986                 public void actionPerformed(ActionEvent e) {
987                     if (
988                         JOptionPane.showConfirmDialog(
989                             null,
990                             "This will start 3 \"Generator\" receivers for use in the Tutorial.  Is that ok?",
991                             "Confirm", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
992                         new Thread(new Tutorial()).start();
993                         putValue("TutorialStarted", Boolean.TRUE);
994                     } else {
995                         putValue("TutorialStarted", Boolean.FALSE);
996                     }
997                 }
998             };
999 
1000         final Action stopTutorial =
1001             new AbstractAction(
1002                 "Stop Tutorial", new ImageIcon(ChainsawIcons.ICON_STOP_RECEIVER)) {
1003                 public void actionPerformed(ActionEvent e) {
1004                     if (
1005                         JOptionPane.showConfirmDialog(
1006                             null,
1007                             "This will stop all of the \"Generator\" receivers used in the Tutorial, but leave any other Receiver untouched.  Is that ok?",
1008                             "Confirm", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
1009                         new Thread(
1010                             () -> {
1011                                 LoggerRepository repo1 = LogManager.getLoggerRepository();
1012                                 if (repo1 instanceof LoggerRepositoryEx) {
1013                                     PluginRegistry pluginRegistry = ((LoggerRepositoryEx) repo1).getPluginRegistry();
1014                                     List list = pluginRegistry.getPlugins(Generator.class);
1015 
1016                                     for (Object aList : list) {
1017                                         Plugin plugin = (Plugin) aList;
1018                                         pluginRegistry.stopPlugin(plugin.getName());
1019                                     }
1020                                 }
1021                             }).start();
1022                         setEnabled(false);
1023                         startTutorial.putValue("TutorialStarted", Boolean.FALSE);
1024                     }
1025                 }
1026             };
1027 
1028         stopTutorial.putValue(
1029             Action.SHORT_DESCRIPTION,
1030             "Removes all of the Tutorials Generator Receivers, leaving all other Receivers untouched");
1031         startTutorial.putValue(
1032             Action.SHORT_DESCRIPTION,
1033             "Begins the Tutorial, starting up some Generator Receivers so you can see Chainsaw in action");
1034         stopTutorial.setEnabled(false);
1035 
1036         final SmallToggleButton startButton = new SmallToggleButton(startTutorial);
1037         PropertyChangeListener pcl =
1038             evt -> {
1039                 stopTutorial.setEnabled(
1040                     startTutorial.getValue("TutorialStarted").equals(Boolean.TRUE));
1041                 startButton.setSelected(stopTutorial.isEnabled());
1042             };
1043 
1044         startTutorial.addPropertyChangeListener(pcl);
1045         stopTutorial.addPropertyChangeListener(pcl);
1046 
1047         pluginRegistry.addPluginListener(
1048             new PluginListener() {
1049                 public void pluginStarted(PluginEvent e) {
1050                 }
1051 
1052                 public void pluginStopped(PluginEvent e) {
1053                     List list = pluginRegistry.getPlugins(Generator.class);
1054 
1055                     if (list.size() == 0) {
1056                         startTutorial.putValue("TutorialStarted", Boolean.FALSE);
1057                     }
1058                 }
1059             });
1060 
1061         final SmallButton stopButton = new SmallButton(stopTutorial);
1062 
1063         final JToolBar tutorialToolbar = new JToolBar();
1064         tutorialToolbar.setFloatable(false);
1065         tutorialToolbar.add(startButton);
1066         tutorialToolbar.add(stopButton);
1067         container.add(tutorialToolbar, BorderLayout.NORTH);
1068         tutorialArea.addHyperlinkListener(
1069             e -> {
1070                 if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
1071                     if (e.getDescription().equals("StartTutorial")) {
1072                         startTutorial.actionPerformed(null);
1073                     } else if (e.getDescription().equals("StopTutorial")) {
1074                         stopTutorial.actionPerformed(null);
1075                     } else {
1076                         try {
1077                             tutorialArea.setPage(e.getURL());
1078                         } catch (IOException e1) {
1079                             MessageCenter.getInstance().getLogger().error(
1080                                 "Failed to change the URL for the Tutorial", e1);
1081                         }
1082                     }
1083                 }
1084             });
1085 
1086         /**
1087          * loads the saved tab settings and if there are hidden tabs,
1088          * hide those tabs out of currently loaded tabs..
1089          */
1090 
1091         if (!getTabbedPane().tabSetting.isWelcome()) {
1092             displayPanel(ChainsawTabbedPane.WELCOME_TAB, false);
1093         }
1094         if (!getTabbedPane().tabSetting.isZeroconf()) {
1095             displayPanel(ChainsawTabbedPane.ZEROCONF, false);
1096         }
1097         tbms.stateChange();
1098 
1099     }
1100 
1101     /**
1102      * Display the log tree pane, using the last known divider location
1103      */
1104     private void showReceiverPanel() {
1105         mainReceiverSplitPane.setDividerSize(dividerSize);
1106         mainReceiverSplitPane.setDividerLocation(lastMainReceiverSplitLocation);
1107         receiversPanel.setVisible(true);
1108         mainReceiverSplitPane.repaint();
1109     }
1110 
1111     /**
1112      * Hide the log tree pane, holding the current divider location for later use
1113      */
1114     private void hideReceiverPanel() {
1115         //subtract one to make sizes match
1116         int currentSize = mainReceiverSplitPane.getWidth() - mainReceiverSplitPane.getDividerSize();
1117         if (mainReceiverSplitPane.getDividerLocation() > -1) {
1118             if (!(((mainReceiverSplitPane.getDividerLocation() + 1) == currentSize)
1119                 || ((mainReceiverSplitPane.getDividerLocation() - 1) == 0))) {
1120                 lastMainReceiverSplitLocation = ((double) mainReceiverSplitPane
1121                     .getDividerLocation() / currentSize);
1122             }
1123         }
1124         mainReceiverSplitPane.setDividerSize(0);
1125         receiversPanel.setVisible(false);
1126         mainReceiverSplitPane.repaint();
1127     }
1128 
1129     private void initSocketConnectionListener() {
1130         final SocketNodeEventListener socketListener =
1131             new SocketNodeEventListener() {
1132                 public void socketOpened(String remoteInfo) {
1133                     statusBar.remoteConnectionReceived(remoteInfo);
1134                 }
1135 
1136                 public void socketClosedEvent(Exception e) {
1137                     MessageCenter.getInstance().getLogger().info(
1138                         "Connection lost! :: " + e.getMessage());
1139                 }
1140             };
1141 
1142         PluginListener pluginListener =
1143             new PluginListener() {
1144                 public void pluginStarted(PluginEvent e) {
1145                     MessageCenter.getInstance().getLogger().info(
1146                         e.getPlugin().getName() + " started!");
1147 
1148                     Method method = getAddListenerMethod(e.getPlugin());
1149 
1150                     if (method != null) {
1151                         try {
1152                             method.invoke(e.getPlugin(), socketListener);
1153                         } catch (Exception ex) {
1154                             MessageCenter.getInstance().getLogger().error(
1155                                 "Failed to add a SocketNodeEventListener", ex);
1156                         }
1157                     }
1158                 }
1159 
1160                 Method getRemoveListenerMethod(Plugin p) {
1161                     try {
1162                         return p.getClass().getMethod(
1163                             "removeSocketNodeEventListener",
1164                             SocketNodeEventListener.class);
1165                     } catch (Exception e) {
1166                         return null;
1167                     }
1168                 }
1169 
1170                 Method getAddListenerMethod(Plugin p) {
1171                     try {
1172                         return p.getClass().getMethod(
1173                             "addSocketNodeEventListener",
1174                             SocketNodeEventListener.class);
1175                     } catch (Exception e) {
1176                         return null;
1177                     }
1178                 }
1179 
1180                 public void pluginStopped(PluginEvent e) {
1181                     Method method = getRemoveListenerMethod(e.getPlugin());
1182 
1183                     if (method != null) {
1184                         try {
1185                             method.invoke(e.getPlugin(), socketListener);
1186                         } catch (Exception ex) {
1187                             MessageCenter.getInstance().getLogger().error(
1188                                 "Failed to remove SocketNodeEventListener", ex);
1189                         }
1190                     }
1191 
1192                     MessageCenter.getInstance().getLogger().info(
1193                         e.getPlugin().getName() + " stopped!");
1194                 }
1195             };
1196 
1197         pluginRegistry.addPluginListener(pluginListener);
1198     }
1199 
1200     private void initPrefModelListeners() {
1201         applicationPreferenceModel.addPropertyChangeListener(
1202             "identifierExpression",
1203             evt -> handler.setIdentifierExpression(evt.getNewValue().toString()));
1204         handler.setIdentifierExpression(applicationPreferenceModel.getIdentifierExpression());
1205 
1206 
1207         applicationPreferenceModel.addPropertyChangeListener(
1208             "toolTipDisplayMillis",
1209             evt -> ToolTipManager.sharedInstance().setDismissDelay(
1210                 (Integer) evt.getNewValue()));
1211         ToolTipManager.sharedInstance().setDismissDelay(
1212             applicationPreferenceModel.getToolTipDisplayMillis());
1213 
1214         applicationPreferenceModel.addPropertyChangeListener(
1215             "responsiveness",
1216             evt -> {
1217                 int value = (Integer) evt.getNewValue();
1218                 handler.setQueueInterval((value * 1000) - 750);
1219             });
1220         handler.setQueueInterval((applicationPreferenceModel.getResponsiveness() * 1000) - 750);
1221 
1222         applicationPreferenceModel.addPropertyChangeListener(
1223             "tabPlacement",
1224             evt -> SwingUtilities.invokeLater(
1225                 () -> {
1226                     int placement = (Integer) evt.getNewValue();
1227 
1228                     switch (placement) {
1229                         case SwingConstants.TOP:
1230                         case SwingConstants.BOTTOM:
1231                             tabbedPane.setTabPlacement(placement);
1232 
1233                             break;
1234 
1235                         default:
1236                             break;
1237                     }
1238                 }));
1239 
1240         applicationPreferenceModel.addPropertyChangeListener(
1241             "statusBar",
1242             evt -> {
1243                 boolean value = (Boolean) evt.getNewValue();
1244                 setStatusBarVisible(value);
1245             });
1246         setStatusBarVisible(applicationPreferenceModel.isStatusBar());
1247 
1248         applicationPreferenceModel.addPropertyChangeListener(
1249             "receivers",
1250             evt -> {
1251                 boolean value = (Boolean) evt.getNewValue();
1252 
1253                 if (value) {
1254                     showReceiverPanel();
1255                 } else {
1256                     hideReceiverPanel();
1257                 }
1258             });
1259 //    if (applicationPreferenceModel.isReceivers()) {
1260 //      showReceiverPanel();
1261 //    } else {
1262 //      hideReceiverPanel();
1263 //    }
1264 
1265 
1266         applicationPreferenceModel.addPropertyChangeListener(
1267             "toolbar",
1268             evt -> {
1269                 boolean value = (Boolean) evt.getNewValue();
1270                 toolbar.setVisible(value);
1271             });
1272         toolbar.setVisible(applicationPreferenceModel.isToolbar());
1273 
1274     }
1275 
1276     /**
1277      * Displays a dialog which will provide options for selecting a configuration
1278      */
1279     private void showReceiverConfigurationPanel() {
1280         SwingUtilities.invokeLater(
1281             () -> {
1282                 final JDialog dialog = new JDialog(LogUI.this, true);
1283                 dialog.setTitle("Load events into Chainsaw");
1284                 dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
1285 
1286                 dialog.setResizable(false);
1287 
1288                 receiverConfigurationPanel.setCompletionActionListener(
1289                     e -> {
1290                         dialog.setVisible(false);
1291 
1292                         if (receiverConfigurationPanel.getModel().isCancelled()) {
1293                             return;
1294                         }
1295                         applicationPreferenceModel.setShowNoReceiverWarning(!receiverConfigurationPanel.isDontWarnMeAgain());
1296                         //remove existing plugins
1297                         List<Plugin> plugins = pluginRegistry.getPlugins();
1298                         for (Object plugin1 : plugins) {
1299                             Plugin plugin = (Plugin) plugin1;
1300                             //don't stop ZeroConfPlugin if it is registered
1301                             if (!plugin.getName().toLowerCase(Locale.ENGLISH).contains("zeroconf")) {
1302                                 pluginRegistry.stopPlugin(plugin.getName());
1303                             }
1304                         }
1305                         URL configURL = null;
1306 
1307                         if (receiverConfigurationPanel.getModel().isNetworkReceiverMode()) {
1308                             int port = receiverConfigurationPanel.getModel().getNetworkReceiverPort();
1309 
1310                             try {
1311                                 Class<? extends Receiver> receiverClass = receiverConfigurationPanel.getModel().getNetworkReceiverClass();
1312                                 Receiver networkReceiver = receiverClass.newInstance();
1313                                 networkReceiver.setName(receiverClass.getSimpleName() + "-" + port);
1314 
1315                                 Method portMethod =
1316                                     networkReceiver.getClass().getMethod(
1317                                         "setPort", int.class);
1318                                 portMethod.invoke(
1319                                     networkReceiver, port);
1320 
1321                                 networkReceiver.setThreshold(Level.TRACE);
1322 
1323                                 pluginRegistry.addPlugin(networkReceiver);
1324                                 networkReceiver.activateOptions();
1325                                 receiversPanel.updateReceiverTreeInDispatchThread();
1326                             } catch (Exception e3) {
1327                                 MessageCenter.getInstance().getLogger().error(
1328                                     "Error creating Receiver", e3);
1329                                 MessageCenter.getInstance().getLogger().info(
1330                                     "An error occurred creating your Receiver");
1331                             }
1332                         } else if (receiverConfigurationPanel.getModel().isLog4jConfig()) {
1333                             File log4jConfigFile = receiverConfigurationPanel.getModel().getLog4jConfigFile();
1334                             if (log4jConfigFile != null) {
1335                                 try {
1336                                     Map<String, Map<String, String>> entries = LogFilePatternLayoutBuilder.getAppenderConfiguration(log4jConfigFile);
1337                                     for (Object o : entries.entrySet()) {
1338                                         try {
1339                                             Map.Entry entry = (Map.Entry) o;
1340                                             String name = (String) entry.getKey();
1341                                             Map values = (Map) entry.getValue();
1342                                             //values: conversion, file
1343                                             String conversionPattern = values.get("conversion").toString();
1344                                             File file = new File(values.get("file").toString());
1345                                             URL fileURL = file.toURI().toURL();
1346                                             String timestampFormat = LogFilePatternLayoutBuilder.getTimeStampFormat(conversionPattern);
1347                                             String receiverPattern = LogFilePatternLayoutBuilder.getLogFormatFromPatternLayout(conversionPattern);
1348                                             VFSLogFilePatternReceiver fileReceiver = new VFSLogFilePatternReceiver();
1349                                             fileReceiver.setName(name);
1350                                             fileReceiver.setAutoReconnect(true);
1351                                             fileReceiver.setContainer(LogUI.this);
1352                                             fileReceiver.setAppendNonMatches(true);
1353                                             fileReceiver.setFileURL(fileURL.toURI().toString());
1354                                             fileReceiver.setTailing(true);
1355                                             fileReceiver.setLogFormat(receiverPattern);
1356                                             fileReceiver.setTimestampFormat(timestampFormat);
1357                                             fileReceiver.setThreshold(Level.TRACE);
1358                                             pluginRegistry.addPlugin(fileReceiver);
1359                                             fileReceiver.activateOptions();
1360                                             receiversPanel.updateReceiverTreeInDispatchThread();
1361                                         } catch (URISyntaxException e1) {
1362                                             e1.printStackTrace();
1363                                         }
1364                                     }
1365                                 } catch (IOException e1) {
1366                                     e1.printStackTrace();
1367                                 }
1368                             }
1369                         } else if (receiverConfigurationPanel.getModel().isLoadConfig()) {
1370                             configURL = receiverConfigurationPanel.getModel().getConfigToLoad();
1371                         } else if (receiverConfigurationPanel.getModel().isLogFileReceiverConfig()) {
1372                             try {
1373                                 URL fileURL = receiverConfigurationPanel.getModel().getLogFileURL();
1374                                 if (fileURL != null) {
1375                                     VFSLogFilePatternReceiver fileReceiver = new VFSLogFilePatternReceiver();
1376                                     fileReceiver.setName(fileURL.getFile());
1377                                     fileReceiver.setAutoReconnect(true);
1378                                     fileReceiver.setContainer(LogUI.this);
1379                                     fileReceiver.setAppendNonMatches(true);
1380                                     fileReceiver.setFileURL(fileURL.toURI().toString());
1381                                     fileReceiver.setTailing(true);
1382                                     if (receiverConfigurationPanel.getModel().isPatternLayoutLogFormat()) {
1383                                         fileReceiver.setLogFormat(LogFilePatternLayoutBuilder.getLogFormatFromPatternLayout(receiverConfigurationPanel.getModel().getLogFormat()));
1384                                     } else {
1385                                         fileReceiver.setLogFormat(receiverConfigurationPanel.getModel().getLogFormat());
1386                                     }
1387                                     fileReceiver.setTimestampFormat(receiverConfigurationPanel.getModel().getLogFormatTimestampFormat());
1388                                     fileReceiver.setThreshold(Level.TRACE);
1389 
1390                                     pluginRegistry.addPlugin(fileReceiver);
1391                                     fileReceiver.activateOptions();
1392                                     receiversPanel.updateReceiverTreeInDispatchThread();
1393                                 }
1394                             } catch (Exception e2) {
1395                                 MessageCenter.getInstance().getLogger().error(
1396                                     "Error creating Receiver", e2);
1397                                 MessageCenter.getInstance().getLogger().info(
1398                                     "An error occurred creating your Receiver");
1399                             }
1400                         }
1401                         if (configURL == null && receiverConfigurationPanel.isDontWarnMeAgain()) {
1402                             //use the saved config file as the config URL if defined
1403                             if (receiverConfigurationPanel.getModel().getSaveConfigFile() != null) {
1404                                 try {
1405                                     configURL = receiverConfigurationPanel.getModel().getSaveConfigFile().toURI().toURL();
1406                                 } catch (MalformedURLException e1) {
1407                                     e1.printStackTrace();
1408                                 }
1409                             } else {
1410                                 //no saved config defined but don't warn me is checked - use default config
1411                                 configURL = receiverConfigurationPanel.getModel().getDefaultConfigFileURL();
1412                             }
1413                         }
1414                         if (configURL != null) {
1415                             MessageCenter.getInstance().getLogger().debug(
1416                                 "Initialiazing Log4j with " + configURL.toExternalForm());
1417                             final URL finalURL = configURL;
1418                             new Thread(
1419                                 () -> {
1420                                     if (receiverConfigurationPanel.isDontWarnMeAgain()) {
1421                                         applicationPreferenceModel.setConfigurationURL(finalURL.toExternalForm());
1422                                     } else {
1423                                         try {
1424                                             if (new File(finalURL.toURI()).exists()) {
1425                                                 loadConfigurationUsingPluginClassLoader(finalURL);
1426                                             }
1427                                         } catch (URISyntaxException e12) {
1428                                             //ignore
1429                                         }
1430                                     }
1431 
1432                                     receiversPanel.updateReceiverTreeInDispatchThread();
1433                                 }).start();
1434                         }
1435                         File saveConfigFile = receiverConfigurationPanel.getModel().getSaveConfigFile();
1436                         if (saveConfigFile != null) {
1437                             ReceiversHelper.getInstance().saveReceiverConfiguration(saveConfigFile);
1438                         }
1439                     });
1440 
1441                 receiverConfigurationPanel.setDialog(dialog);
1442                 dialog.getContentPane().add(receiverConfigurationPanel);
1443 
1444                 dialog.pack();
1445 
1446                 Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
1447                 dialog.setLocation(
1448                     (screenSize.width / 2) - (dialog.getWidth() / 2),
1449                     (screenSize.height / 2) - (dialog.getHeight() / 2));
1450 
1451                 dialog.setVisible(true);
1452             });
1453     }
1454 
1455     /**
1456      * Exits the application, ensuring Settings are saved.
1457      */
1458     public boolean exit() {
1459         getSettingsManager().saveSettings();
1460 
1461         return shutdown();
1462     }
1463 
1464     void addWelcomePanel() {
1465         getTabbedPane().insertTab(
1466             ChainsawTabbedPane.WELCOME_TAB, new ImageIcon(ChainsawIcons.ABOUT), welcomePanel,
1467             "Welcome/Help", 0);
1468         getTabbedPane().setSelectedComponent(welcomePanel);
1469         getPanelMap().put(ChainsawTabbedPane.WELCOME_TAB, welcomePanel);
1470     }
1471 
1472     void removeWelcomePanel() {
1473         EventQueue.invokeLater(() -> {
1474             if (getTabbedPane().containsWelcomePanel()) {
1475                 getTabbedPane().remove(
1476                     getTabbedPane().getComponentAt(getTabbedPane().indexOfTab(ChainsawTabbedPane.WELCOME_TAB)));
1477             }
1478         });
1479     }
1480 
1481     ChainsawStatusBar getStatusBar() {
1482         return statusBar;
1483     }
1484 
1485     public void showApplicationPreferences() {
1486         applicationPreferenceModelPanel.updateModel();
1487         preferencesFrame.setVisible(true);
1488     }
1489 
1490     public void showReceiverConfiguration() {
1491         showReceiverConfigurationPanel();
1492     }
1493 
1494     public void showAboutBox() {
1495         if (aboutBox == null) {
1496             aboutBox = new ChainsawAbout(this);
1497         }
1498 
1499         aboutBox.setVisible(true);
1500     }
1501 
1502     Map getPanels() {
1503         Map m = new HashMap();
1504         Set<Map.Entry<String, Component>> panelSet = getPanelMap().entrySet();
1505 
1506         for (Object aPanelSet : panelSet) {
1507             Map.Entry entry = (Map.Entry) aPanelSet;
1508             Object o = entry.getValue();
1509             boolean valueToSend;
1510             valueToSend = !(o instanceof LogPanel) || ((DockablePanel) entry.getValue()).isDocked();
1511             m.put(entry.getKey(), valueToSend);
1512         }
1513 
1514         return m;
1515     }
1516 
1517     void displayPanel(String panelName, boolean display) {
1518         Component p = getPanelMap().get(panelName);
1519 
1520         int index = getTabbedPane().indexOfTab(panelName);
1521 
1522         if ((index == -1) && display) {
1523             getTabbedPane().addTab(panelName, p);
1524         }
1525 
1526         if ((index > -1) && !display) {
1527             getTabbedPane().removeTabAt(index);
1528         }
1529     }
1530 
1531 
1532     /**
1533      * Shutsdown by ensuring the Appender gets a chance to close.
1534      */
1535     public boolean shutdown() {
1536         if (getApplicationPreferenceModel().isConfirmExit()) {
1537             if (
1538                 JOptionPane.showConfirmDialog(
1539                     LogUI.this, "Are you sure you want to exit Chainsaw?",
1540                     "Confirm Exit", JOptionPane.YES_NO_OPTION,
1541                     JOptionPane.INFORMATION_MESSAGE) != JOptionPane.YES_OPTION) {
1542                 return false;
1543             }
1544 
1545         }
1546 
1547         final JWindow progressWindow = new JWindow();
1548         final ProgressPanel panel = new ProgressPanel(1, 3, "Shutting down");
1549         progressWindow.getContentPane().add(panel);
1550         progressWindow.pack();
1551 
1552         Point p = new Point(getLocation());
1553         p.move((int) getSize().getWidth() >> 1, (int) getSize().getHeight() >> 1);
1554         progressWindow.setLocation(p);
1555         progressWindow.setVisible(true);
1556 
1557         Runnable runnable =
1558             () -> {
1559                 try {
1560                     int progress = 1;
1561                     final int delay = 25;
1562 
1563                     handler.close();
1564                     panel.setProgress(progress++);
1565 
1566                     Thread.sleep(delay);
1567 
1568                     pluginRegistry.stopAllPlugins();
1569                     panel.setProgress(progress++);
1570 
1571                     Thread.sleep(delay);
1572 
1573                     panel.setProgress(progress++);
1574                     Thread.sleep(delay);
1575                 } catch (Exception e) {
1576                     e.printStackTrace();
1577                 }
1578 
1579                 fireShutdownEvent();
1580                 performShutdownAction();
1581                 progressWindow.setVisible(false);
1582             };
1583 
1584         if (OSXIntegration.IS_OSX) {
1585             /**
1586              * or OSX we do it in the current thread because otherwise returning
1587              * will exit the process before it's had a chance to save things
1588              *
1589              */
1590             runnable.run();
1591         } else {
1592             new Thread(runnable).start();
1593         }
1594         return true;
1595     }
1596 
1597     /**
1598      * Ensures all the registered ShutdownListeners are notified.
1599      */
1600     private void fireShutdownEvent() {
1601         ShutdownListener[] listeners =
1602             shutdownListenerList.getListeners(
1603                 ShutdownListener.class);
1604 
1605         for (ShutdownListener listener : listeners) {
1606             listener.shuttingDown();
1607         }
1608     }
1609 
1610     /**
1611      * Configures LogUI's with an action to execute when the user requests to
1612      * exit the application, the default action is to exit the VM. This Action is
1613      * called AFTER all the ShutdownListeners have been notified
1614      *
1615      * @param shutdownAction
1616      */
1617     public final void setShutdownAction(Action shutdownAction) {
1618         this.shutdownAction = shutdownAction;
1619     }
1620 
1621     /**
1622      * Using the current thread, calls the registed Shutdown action's
1623      * actionPerformed(...) method.
1624      */
1625     private void performShutdownAction() {
1626         MessageCenter.getInstance().getLogger().debug(
1627             "Calling the shutdown Action. Goodbye!");
1628 
1629         shutdownAction.actionPerformed(
1630             new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "Shutting Down"));
1631     }
1632 
1633     /**
1634      * Returns the currently selected LogPanel, if there is one, otherwise null
1635      *
1636      * @return current log panel
1637      */
1638     LogPanel getCurrentLogPanel() {
1639         Component selectedTab = getTabbedPane().getSelectedComponent();
1640 
1641         if (selectedTab instanceof LogPanel) {
1642             return (LogPanel) selectedTab;
1643         }
1644 
1645         return null;
1646     }
1647 
1648     /**
1649      * @param visible
1650      */
1651     private void setStatusBarVisible(final boolean visible) {
1652         MessageCenter.getInstance().getLogger().debug(
1653             "Setting StatusBar to " + visible);
1654         SwingUtilities.invokeLater(
1655             () -> statusBar.setVisible(visible));
1656     }
1657 
1658     boolean isStatusBarVisible() {
1659         return statusBar.isVisible();
1660     }
1661 
1662     /**
1663      * DOCUMENT ME!
1664      *
1665      * @return DOCUMENT ME!
1666      */
1667     public String getActiveTabName() {
1668         int index = getTabbedPane().getSelectedIndex();
1669 
1670         if (index == -1) {
1671             return null;
1672         } else {
1673             return getTabbedPane().getTitleAt(index);
1674         }
1675     }
1676 
1677     /**
1678      * Causes the Welcome Panel to become visible, and shows the URL specified as
1679      * it's contents
1680      *
1681      * @param url for content to show
1682      */
1683     public void showHelp(URL url) {
1684         ensureWelcomePanelVisible();
1685         //    TODO ensure the Welcome Panel is the selected tab
1686         getWelcomePanel().setURL(url);
1687     }
1688 
1689     /**
1690      * DOCUMENT ME!
1691      *
1692      * @return welcome panel
1693      */
1694     private WelcomePanel getWelcomePanel() {
1695         return welcomePanel;
1696     }
1697 
1698     /**
1699      * DOCUMENT ME!
1700      *
1701      * @return log tree panel visible flag
1702      */
1703     public boolean isLogTreePanelVisible() {
1704         return getCurrentLogPanel() != null && getCurrentLogPanel().isLogTreeVisible();
1705 
1706     }
1707 
1708     /**
1709      * DOCUMENT ME!
1710      *
1711      * @return DOCUMENT ME!
1712      */
1713     public Map<String, Component> getPanelMap() {
1714         return panelMap;
1715     }
1716 
1717     //  public Map getLevelMap() {
1718     //    return levelMap;
1719     //  }
1720 
1721     /**
1722      * DOCUMENT ME!
1723      *
1724      * @return DOCUMENT ME!
1725      */
1726     public SettingsManager getSettingsManager() {
1727         return sm;
1728     }
1729 
1730     /**
1731      * DOCUMENT ME!
1732      *
1733      * @return DOCUMENT ME!
1734      */
1735     public List<String> getFilterableColumns() {
1736         return filterableColumns;
1737     }
1738 
1739     /**
1740      * DOCUMENT ME!
1741      *
1742      * @param tbms DOCUMENT ME!
1743      */
1744     public void setToolBarAndMenus(ChainsawToolBarAndMenus tbms) {
1745         this.tbms = tbms;
1746     }
1747 
1748     /**
1749      * DOCUMENT ME!
1750      *
1751      * @return DOCUMENT ME!
1752      */
1753     public ChainsawToolBarAndMenus getToolBarAndMenus() {
1754         return tbms;
1755     }
1756 
1757     /**
1758      * DOCUMENT ME!
1759      *
1760      * @return DOCUMENT ME!
1761      */
1762     public Map getTableMap() {
1763         return tableMap;
1764     }
1765 
1766     /**
1767      * DOCUMENT ME!
1768      *
1769      * @return DOCUMENT ME!
1770      */
1771     public Map getTableModelMap() {
1772         return tableModelMap;
1773     }
1774 
1775     /**
1776      * DOCUMENT ME!
1777      *
1778      * @param tabbedPane DOCUMENT ME!
1779      */
1780     public void setTabbedPane(ChainsawTabbedPane tabbedPane) {
1781         this.tabbedPane = tabbedPane;
1782     }
1783 
1784     /**
1785      * DOCUMENT ME!
1786      *
1787      * @return DOCUMENT ME!
1788      */
1789     public ChainsawTabbedPane getTabbedPane() {
1790         return tabbedPane;
1791     }
1792 
1793     /**
1794      * @return Returns the applicationPreferenceModel.
1795      */
1796     public final ApplicationPreferenceModel getApplicationPreferenceModel() {
1797         return applicationPreferenceModel;
1798     }
1799 
1800     /**
1801      * DOCUMENT ME!
1802      */
1803     public void setupTutorial() {
1804         SwingUtilities.invokeLater(
1805             () -> {
1806                 Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
1807                 setLocation(0, getLocation().y);
1808 
1809                 double chainsawwidth = 0.7;
1810                 double tutorialwidth = 1 - chainsawwidth;
1811                 setSize((int) (screen.width * chainsawwidth), getSize().height);
1812                 invalidate();
1813                 validate();
1814 
1815                 Dimension size = getSize();
1816                 Point loc = getLocation();
1817                 tutorialFrame.setSize(
1818                     (int) (screen.width * tutorialwidth), size.height);
1819                 tutorialFrame.setLocation(loc.x + size.width, loc.y);
1820                 tutorialFrame.setVisible(true);
1821             });
1822     }
1823 
1824     private void buildLogPanel(
1825         boolean customExpression, final String ident, final List<LoggingEvent> events)
1826         throws IllegalArgumentException {
1827         final LogPanel thisPanel = new LogPanel(getStatusBar(), ident, cyclicBufferSize, allColorizers, applicationPreferenceModel);
1828 
1829         getSettingsManager().addSettingsListener(thisPanel);
1830         getSettingsManager().configure(thisPanel);
1831 
1832 
1833         /**
1834          * Now add the panel as a batch listener so it can handle it's own
1835          * batchs
1836          */
1837         if (customExpression) {
1838             handler.addCustomEventBatchListener(ident, thisPanel);
1839         } else {
1840             identifierPanels.add(thisPanel);
1841             handler.addEventBatchListener(thisPanel);
1842         }
1843 
1844         TabIconHandler iconHandler = new TabIconHandler(ident);
1845         thisPanel.addEventCountListener(iconHandler);
1846 
1847 
1848         tabbedPane.addChangeListener(iconHandler);
1849 
1850         PropertyChangeListener toolbarMenuUpdateListener =
1851             evt -> tbms.stateChange();
1852 
1853         thisPanel.addPropertyChangeListener(toolbarMenuUpdateListener);
1854         thisPanel.addPreferencePropertyChangeListener(toolbarMenuUpdateListener);
1855 
1856         thisPanel.addPropertyChangeListener(
1857             "docked",
1858             evt -> {
1859                 LogPanel logPanel = (LogPanel) evt.getSource();
1860 
1861                 if (logPanel.isDocked()) {
1862                     getPanelMap().put(logPanel.getIdentifier(), logPanel);
1863                     getTabbedPane().addANewTab(
1864                         logPanel.getIdentifier(), logPanel, null);
1865                     getTabbedPane().setSelectedTab(getTabbedPane().indexOfTab(logPanel.getIdentifier()));
1866                 } else {
1867                     getTabbedPane().remove(logPanel);
1868                 }
1869             });
1870 
1871         logger.debug("adding logpanel to tabbed pane: " + ident);
1872 
1873         //NOTE: tab addition is a very fragile process - if you modify this code,
1874         //verify the frames in the individual log panels initialize to their
1875         //correct sizes
1876         getTabbedPane().add(ident, thisPanel);
1877         getPanelMap().put(ident, thisPanel);
1878 
1879         /**
1880          * Let the new LogPanel receive this batch
1881          */
1882 
1883         SwingUtilities.invokeLater(
1884             () -> {
1885                 getTabbedPane().addANewTab(
1886                     ident, thisPanel, new ImageIcon(ChainsawIcons.ANIM_RADIO_TOWER));
1887                 thisPanel.layoutComponents();
1888                 thisPanel.receiveEventBatch(ident, events);
1889                 if (!getTabbedPane().tabSetting.isChainsawLog()) {
1890                     displayPanel("chainsaw-log", false);
1891                 }
1892             });
1893 
1894         String msg = "added tab " + ident;
1895         MessageCenter.getInstance().getLogger().debug(msg);
1896     }
1897 
1898 
1899     public void createCustomExpressionLogPanel(String ident) {
1900         //collect events matching the rule from all of the tabs
1901         try {
1902             List<LoggingEvent> list = new ArrayList<>();
1903             Rule rule = ExpressionRule.getRule(ident);
1904 
1905             for (Object identifierPanel : identifierPanels) {
1906                 LogPanel panel = (LogPanel) identifierPanel;
1907 
1908                 for (Object o : panel.getMatchingEvents(rule)) {
1909                     LoggingEventWrapper e = (LoggingEventWrapper) o;
1910                     list.add(e.getLoggingEvent());
1911                 }
1912             }
1913 
1914             buildLogPanel(true, ident, list);
1915         } catch (IllegalArgumentException iae) {
1916             MessageCenter.getInstance().getLogger().info(
1917                 "Unable to add tab using expression: " + ident + ", reason: "
1918                     + iae.getMessage());
1919         }
1920     }
1921 
1922     /**
1923      * Loads the log4j configuration file specified by the url, using
1924      * the PluginClassLoader instance as a TCCL, but only replacing it temporarily, with the original
1925      * TCCL being restored in a finally block to ensure consitency.
1926      *
1927      * @param url
1928      */
1929     private void loadConfigurationUsingPluginClassLoader(final URL url) {
1930         ClassLoader classLoader = PluginClassLoaderFactory.getInstance().getClassLoader();
1931         ClassLoader previousTCCL = Thread.currentThread().getContextClassLoader();
1932 
1933         if (url != null) {
1934             try {
1935                 // we temporarily swap the TCCL so that plugins can find resources
1936                 Thread.currentThread().setContextClassLoader(classLoader);
1937                 try {
1938                     DOMConfigurator.configure(url);
1939                 } catch (Exception e) {
1940                     logger.warn("Unable to load configuration URL: " + url, e);
1941                 }
1942             } finally {
1943                 // now switch it back...
1944                 Thread.currentThread().setContextClassLoader(previousTCCL);
1945             }
1946         }
1947         ensureChainsawAppenderHandlerAdded();
1948     }
1949 
1950     private static void loadLookAndFeelUsingPluginClassLoader(String lookAndFeelClassName) {
1951         ClassLoader classLoader = PluginClassLoaderFactory.getInstance().getClassLoader();
1952         ClassLoader previousTCCL = Thread.currentThread().getContextClassLoader();
1953         try {
1954             // we temporarily swap the TCCL so that plugins can find resources
1955             Thread.currentThread().setContextClassLoader(classLoader);
1956             UIManager.setLookAndFeel(lookAndFeelClassName);
1957             UIManager.getLookAndFeelDefaults().put("ClassLoader", classLoader);
1958         } catch (Exception e) {
1959             e.printStackTrace();
1960         } finally {
1961             // now switch it back...
1962             Thread.currentThread().setContextClassLoader(previousTCCL);
1963         }
1964     }
1965 
1966     /**
1967      * Makes sure that the LoggerRepository has the ChainsawAppenderHandler
1968      * added to the root logger so Chainsaw can receive all the events.
1969      */
1970     private void ensureChainsawAppenderHandlerAdded() {
1971         if (!LogManager.getLoggerRepository().getRootLogger().isAttached(handler)) {
1972             LogManager.getLoggerRepository().getRootLogger().addAppender(handler);
1973         }
1974     }
1975 
1976     /**
1977      * This class handles the recption of the Event batches and creates new
1978      * LogPanels if the identifier is not in use otherwise it ignores the event
1979      * batch.
1980      *
1981      * @author Paul Smith
1982      * &lt;psmith@apache.org&gt;
1983      */
1984     private class NewTabEventBatchReceiver implements EventBatchListener {
1985         /**
1986          * DOCUMENT ME!
1987          *
1988          * @param ident
1989          * @param events
1990          */
1991         public void receiveEventBatch(
1992             final String ident, final List<LoggingEvent> events) {
1993             if (events.size() == 0) {
1994                 return;
1995             }
1996 
1997             if (!isGUIFullyInitialized) {
1998                 synchronized (initializationLock) {
1999                     while (!isGUIFullyInitialized) {
2000                         System.out.println(
2001                             "Wanting to add a row, but GUI not initialized, waiting...");
2002 
2003                         /**
2004                          * Lets wait 1 seconds and recheck.
2005                          */
2006                         try {
2007                             initializationLock.wait(1000);
2008                             logger.debug("waiting for initialization to complete");
2009                         } catch (InterruptedException e) {
2010                         }
2011                     }
2012                     logger.debug("out of system initialization wait loop");
2013                 }
2014             }
2015 
2016             if (!getPanelMap().containsKey(ident)) {
2017                 logger.debug("panel " + ident + " does not exist - creating");
2018                 try {
2019                     buildLogPanel(false, ident, events);
2020                 } catch (IllegalArgumentException iae) {
2021                     logger.error("error creating log panel", iae);
2022                     //should not happen - not a custom expression panel
2023                 }
2024             }
2025         }
2026 
2027         /*
2028          * (non-Javadoc)
2029          *
2030          * @see org.apache.log4j.chainsaw.EventBatchListener#getInterestedIdentifier()
2031          */
2032 
2033         /**
2034          * DOCUMENT ME!
2035          *
2036          * @return DOCUMENT ME!
2037          */
2038         public String getInterestedIdentifier() {
2039             // we are interested in all batches so we can detect new identifiers
2040             return null;
2041         }
2042     }
2043 
2044     private class TabIconHandler implements EventCountListener, ChangeListener {
2045         //the tabIconHandler is associated with a new tab, and a new tab always
2046         //shows the 'new events' icon
2047         private boolean newEvents = true;
2048         private boolean seenEvents = false;
2049         private final String ident;
2050         ImageIcon NEW_EVENTS = new ImageIcon(ChainsawIcons.ANIM_RADIO_TOWER);
2051         ImageIcon HAS_EVENTS = new ImageIcon(ChainsawIcons.INFO);
2052         Icon SELECTED = LineIconFactory.createBlankIcon();
2053 
2054         public TabIconHandler(String identifier) {
2055             ident = identifier;
2056 
2057             new Thread(
2058                 () -> {
2059                     while (true) {
2060                         //if this tab is active, remove the icon
2061                         //don't process undocked tabs
2062                         if (getTabbedPane().indexOfTab(ident) > -1 &&
2063                             getTabbedPane().getSelectedIndex() == getTabbedPane()
2064                                 .indexOfTab(ident)) {
2065                             getTabbedPane().setIconAt(
2066                                 getTabbedPane().indexOfTab(ident), SELECTED);
2067                             newEvents = false;
2068                             seenEvents = true;
2069                         } else if (getTabbedPane().indexOfTab(ident) > -1) {
2070                             if (newEvents) {
2071                                 getTabbedPane().setIconAt(
2072                                     getTabbedPane().indexOfTab(ident), NEW_EVENTS);
2073                                 newEvents = false;
2074                                 seenEvents = false;
2075                             } else if (!seenEvents) {
2076                                 getTabbedPane().setIconAt(
2077                                     getTabbedPane().indexOfTab(ident), HAS_EVENTS);
2078                             }
2079                         }
2080 
2081                         try {
2082                             Thread.sleep(handler.getQueueInterval() + 1000);
2083                         } catch (InterruptedException ie) {
2084                         }
2085                     }
2086                 }).start();
2087         }
2088 
2089         /**
2090          * DOCUMENT ME!
2091          *
2092          * @param currentCount DOCUMENT ME!
2093          * @param totalCount   DOCUMENT ME!
2094          */
2095         public void eventCountChanged(int currentCount, int totalCount) {
2096             newEvents = true;
2097         }
2098 
2099         public void stateChanged(ChangeEvent event) {
2100             if (
2101                 getTabbedPane().indexOfTab(ident) > -1 && getTabbedPane().indexOfTab(ident) == getTabbedPane().getSelectedIndex()) {
2102                 getTabbedPane().setIconAt(getTabbedPane().indexOfTab(ident), SELECTED);
2103             }
2104         }
2105     }
2106 }