/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.log4j.chainsaw;

import org.apache.log4j.*;
import org.apache.log4j.chainsaw.color.RuleColorizer;
import org.apache.log4j.chainsaw.dnd.FileDnDTarget;
import org.apache.log4j.chainsaw.help.HelpManager;
import org.apache.log4j.chainsaw.help.Tutorial;
import org.apache.log4j.chainsaw.helper.SwingHelper;
import org.apache.log4j.chainsaw.icons.ChainsawIcons;
import org.apache.log4j.chainsaw.icons.LineIconFactory;
import org.apache.log4j.chainsaw.messages.MessageCenter;
import org.apache.log4j.chainsaw.osx.OSXIntegration;
import org.apache.log4j.chainsaw.plugins.PluginClassLoaderFactory;
import org.apache.log4j.chainsaw.prefs.*;
import org.apache.log4j.chainsaw.receivers.ReceiversHelper;
import org.apache.log4j.chainsaw.receivers.ReceiversPanel;
import org.apache.log4j.chainsaw.vfs.VFSLogFilePatternReceiver;
import org.apache.log4j.net.SocketNodeEventListener;
import org.apache.log4j.plugins.*;
import org.apache.log4j.rewrite.PropertyRewritePolicy;
import org.apache.log4j.rewrite.RewriteAppender;
import org.apache.log4j.rule.ExpressionRule;
import org.apache.log4j.rule.Rule;
import org.apache.log4j.spi.Decoder;
import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.spi.LoggerRepositoryEx;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.xml.DOMConfigurator;
import org.apache.log4j.xml.XMLDecoder;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;
import javax.swing.event.HyperlinkEvent;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.*;
import java.util.*;
import java.util.List;


/**
 * The main entry point for Chainsaw, this class represents the first frame
 * that is used to display a Welcome panel, and any other panels that are
 * generated because Logging Events are streamed via a Receiver, or other
 * mechanism.
 * <p>
 * NOTE: Some of Chainsaw's application initialization should be performed prior
 * to activating receivers and the logging framework used to perform self-logging.
 * <p>
 * DELAY as much as possible the logging framework initialization process,
 * currently initialized by the creation of a ChainsawAppenderHandler.
 *
 * @author Scott Deboy &lt;sdeboy@apache.org&gt;
 * @author Paul Smith  &lt;psmith@apache.org&gt;
 */
public class LogUI extends JFrame implements ChainsawViewer, SettingsListener {
    private static final String MAIN_WINDOW_HEIGHT = "main.window.height";
    private static final String MAIN_WINDOW_WIDTH = "main.window.width";
    private static final String MAIN_WINDOW_Y = "main.window.y";
    private static final String MAIN_WINDOW_X = "main.window.x";
    private static ChainsawSplash splash;
    private static final double DEFAULT_MAIN_RECEIVER_SPLIT_LOCATION = 0.85d;
    private final JFrame preferencesFrame = new JFrame();
    private boolean noReceiversDefined;
    private ReceiversPanel receiversPanel;
    private ChainsawTabbedPane tabbedPane;
    private JToolBar toolbar;
    private ChainsawStatusBar statusBar;
    private ApplicationPreferenceModel applicationPreferenceModel;
    private ApplicationPreferenceModelPanel applicationPreferenceModelPanel;
    private final Map tableModelMap = new HashMap();
    private final Map tableMap = new HashMap();
    private final List<String> filterableColumns = new ArrayList<>();
    private final Map<String, Component> panelMap = new HashMap<>();
    ChainsawAppenderHandler handler;
    private ChainsawToolBarAndMenus tbms;
    private ChainsawAbout aboutBox;
    private final SettingsManager sm = SettingsManager.getInstance();
    private final JFrame tutorialFrame = new JFrame("Chainsaw Tutorial");
    private JSplitPane mainReceiverSplitPane;
    private double lastMainReceiverSplitLocation = DEFAULT_MAIN_RECEIVER_SPLIT_LOCATION;
    private final List<LogPanel> identifierPanels = new ArrayList<>();
    private int dividerSize;
    private int cyclicBufferSize;
    private static Logger logger;
    private static String configurationURLAppArg;

    /**
     * Set to true, if and only if the GUI has completed it's full
     * initialization. Any logging events that come in must wait until this is
     * true, and if it is false, should wait on the initializationLock object
     * until notified.
     */
    private boolean isGUIFullyInitialized = false;
    private final Object initializationLock = new Object();

    /**
     * The shutdownAction is called when the user requests to exit Chainsaw, and
     * by default this exits the VM, but a developer may replace this action with
     * something that better suits their needs
     */
    private Action shutdownAction = null;

    /**
     * Clients can register a ShutdownListener to be notified when the user has
     * requested Chainsaw to exit.
     */
    private EventListenerList shutdownListenerList = new EventListenerList();
    private WelcomePanel welcomePanel;

    private static final Object repositorySelectorGuard = new Object();
    private static final LoggerRepositoryExImpl repositoryExImpl = new LoggerRepositoryExImpl(LogManager.getLoggerRepository());

    private PluginRegistry pluginRegistry;
    //map of tab names to rulecolorizers
    private Map<String, RuleColorizer> allColorizers = new HashMap<>();
    private ReceiverConfigurationPanel receiverConfigurationPanel = new ReceiverConfigurationPanel();

    /**
     * Constructor which builds up all the visual elements of the frame including
     * the Menu bar
     */
    public LogUI() {
        super("Chainsaw");
        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);

        if (ChainsawIcons.WINDOW_ICON != null) {
            setIconImage(new ImageIcon(ChainsawIcons.WINDOW_ICON).getImage());
        }
    }

    private static final void showSplash(Frame owner) {
        splash = new ChainsawSplash(owner);
        SwingHelper.centerOnScreen(splash);
        splash.setVisible(true);
    }

    private static final void removeSplash() {
        if (splash != null) {
            splash.setVisible(false);
            splash.dispose();
        }
    }

    /**
     * Registers a ShutdownListener with this calss so that it can be notified
     * when the user has requested that Chainsaw exit.
     *
     * @param l
     */
    public void addShutdownListener(ShutdownListener l) {
        shutdownListenerList.add(ShutdownListener.class, l);
    }

    /**
     * Removes the registered ShutdownListener so that the listener will not be
     * notified on a shutdown.
     *
     * @param l
     */
    public void removeShutdownListener(ShutdownListener l) {
        shutdownListenerList.remove(ShutdownListener.class, l);
    }

    /**
     * Starts Chainsaw by attaching a new instance to the Log4J main root Logger
     * via a ChainsawAppender, and activates itself
     *
     * @param args
     */
    public static void main(String[] args) {
        if (args.length > 0) {
            configurationURLAppArg = args[0];
        }

        if (OSXIntegration.IS_OSX) {
            System.setProperty("apple.laf.useScreenMenuBar", "true");
        }


        LogManager.setRepositorySelector(() -> repositoryExImpl, repositorySelectorGuard);

        final ApplicationPreferenceModel model = new ApplicationPreferenceModel();

        SettingsManager.getInstance().configure(new ApplicationPreferenceModelSaver(model));
        //if a configuration URL param was provided, set the configuration URL field to null
        if (configurationURLAppArg != null) {
            model.setBypassConfigurationURL(configurationURLAppArg);
        }

        EventQueue.invokeLater(() -> {
            String lookAndFeelClassName = model.getLookAndFeelClassName();
            if (lookAndFeelClassName == null || lookAndFeelClassName.trim().equals("")) {
                String osName = System.getProperty("os.name");
                if (osName.toLowerCase(Locale.ENGLISH).startsWith("mac")) {
                    //no need to assign look and feel
                } else if (osName.toLowerCase(Locale.ENGLISH).startsWith("windows")) {
                    lookAndFeelClassName = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";
                    model.setLookAndFeelClassName(lookAndFeelClassName);
                } else if (osName.toLowerCase(Locale.ENGLISH).startsWith("linux")) {
                    lookAndFeelClassName = "com.sun.java.swing.plaf.gtk.GTKLookAndFeel";
                    model.setLookAndFeelClassName(lookAndFeelClassName);
                }
            }

            if (lookAndFeelClassName != null && !(lookAndFeelClassName.trim().equals(""))) {
                loadLookAndFeelUsingPluginClassLoader(lookAndFeelClassName);
            }
            createChainsawGUI(model, null);
        });
    }

    /**
     * Creates, activates, and then shows the Chainsaw GUI, optionally showing
     * the splash screen, and using the passed shutdown action when the user
     * requests to exit the application (if null, then Chainsaw will exit the vm)
     *
     * @param model
     * @param newShutdownAction DOCUMENT ME!
     */
    public static void createChainsawGUI(
        ApplicationPreferenceModel model, Action newShutdownAction) {

        if (model.isOkToRemoveSecurityManager()) {
            MessageCenter
                .getInstance()
                .addMessage(
                    "User has authorised removal of Java Security Manager via preferences");
            System.setSecurityManager(null);
            // this SHOULD set the Policy/Permission stuff for any
            // code loaded from our custom classloader.  
            // crossing fingers...
            Policy.setPolicy(new Policy() {

                public void refresh() {
                }

                public PermissionCollection getPermissions(CodeSource codesource) {
                    Permissions perms = new Permissions();
                    perms.add(new AllPermission());
                    return (perms);
                }
            });
        }

        final LogUI logUI = new LogUI();
        logUI.applicationPreferenceModel = model;

        if (model.isShowSplash()) {
            showSplash(logUI);
        }
        logUI.cyclicBufferSize = model.getCyclicBufferSize();
        logUI.pluginRegistry = repositoryExImpl.getPluginRegistry();

        logUI.handler = new ChainsawAppenderHandler();
        logUI.handler.addEventBatchListener(logUI.new NewTabEventBatchReceiver());

        /**
         * TODO until we work out how JoranConfigurator might be able to have
         * configurable class loader, if at all.  For now we temporarily replace the
         * TCCL so that Plugins that need access to resources in
         * the Plugins directory can find them (this is particularly
         * important for the Web start version of Chainsaw
         */
        //configuration initialized here
        logUI.ensureChainsawAppenderHandlerAdded();
        logger = LogManager.getLogger(LogUI.class);

        //set hostname, application and group properties which will cause Chainsaw and other apache-generated
        //logging events to route (by default) to a tab named 'chainsaw-log'
        PropertyRewritePolicy policy = new PropertyRewritePolicy();
        policy.setProperties("hostname=chainsaw,application=log,group=chainsaw");

        RewriteAppender rewriteAppender = new RewriteAppender();
        rewriteAppender.setRewritePolicy(policy);

        Enumeration appenders = Logger.getLogger("org.apache").getAllAppenders();
        if (!appenders.hasMoreElements()) {
            appenders = Logger.getRootLogger().getAllAppenders();
        }
        while (appenders.hasMoreElements()) {
            Appender nextAppender = (Appender) appenders.nextElement();
            rewriteAppender.addAppender(nextAppender);
        }
        Logger.getLogger("org.apache").removeAllAppenders();
        Logger.getLogger("org.apache").addAppender(rewriteAppender);
        Logger.getLogger("org.apache").setAdditivity(false);

        //commons-vfs uses httpclient for http filesystem support, route this to the chainsaw-log tab as well
        appenders = Logger.getLogger("httpclient").getAllAppenders();
        if (!appenders.hasMoreElements()) {
            appenders = Logger.getRootLogger().getAllAppenders();
        }
        while (appenders.hasMoreElements()) {
            Appender nextAppender = (Appender) appenders.nextElement();
            rewriteAppender.addAppender(nextAppender);
        }
        Logger.getLogger("httpclient").removeAllAppenders();
        Logger.getLogger("httpclient").addAppender(rewriteAppender);
        Logger.getLogger("httpclient").setAdditivity(false);

        //set the commons.vfs.cache logger to info, since it can contain password information
        Logger.getLogger("org.apache.commons.vfs.cache").setLevel(Level.INFO);

        Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
            e.printStackTrace();
            logger.error("Uncaught exception in thread " + t, e);
        });

        String config = configurationURLAppArg;
        if (config != null) {
            logger.info("Command-line configuration arg provided (overriding auto-configuration URL) - using: " + config);
        } else {
            config = model.getConfigurationURL();
        }

        if (config != null && (!config.trim().equals(""))) {
            config = config.trim();
            try {
                URL configURL = new URL(config);
                logger.info("Using '" + config + "' for auto-configuration");
                logUI.loadConfigurationUsingPluginClassLoader(configURL);
            } catch (MalformedURLException e) {
                logger.error("Initial configuration - failed to convert config string to url", e);
            } catch (IOException e) {
                logger.error("Unable to access auto-configuration URL: " + config);
            }
        }

        //register a listener to load the configuration when it changes (avoid having to restart Chainsaw when applying a new configuration)
        //this doesn't remove receivers from receivers panel, it just triggers DOMConfigurator.configure.
        model.addPropertyChangeListener("configurationURL", evt -> {
            String newConfiguration = evt.getNewValue().toString();
            if (newConfiguration != null && !(newConfiguration.trim().equals(""))) {
                newConfiguration = newConfiguration.trim();
                try {
                    logger.info("loading updated configuration: " + newConfiguration);
                    URL newConfigurationURL = new URL(newConfiguration);
                    File file = new File(newConfigurationURL.toURI());
                    if (file.exists()) {
                        logUI.loadConfigurationUsingPluginClassLoader(newConfigurationURL);
                    } else {
                        logger.info("Updated configuration but file does not exist");
                    }
                } catch (MalformedURLException | URISyntaxException e) {
                    logger.error("Updated configuration - failed to convert config string to URL", e);
                }
            }
        });

        LogManager.getRootLogger().setLevel(Level.TRACE);
        EventQueue.invokeLater(logUI::activateViewer);

        logger.info("SecurityManager is now: " + System.getSecurityManager());

        if (newShutdownAction != null) {
            logUI.setShutdownAction(newShutdownAction);
        } else {
            logUI.setShutdownAction(
                new AbstractAction() {
                    public void actionPerformed(ActionEvent e) {
                        System.exit(0);
                    }
                });
        }
    }

    /**
     * Allow Chainsaw v2 to be ran in-process (configured as a ChainsawAppender)
     * NOTE: Closing Chainsaw will NOT stop the application generating the events.
     *
     * @param appender
     */
    public void activateViewer(ChainsawAppender appender) {

        if (OSXIntegration.IS_OSX) {
            System.setProperty("apple.laf.useScreenMenuBar", "true");
        }

        LogManager.setRepositorySelector(() -> repositoryExImpl, repositorySelectorGuard);

        //if Chainsaw is launched as an appender, ensure the root logger level is TRACE
        LogManager.getRootLogger().setLevel(Level.TRACE);

        final ApplicationPreferenceModel model = new ApplicationPreferenceModel();
        SettingsManager.getInstance().configure(new ApplicationPreferenceModelSaver(model));

        cyclicBufferSize = model.getCyclicBufferSize();

        handler = new ChainsawAppenderHandler(appender);
        handler.addEventBatchListener(new NewTabEventBatchReceiver());

        logger = LogManager.getLogger(LogUI.class);

        setShutdownAction(
            new AbstractAction() {
                public void actionPerformed(ActionEvent e) {
                }
            });

        applicationPreferenceModel = new ApplicationPreferenceModel();

        SettingsManager.getInstance().configure(new ApplicationPreferenceModelSaver(model));

        EventQueue.invokeLater(() -> {
            loadLookAndFeelUsingPluginClassLoader(model.getLookAndFeelClassName());
            createChainsawGUI(model, null);
            getApplicationPreferenceModel().apply(model);
            activateViewer();
        });
    }

    /**
     * Initialises the menu's and toolbars, but does not actually create any of
     * the main panel components.
     */
    private void initGUI() {

        setupHelpSystem();
        statusBar = new ChainsawStatusBar(this);
        setupReceiverPanel();

        setToolBarAndMenus(new ChainsawToolBarAndMenus(this));
        toolbar = getToolBarAndMenus().getToolbar();
        setJMenuBar(getToolBarAndMenus().getMenubar());

        setTabbedPane(new ChainsawTabbedPane());
        getSettingsManager().addSettingsListener(getTabbedPane());
        getSettingsManager().configure(getTabbedPane());

        /**
         * This adds Drag & Drop capability to Chainsaw
         */
        FileDnDTarget dnDTarget = new FileDnDTarget(tabbedPane);
        dnDTarget.addPropertyChangeListener("fileList", evt -> {
            final List fileList = (List) evt.getNewValue();

            Thread thread = new Thread(() -> {
                logger.debug("Loading files: " + fileList);
                for (Object aFileList : fileList) {
                    File file = (File) aFileList;
                    final Decoder decoder = new XMLDecoder();
                    try {
                        getStatusBar().setMessage("Loading " + file.getAbsolutePath() + "...");
                        FileLoadAction.importURL(handler, decoder, file
                            .getName(), file.toURI().toURL());
                    } catch (Exception e) {
                        String errorMsg = "Failed to import a file";
                        logger.error(errorMsg, e);
                        getStatusBar().setMessage(errorMsg);
                    }
                }

            });

            thread.setPriority(Thread.MIN_PRIORITY);
            thread.start();

        });

        applicationPreferenceModelPanel = new ApplicationPreferenceModelPanel(applicationPreferenceModel);

        applicationPreferenceModelPanel.setOkCancelActionListener(
            e -> preferencesFrame.setVisible(false));
        KeyStroke escape = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
        Action closeAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                preferencesFrame.setVisible(false);
            }
        };
        preferencesFrame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escape, "ESCAPE");
        preferencesFrame.getRootPane().
            getActionMap().put("ESCAPE", closeAction);

        OSXIntegration.init(this);

    }

    private void initPlugins(PluginRegistry pluginRegistry) {
        pluginRegistry.addPluginListener(
            new PluginListener() {
                public void pluginStarted(PluginEvent e) {
                    if (e.getPlugin() instanceof JComponent) {
                        JComponent plugin = (JComponent) e.getPlugin();
                        getTabbedPane().addANewTab(plugin.getName(), plugin, null, null);
                        getPanelMap().put(plugin.getName(), plugin);
                    }
                }

                public void pluginStopped(PluginEvent e) {
                    //TODO remove the plugin from the gui
                }
            });

        // TODO this should all be in a config file
//    ChainsawCentral cc = new ChainsawCentral();
//    pluginRegistry.addPlugin(cc);
//    cc.activateOptions();

        try {
            Class<? extends Plugin> pluginClass = Class.forName("org.apache.log4j.chainsaw.zeroconf.ZeroConfPlugin").asSubclass(Plugin.class);
            Plugin plugin = pluginClass.newInstance();
            pluginRegistry.addPlugin(plugin);
            plugin.activateOptions();
            MessageCenter.getInstance().getLogger().info("Looks like ZeroConf is available... WooHoo!");
        } catch (Throwable e) {
            MessageCenter.getInstance().getLogger().error("Doesn't look like ZeroConf is available", e);
        }
    }

    private void setupReceiverPanel() {
        receiversPanel = new ReceiversPanel();
        receiversPanel.addPropertyChangeListener(
            "visible",
            evt -> getApplicationPreferenceModel().setReceivers(
                (Boolean) evt.getNewValue()));
    }

    /**
     * Initialises the Help system and the WelcomePanel
     */
    private void setupHelpSystem() {
        welcomePanel = new WelcomePanel();

        JToolBar tb = welcomePanel.getToolbar();


        tb.add(
            new SmallButton(
                new AbstractAction("Tutorial", new ImageIcon(ChainsawIcons.HELP)) {
                    public void actionPerformed(ActionEvent e) {
                        setupTutorial();
                    }
                }));
        tb.addSeparator();

        final Action exampleConfigAction =
            new AbstractAction("View example Receiver configuration") {
                public void actionPerformed(ActionEvent e) {
                    HelpManager.getInstance().setHelpURL(
                        ChainsawConstants.EXAMPLE_CONFIG_URL);
                }
            };

        exampleConfigAction.putValue(
            Action.SHORT_DESCRIPTION,
            "Displays an example Log4j configuration file with several Receivers defined.");

        JButton exampleButton = new SmallButton(exampleConfigAction);
        tb.add(exampleButton);

        tb.add(Box.createHorizontalGlue());

        /**
         * Setup a listener on the HelpURL property and automatically change the WelcomePages URL
         * to it.
         */
        HelpManager.getInstance().addPropertyChangeListener(
            "helpURL",
            evt -> {
                URL newURL = (URL) evt.getNewValue();

                if (newURL != null) {
                    welcomePanel.setURL(newURL);
                    ensureWelcomePanelVisible();
                }
            });
    }

    private void ensureWelcomePanelVisible() {
        // ensure that the Welcome Panel is made visible
        if (!getTabbedPane().containsWelcomePanel()) {
            addWelcomePanel();
        }
        getTabbedPane().setSelectedComponent(welcomePanel);
    }

    /**
     * Given the load event, configures the size/location of the main window etc
     * etc.
     *
     * @param event DOCUMENT ME!
     */
    public void loadSettings(LoadSettingsEvent event) {
        setLocation(
            event.asInt(LogUI.MAIN_WINDOW_X), event.asInt(LogUI.MAIN_WINDOW_Y));
        int width = event.asInt(LogUI.MAIN_WINDOW_WIDTH);
        int height = event.asInt(LogUI.MAIN_WINDOW_HEIGHT);
        if (width == -1 && height == -1) {
            width = Toolkit.getDefaultToolkit().getScreenSize().width;
            height = Toolkit.getDefaultToolkit().getScreenSize().height;
            setSize(width, height);
            setExtendedState(getExtendedState() | MAXIMIZED_BOTH);
        } else {
            setSize(width, height);
        }

        getToolBarAndMenus().stateChange();
        RuleColorizer colorizer = new RuleColorizer();
        colorizer.loadColorSettings(ChainsawConstants.DEFAULT_COLOR_RULE_NAME);
        allColorizers.put(ChainsawConstants.DEFAULT_COLOR_RULE_NAME, colorizer);
        if (event.getSetting("SavedConfig.logFormat") != null) {
            receiverConfigurationPanel.getModel().setLogFormat(event.getSetting("SavedConfig.logFormat"));
        }
    }

    /**
     * Ensures the location/size of the main window is stored with the settings
     *
     * @param event DOCUMENT ME!
     */
    public void saveSettings(SaveSettingsEvent event) {
        event.saveSetting(LogUI.MAIN_WINDOW_X, (int) getLocation().getX());
        event.saveSetting(LogUI.MAIN_WINDOW_Y, (int) getLocation().getY());

        event.saveSetting(LogUI.MAIN_WINDOW_WIDTH, getWidth());
        event.saveSetting(LogUI.MAIN_WINDOW_HEIGHT, getHeight());
        RuleColorizer colorizer = allColorizers.get(ChainsawConstants.DEFAULT_COLOR_RULE_NAME);
        colorizer.saveColorSettings(ChainsawConstants.DEFAULT_COLOR_RULE_NAME);
        if (receiverConfigurationPanel.getModel().getLogFormat() != null) {
            event.saveSetting("SavedConfig.logFormat", receiverConfigurationPanel.getModel().getLogFormat());
        }
    }

    /**
     * Activates itself as a viewer by configuring Size, and location of itself,
     * and configures the default Tabbed Pane elements with the correct layout,
     * table columns, and sets itself viewable.
     */
    public void activateViewer() {
        LoggerRepository repo = LogManager.getLoggerRepository();
        if (repo instanceof LoggerRepositoryEx) {
            this.pluginRegistry = ((LoggerRepositoryEx) repo).getPluginRegistry();
        }
        initGUI();

        initPrefModelListeners();

        /**
         * We add a simple appender to the MessageCenter logger
         * so that each message is displayed in the Status bar
         */
        MessageCenter.getInstance().getLogger().addAppender(
            new AppenderSkeleton() {
                protected void append(LoggingEvent event) {
                    getStatusBar().setMessage(event.getMessage().toString());
                }

                public void close() {
                }

                public boolean requiresLayout() {
                    return false;
                }
            });


        initSocketConnectionListener();

        if (pluginRegistry.getPlugins(Receiver.class).size() == 0) {
            noReceiversDefined = true;
        }

        getFilterableColumns().add(ChainsawConstants.LEVEL_COL_NAME);
        getFilterableColumns().add(ChainsawConstants.LOGGER_COL_NAME);
        getFilterableColumns().add(ChainsawConstants.THREAD_COL_NAME);
        getFilterableColumns().add(ChainsawConstants.NDC_COL_NAME);
        getFilterableColumns().add(ChainsawConstants.PROPERTIES_COL_NAME);
        getFilterableColumns().add(ChainsawConstants.CLASS_COL_NAME);
        getFilterableColumns().add(ChainsawConstants.METHOD_COL_NAME);
        getFilterableColumns().add(ChainsawConstants.FILE_COL_NAME);
        getFilterableColumns().add(ChainsawConstants.NONE_COL_NAME);

        JPanel panePanel = new JPanel();
        panePanel.setLayout(new BorderLayout(2, 2));

        getContentPane().setLayout(new BorderLayout());

        getTabbedPane().addChangeListener(getToolBarAndMenus());
        getTabbedPane().addChangeListener(e -> {
            LogPanel thisLogPanel = getCurrentLogPanel();
            if (thisLogPanel != null) {
                thisLogPanel.updateStatusBar();
            }
        });

        KeyStroke ksRight =
            KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
        KeyStroke ksLeft =
            KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
        KeyStroke ksGotoLine =
            KeyStroke.getKeyStroke(KeyEvent.VK_G, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());

        getTabbedPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
            ksRight, "MoveRight");
        getTabbedPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
            ksLeft, "MoveLeft");
        getTabbedPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
            ksGotoLine, "GotoLine");

        Action moveRight =
            new AbstractAction() {
                public void actionPerformed(ActionEvent e) {
                    int temp = getTabbedPane().getSelectedIndex();
                    ++temp;

                    if (temp != getTabbedPane().getTabCount()) {
                        getTabbedPane().setSelectedTab(temp);
                    }
                }
            };

        Action moveLeft =
            new AbstractAction() {
                public void actionPerformed(ActionEvent e) {
                    int temp = getTabbedPane().getSelectedIndex();
                    --temp;

                    if (temp > -1) {
                        getTabbedPane().setSelectedTab(temp);
                    }
                }
            };

        Action gotoLine =
            new AbstractAction() {
                public void actionPerformed(ActionEvent e) {
                    String inputLine = JOptionPane.showInputDialog(LogUI.this, "Enter the line number to go:", "Goto Line", JOptionPane.PLAIN_MESSAGE);
                    try {
                        int lineNumber = Integer.parseInt(inputLine);
                        int row = getCurrentLogPanel().setSelectedEvent(lineNumber);
                        if (row == -1) {
                            JOptionPane.showMessageDialog(LogUI.this, "You have entered an invalid line number", "Error", JOptionPane.ERROR_MESSAGE);
                        }
                    } catch (NumberFormatException nfe) {
                        JOptionPane.showMessageDialog(LogUI.this, "You have entered an invalid line number", "Error", JOptionPane.ERROR_MESSAGE);
                    }
                }
            };


        getTabbedPane().getActionMap().put("MoveRight", moveRight);
        getTabbedPane().getActionMap().put("MoveLeft", moveLeft);
        getTabbedPane().getActionMap().put("GotoLine", gotoLine);

        /**
         * We listen for double clicks, and auto-undock currently selected Tab if
         * the mouse event location matches the currently selected tab
         */
        getTabbedPane().addMouseListener(
            new MouseAdapter() {
                public void mouseClicked(MouseEvent e) {
                    super.mouseClicked(e);

                    if (
                        (e.getClickCount() > 1)
                            && ((e.getModifiers() & InputEvent.BUTTON1_MASK) > 0)) {
                        int tabIndex = getTabbedPane().getSelectedIndex();

                        if (
                            (tabIndex != -1)
                                && (tabIndex == getTabbedPane().getSelectedIndex())) {
                            LogPanel logPanel = getCurrentLogPanel();

                            if (logPanel != null) {
                                logPanel.undock();
                            }
                        }
                    }
                }
            });

        panePanel.add(getTabbedPane());
        addWelcomePanel();

        getContentPane().add(toolbar, BorderLayout.NORTH);
        getContentPane().add(statusBar, BorderLayout.SOUTH);

        mainReceiverSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panePanel, receiversPanel);
        dividerSize = mainReceiverSplitPane.getDividerSize();
        mainReceiverSplitPane.setDividerLocation(-1);

        getContentPane().add(mainReceiverSplitPane, BorderLayout.CENTER);

        /**
         * We need to make sure that all the internal GUI components have been added to the
         * JFrame so that any plugns that get activated during initPlugins(...) method
         * have access to inject menus
         */
        initPlugins(pluginRegistry);

        mainReceiverSplitPane.setResizeWeight(1.0);
        addWindowListener(
            new WindowAdapter() {
                public void windowClosing(WindowEvent event) {
                    exit();
                }
            });
        preferencesFrame.setTitle("'Application-wide Preferences");
        preferencesFrame.setIconImage(
            ((ImageIcon) ChainsawIcons.ICON_PREFERENCES).getImage());
        preferencesFrame.getContentPane().add(applicationPreferenceModelPanel);

        preferencesFrame.setSize(750, 520);

        Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize();
        preferencesFrame.setLocation(
            new Point(
                (screenDimension.width / 2) - (preferencesFrame.getSize().width / 2),
                (screenDimension.height / 2) - (preferencesFrame.getSize().height / 2)));

        pack();

        final JPopupMenu tabPopup = new JPopupMenu();
        final Action hideCurrentTabAction =
            new AbstractAction("Hide") {
                public void actionPerformed(ActionEvent e) {
                    Component selectedComp = getTabbedPane().getSelectedComponent();
                    if (selectedComp instanceof LogPanel) {
                        displayPanel(getCurrentLogPanel().getIdentifier(), false);
                        tbms.stateChange();
                    } else {
                        getTabbedPane().remove(selectedComp);
                    }
                }
            };

        final Action hideOtherTabsAction =
            new AbstractAction("Hide Others") {
                public void actionPerformed(ActionEvent e) {
                    Component selectedComp = getTabbedPane().getSelectedComponent();
                    String currentName;
                    if (selectedComp instanceof LogPanel) {
                        currentName = getCurrentLogPanel().getIdentifier();
                    } else if (selectedComp instanceof WelcomePanel) {
                        currentName = ChainsawTabbedPane.WELCOME_TAB;
                    } else {
                        currentName = ChainsawTabbedPane.ZEROCONF;
                    }

                    int count = getTabbedPane().getTabCount();
                    int index = 0;

                    for (int i = 0; i < count; i++) {
                        String name = getTabbedPane().getTitleAt(index);

                        if (
                            getPanelMap().keySet().contains(name)
                                && !name.equals(currentName)) {
                            displayPanel(name, false);
                            tbms.stateChange();
                        } else {
                            index++;
                        }
                    }
                }
            };

        Action showHiddenTabsAction =
            new AbstractAction("Show All Hidden") {
                public void actionPerformed(ActionEvent e) {
                    for (Object o : getPanels().entrySet()) {
                        Map.Entry entry = (Map.Entry) o;
                        Boolean docked = (Boolean) entry.getValue();
                        if (docked) {
                            String identifier = (String) entry.getKey();
                            int count = getTabbedPane().getTabCount();
                            boolean found = false;

                            for (int i = 0; i < count; i++) {
                                String name = getTabbedPane().getTitleAt(i);

                                if (name.equals(identifier)) {
                                    found = true;

                                    break;
                                }
                            }

                            if (!found) {
                                displayPanel(identifier, true);
                                tbms.stateChange();
                            }
                        }
                    }
                }
            };

        tabPopup.add(hideCurrentTabAction);
        tabPopup.add(hideOtherTabsAction);
        tabPopup.addSeparator();
        tabPopup.add(showHiddenTabsAction);

        final PopupListener tabPopupListener = new PopupListener(tabPopup);
        getTabbedPane().addMouseListener(tabPopupListener);

        this.handler.addPropertyChangeListener(
            "dataRate",
            evt -> {
                double dataRate = (Double) evt.getNewValue();
                statusBar.setDataRate(dataRate);
            });

        getSettingsManager().addSettingsListener(this);
        getSettingsManager().addSettingsListener(MRUFileListPreferenceSaver.getInstance());
        getSettingsManager().addSettingsListener(receiversPanel);
        try {
            //if an uncaught exception is thrown, allow the UI to continue to load
            getSettingsManager().loadSettings();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //app preferences have already been loaded (and configuration url possibly set to blank if being overridden)
        //but we need a listener so the settings will be saved on exit (added after loadsettings was called)
        getSettingsManager().addSettingsListener(new ApplicationPreferenceModelSaver(applicationPreferenceModel));

        setVisible(true);

        if (applicationPreferenceModel.isReceivers()) {
            showReceiverPanel();
        } else {
            hideReceiverPanel();
        }

        removeSplash();

        synchronized (initializationLock) {
            isGUIFullyInitialized = true;
            initializationLock.notifyAll();
        }

        if (
            noReceiversDefined
                && applicationPreferenceModel.isShowNoReceiverWarning()) {
            SwingHelper.invokeOnEDT(this::showReceiverConfigurationPanel);
        }

        Container container = tutorialFrame.getContentPane();
        final JEditorPane tutorialArea = new JEditorPane();
        tutorialArea.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
        tutorialArea.setEditable(false);
        container.setLayout(new BorderLayout());

        try {
            tutorialArea.setPage(ChainsawConstants.TUTORIAL_URL);
            JTextComponentFormatter.applySystemFontAndSize(tutorialArea);

            container.add(new JScrollPane(tutorialArea), BorderLayout.CENTER);
        } catch (Exception e) {
            MessageCenter.getInstance().getLogger().error(
                "Error occurred loading the Tutorial", e);
        }

        tutorialFrame.setIconImage(new ImageIcon(ChainsawIcons.HELP).getImage());
        tutorialFrame.setSize(new Dimension(640, 480));

        final Action startTutorial =
            new AbstractAction(
                "Start Tutorial", new ImageIcon(ChainsawIcons.ICON_RESUME_RECEIVER)) {
                public void actionPerformed(ActionEvent e) {
                    if (
                        JOptionPane.showConfirmDialog(
                            null,
                            "This will start 3 \"Generator\" receivers for use in the Tutorial.  Is that ok?",
                            "Confirm", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
                        new Thread(new Tutorial()).start();
                        putValue("TutorialStarted", Boolean.TRUE);
                    } else {
                        putValue("TutorialStarted", Boolean.FALSE);
                    }
                }
            };

        final Action stopTutorial =
            new AbstractAction(
                "Stop Tutorial", new ImageIcon(ChainsawIcons.ICON_STOP_RECEIVER)) {
                public void actionPerformed(ActionEvent e) {
                    if (
                        JOptionPane.showConfirmDialog(
                            null,
                            "This will stop all of the \"Generator\" receivers used in the Tutorial, but leave any other Receiver untouched.  Is that ok?",
                            "Confirm", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
                        new Thread(
                            () -> {
                                LoggerRepository repo1 = LogManager.getLoggerRepository();
                                if (repo1 instanceof LoggerRepositoryEx) {
                                    PluginRegistry pluginRegistry = ((LoggerRepositoryEx) repo1).getPluginRegistry();
                                    List list = pluginRegistry.getPlugins(Generator.class);

                                    for (Object aList : list) {
                                        Plugin plugin = (Plugin) aList;
                                        pluginRegistry.stopPlugin(plugin.getName());
                                    }
                                }
                            }).start();
                        setEnabled(false);
                        startTutorial.putValue("TutorialStarted", Boolean.FALSE);
                    }
                }
            };

        stopTutorial.putValue(
            Action.SHORT_DESCRIPTION,
            "Removes all of the Tutorials Generator Receivers, leaving all other Receivers untouched");
        startTutorial.putValue(
            Action.SHORT_DESCRIPTION,
            "Begins the Tutorial, starting up some Generator Receivers so you can see Chainsaw in action");
        stopTutorial.setEnabled(false);

        final SmallToggleButton startButton = new SmallToggleButton(startTutorial);
        PropertyChangeListener pcl =
            evt -> {
                stopTutorial.setEnabled(
                    startTutorial.getValue("TutorialStarted").equals(Boolean.TRUE));
                startButton.setSelected(stopTutorial.isEnabled());
            };

        startTutorial.addPropertyChangeListener(pcl);
        stopTutorial.addPropertyChangeListener(pcl);

        pluginRegistry.addPluginListener(
            new PluginListener() {
                public void pluginStarted(PluginEvent e) {
                }

                public void pluginStopped(PluginEvent e) {
                    List list = pluginRegistry.getPlugins(Generator.class);

                    if (list.size() == 0) {
                        startTutorial.putValue("TutorialStarted", Boolean.FALSE);
                    }
                }
            });

        final SmallButton stopButton = new SmallButton(stopTutorial);

        final JToolBar tutorialToolbar = new JToolBar();
        tutorialToolbar.setFloatable(false);
        tutorialToolbar.add(startButton);
        tutorialToolbar.add(stopButton);
        container.add(tutorialToolbar, BorderLayout.NORTH);
        tutorialArea.addHyperlinkListener(
            e -> {
                if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
                    if (e.getDescription().equals("StartTutorial")) {
                        startTutorial.actionPerformed(null);
                    } else if (e.getDescription().equals("StopTutorial")) {
                        stopTutorial.actionPerformed(null);
                    } else {
                        try {
                            tutorialArea.setPage(e.getURL());
                        } catch (IOException e1) {
                            MessageCenter.getInstance().getLogger().error(
                                "Failed to change the URL for the Tutorial", e1);
                        }
                    }
                }
            });

        /**
         * loads the saved tab settings and if there are hidden tabs,
         * hide those tabs out of currently loaded tabs..
         */

        if (!getTabbedPane().tabSetting.isWelcome()) {
            displayPanel(ChainsawTabbedPane.WELCOME_TAB, false);
        }
        if (!getTabbedPane().tabSetting.isZeroconf()) {
            displayPanel(ChainsawTabbedPane.ZEROCONF, false);
        }
        tbms.stateChange();

    }

    /**
     * Display the log tree pane, using the last known divider location
     */
    private void showReceiverPanel() {
        mainReceiverSplitPane.setDividerSize(dividerSize);
        mainReceiverSplitPane.setDividerLocation(lastMainReceiverSplitLocation);
        receiversPanel.setVisible(true);
        mainReceiverSplitPane.repaint();
    }

    /**
     * Hide the log tree pane, holding the current divider location for later use
     */
    private void hideReceiverPanel() {
        //subtract one to make sizes match
        int currentSize = mainReceiverSplitPane.getWidth() - mainReceiverSplitPane.getDividerSize();
        if (mainReceiverSplitPane.getDividerLocation() > -1) {
            if (!(((mainReceiverSplitPane.getDividerLocation() + 1) == currentSize)
                || ((mainReceiverSplitPane.getDividerLocation() - 1) == 0))) {
                lastMainReceiverSplitLocation = ((double) mainReceiverSplitPane
                    .getDividerLocation() / currentSize);
            }
        }
        mainReceiverSplitPane.setDividerSize(0);
        receiversPanel.setVisible(false);
        mainReceiverSplitPane.repaint();
    }

    private void initSocketConnectionListener() {
        final SocketNodeEventListener socketListener =
            new SocketNodeEventListener() {
                public void socketOpened(String remoteInfo) {
                    statusBar.remoteConnectionReceived(remoteInfo);
                }

                public void socketClosedEvent(Exception e) {
                    MessageCenter.getInstance().getLogger().info(
                        "Connection lost! :: " + e.getMessage());
                }
            };

        PluginListener pluginListener =
            new PluginListener() {
                public void pluginStarted(PluginEvent e) {
                    MessageCenter.getInstance().getLogger().info(
                        e.getPlugin().getName() + " started!");

                    Method method = getAddListenerMethod(e.getPlugin());

                    if (method != null) {
                        try {
                            method.invoke(e.getPlugin(), socketListener);
                        } catch (Exception ex) {
                            MessageCenter.getInstance().getLogger().error(
                                "Failed to add a SocketNodeEventListener", ex);
                        }
                    }
                }

                Method getRemoveListenerMethod(Plugin p) {
                    try {
                        return p.getClass().getMethod(
                            "removeSocketNodeEventListener",
                            SocketNodeEventListener.class);
                    } catch (Exception e) {
                        return null;
                    }
                }

                Method getAddListenerMethod(Plugin p) {
                    try {
                        return p.getClass().getMethod(
                            "addSocketNodeEventListener",
                            SocketNodeEventListener.class);
                    } catch (Exception e) {
                        return null;
                    }
                }

                public void pluginStopped(PluginEvent e) {
                    Method method = getRemoveListenerMethod(e.getPlugin());

                    if (method != null) {
                        try {
                            method.invoke(e.getPlugin(), socketListener);
                        } catch (Exception ex) {
                            MessageCenter.getInstance().getLogger().error(
                                "Failed to remove SocketNodeEventListener", ex);
                        }
                    }

                    MessageCenter.getInstance().getLogger().info(
                        e.getPlugin().getName() + " stopped!");
                }
            };

        pluginRegistry.addPluginListener(pluginListener);
    }

    private void initPrefModelListeners() {
        applicationPreferenceModel.addPropertyChangeListener(
            "identifierExpression",
            evt -> handler.setIdentifierExpression(evt.getNewValue().toString()));
        handler.setIdentifierExpression(applicationPreferenceModel.getIdentifierExpression());


        applicationPreferenceModel.addPropertyChangeListener(
            "toolTipDisplayMillis",
            evt -> ToolTipManager.sharedInstance().setDismissDelay(
                (Integer) evt.getNewValue()));
        ToolTipManager.sharedInstance().setDismissDelay(
            applicationPreferenceModel.getToolTipDisplayMillis());

        applicationPreferenceModel.addPropertyChangeListener(
            "responsiveness",
            evt -> {
                int value = (Integer) evt.getNewValue();
                handler.setQueueInterval((value * 1000) - 750);
            });
        handler.setQueueInterval((applicationPreferenceModel.getResponsiveness() * 1000) - 750);

        applicationPreferenceModel.addPropertyChangeListener(
            "tabPlacement",
            evt -> SwingUtilities.invokeLater(
                () -> {
                    int placement = (Integer) evt.getNewValue();

                    switch (placement) {
                        case SwingConstants.TOP:
                        case SwingConstants.BOTTOM:
                            tabbedPane.setTabPlacement(placement);

                            break;

                        default:
                            break;
                    }
                }));

        applicationPreferenceModel.addPropertyChangeListener(
            "statusBar",
            evt -> {
                boolean value = (Boolean) evt.getNewValue();
                setStatusBarVisible(value);
            });
        setStatusBarVisible(applicationPreferenceModel.isStatusBar());

        applicationPreferenceModel.addPropertyChangeListener(
            "receivers",
            evt -> {
                boolean value = (Boolean) evt.getNewValue();

                if (value) {
                    showReceiverPanel();
                } else {
                    hideReceiverPanel();
                }
            });
//    if (applicationPreferenceModel.isReceivers()) {
//      showReceiverPanel();
//    } else {
//      hideReceiverPanel();
//    }


        applicationPreferenceModel.addPropertyChangeListener(
            "toolbar",
            evt -> {
                boolean value = (Boolean) evt.getNewValue();
                toolbar.setVisible(value);
            });
        toolbar.setVisible(applicationPreferenceModel.isToolbar());

    }

    /**
     * Displays a dialog which will provide options for selecting a configuration
     */
    private void showReceiverConfigurationPanel() {
        SwingUtilities.invokeLater(
            () -> {
                final JDialog dialog = new JDialog(LogUI.this, true);
                dialog.setTitle("Load events into Chainsaw");
                dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);

                dialog.setResizable(false);

                receiverConfigurationPanel.setCompletionActionListener(
                    e -> {
                        dialog.setVisible(false);

                        if (receiverConfigurationPanel.getModel().isCancelled()) {
                            return;
                        }
                        applicationPreferenceModel.setShowNoReceiverWarning(!receiverConfigurationPanel.isDontWarnMeAgain());
                        //remove existing plugins
                        List<Plugin> plugins = pluginRegistry.getPlugins();
                        for (Object plugin1 : plugins) {
                            Plugin plugin = (Plugin) plugin1;
                            //don't stop ZeroConfPlugin if it is registered
                            if (!plugin.getName().toLowerCase(Locale.ENGLISH).contains("zeroconf")) {
                                pluginRegistry.stopPlugin(plugin.getName());
                            }
                        }
                        URL configURL = null;

                        if (receiverConfigurationPanel.getModel().isNetworkReceiverMode()) {
                            int port = receiverConfigurationPanel.getModel().getNetworkReceiverPort();

                            try {
                                Class<? extends Receiver> receiverClass = receiverConfigurationPanel.getModel().getNetworkReceiverClass();
                                Receiver networkReceiver = receiverClass.newInstance();
                                networkReceiver.setName(receiverClass.getSimpleName() + "-" + port);

                                Method portMethod =
                                    networkReceiver.getClass().getMethod(
                                        "setPort", int.class);
                                portMethod.invoke(
                                    networkReceiver, port);

                                networkReceiver.setThreshold(Level.TRACE);

                                pluginRegistry.addPlugin(networkReceiver);
                                networkReceiver.activateOptions();
                                receiversPanel.updateReceiverTreeInDispatchThread();
                            } catch (Exception e3) {
                                MessageCenter.getInstance().getLogger().error(
                                    "Error creating Receiver", e3);
                                MessageCenter.getInstance().getLogger().info(
                                    "An error occurred creating your Receiver");
                            }
                        } else if (receiverConfigurationPanel.getModel().isLog4jConfig()) {
                            File log4jConfigFile = receiverConfigurationPanel.getModel().getLog4jConfigFile();
                            if (log4jConfigFile != null) {
                                try {
                                    Map<String, Map<String, String>> entries = LogFilePatternLayoutBuilder.getAppenderConfiguration(log4jConfigFile);
                                    for (Object o : entries.entrySet()) {
                                        try {
                                            Map.Entry entry = (Map.Entry) o;
                                            String name = (String) entry.getKey();
                                            Map values = (Map) entry.getValue();
                                            //values: conversion, file
                                            String conversionPattern = values.get("conversion").toString();
                                            File file = new File(values.get("file").toString());
                                            URL fileURL = file.toURI().toURL();
                                            String timestampFormat = LogFilePatternLayoutBuilder.getTimeStampFormat(conversionPattern);
                                            String receiverPattern = LogFilePatternLayoutBuilder.getLogFormatFromPatternLayout(conversionPattern);
                                            VFSLogFilePatternReceiver fileReceiver = new VFSLogFilePatternReceiver();
                                            fileReceiver.setName(name);
                                            fileReceiver.setAutoReconnect(true);
                                            fileReceiver.setContainer(LogUI.this);
                                            fileReceiver.setAppendNonMatches(true);
                                            fileReceiver.setFileURL(fileURL.toURI().toString());
                                            fileReceiver.setTailing(true);
                                            fileReceiver.setLogFormat(receiverPattern);
                                            fileReceiver.setTimestampFormat(timestampFormat);
                                            fileReceiver.setThreshold(Level.TRACE);
                                            pluginRegistry.addPlugin(fileReceiver);
                                            fileReceiver.activateOptions();
                                            receiversPanel.updateReceiverTreeInDispatchThread();
                                        } catch (URISyntaxException e1) {
                                            e1.printStackTrace();
                                        }
                                    }
                                } catch (IOException e1) {
                                    e1.printStackTrace();
                                }
                            }
                        } else if (receiverConfigurationPanel.getModel().isLoadConfig()) {
                            configURL = receiverConfigurationPanel.getModel().getConfigToLoad();
                        } else if (receiverConfigurationPanel.getModel().isLogFileReceiverConfig()) {
                            try {
                                URL fileURL = receiverConfigurationPanel.getModel().getLogFileURL();
                                if (fileURL != null) {
                                    VFSLogFilePatternReceiver fileReceiver = new VFSLogFilePatternReceiver();
                                    fileReceiver.setName(fileURL.getFile());
                                    fileReceiver.setAutoReconnect(true);
                                    fileReceiver.setContainer(LogUI.this);
                                    fileReceiver.setAppendNonMatches(true);
                                    fileReceiver.setFileURL(fileURL.toURI().toString());
                                    fileReceiver.setTailing(true);
                                    if (receiverConfigurationPanel.getModel().isPatternLayoutLogFormat()) {
                                        fileReceiver.setLogFormat(LogFilePatternLayoutBuilder.getLogFormatFromPatternLayout(receiverConfigurationPanel.getModel().getLogFormat()));
                                    } else {
                                        fileReceiver.setLogFormat(receiverConfigurationPanel.getModel().getLogFormat());
                                    }
                                    fileReceiver.setTimestampFormat(receiverConfigurationPanel.getModel().getLogFormatTimestampFormat());
                                    fileReceiver.setThreshold(Level.TRACE);

                                    pluginRegistry.addPlugin(fileReceiver);
                                    fileReceiver.activateOptions();
                                    receiversPanel.updateReceiverTreeInDispatchThread();
                                }
                            } catch (Exception e2) {
                                MessageCenter.getInstance().getLogger().error(
                                    "Error creating Receiver", e2);
                                MessageCenter.getInstance().getLogger().info(
                                    "An error occurred creating your Receiver");
                            }
                        }
                        if (configURL == null && receiverConfigurationPanel.isDontWarnMeAgain()) {
                            //use the saved config file as the config URL if defined
                            if (receiverConfigurationPanel.getModel().getSaveConfigFile() != null) {
                                try {
                                    configURL = receiverConfigurationPanel.getModel().getSaveConfigFile().toURI().toURL();
                                } catch (MalformedURLException e1) {
                                    e1.printStackTrace();
                                }
                            } else {
                                //no saved config defined but don't warn me is checked - use default config
                                configURL = receiverConfigurationPanel.getModel().getDefaultConfigFileURL();
                            }
                        }
                        if (configURL != null) {
                            MessageCenter.getInstance().getLogger().debug(
                                "Initialiazing Log4j with " + configURL.toExternalForm());
                            final URL finalURL = configURL;
                            new Thread(
                                () -> {
                                    if (receiverConfigurationPanel.isDontWarnMeAgain()) {
                                        applicationPreferenceModel.setConfigurationURL(finalURL.toExternalForm());
                                    } else {
                                        try {
                                            if (new File(finalURL.toURI()).exists()) {
                                                loadConfigurationUsingPluginClassLoader(finalURL);
                                            }
                                        } catch (URISyntaxException e12) {
                                            //ignore
                                        }
                                    }

                                    receiversPanel.updateReceiverTreeInDispatchThread();
                                }).start();
                        }
                        File saveConfigFile = receiverConfigurationPanel.getModel().getSaveConfigFile();
                        if (saveConfigFile != null) {
                            ReceiversHelper.getInstance().saveReceiverConfiguration(saveConfigFile);
                        }
                    });

                receiverConfigurationPanel.setDialog(dialog);
                dialog.getContentPane().add(receiverConfigurationPanel);

                dialog.pack();

                Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
                dialog.setLocation(
                    (screenSize.width / 2) - (dialog.getWidth() / 2),
                    (screenSize.height / 2) - (dialog.getHeight() / 2));

                dialog.setVisible(true);
            });
    }

    /**
     * Exits the application, ensuring Settings are saved.
     */
    public boolean exit() {
        getSettingsManager().saveSettings();

        return shutdown();
    }

    void addWelcomePanel() {
        getTabbedPane().insertTab(
            ChainsawTabbedPane.WELCOME_TAB, new ImageIcon(ChainsawIcons.ABOUT), welcomePanel,
            "Welcome/Help", 0);
        getTabbedPane().setSelectedComponent(welcomePanel);
        getPanelMap().put(ChainsawTabbedPane.WELCOME_TAB, welcomePanel);
    }

    void removeWelcomePanel() {
        EventQueue.invokeLater(() -> {
            if (getTabbedPane().containsWelcomePanel()) {
                getTabbedPane().remove(
                    getTabbedPane().getComponentAt(getTabbedPane().indexOfTab(ChainsawTabbedPane.WELCOME_TAB)));
            }
        });
    }

    ChainsawStatusBar getStatusBar() {
        return statusBar;
    }

    public void showApplicationPreferences() {
        applicationPreferenceModelPanel.updateModel();
        preferencesFrame.setVisible(true);
    }

    public void showReceiverConfiguration() {
        showReceiverConfigurationPanel();
    }

    public void showAboutBox() {
        if (aboutBox == null) {
            aboutBox = new ChainsawAbout(this);
        }

        aboutBox.setVisible(true);
    }

    Map getPanels() {
        Map m = new HashMap();
        Set<Map.Entry<String, Component>> panelSet = getPanelMap().entrySet();

        for (Object aPanelSet : panelSet) {
            Map.Entry entry = (Map.Entry) aPanelSet;
            Object o = entry.getValue();
            boolean valueToSend;
            valueToSend = !(o instanceof LogPanel) || ((DockablePanel) entry.getValue()).isDocked();
            m.put(entry.getKey(), valueToSend);
        }

        return m;
    }

    void displayPanel(String panelName, boolean display) {
        Component p = getPanelMap().get(panelName);

        int index = getTabbedPane().indexOfTab(panelName);

        if ((index == -1) && display) {
            getTabbedPane().addTab(panelName, p);
        }

        if ((index > -1) && !display) {
            getTabbedPane().removeTabAt(index);
        }
    }


    /**
     * Shutsdown by ensuring the Appender gets a chance to close.
     */
    public boolean shutdown() {
        if (getApplicationPreferenceModel().isConfirmExit()) {
            if (
                JOptionPane.showConfirmDialog(
                    LogUI.this, "Are you sure you want to exit Chainsaw?",
                    "Confirm Exit", JOptionPane.YES_NO_OPTION,
                    JOptionPane.INFORMATION_MESSAGE) != JOptionPane.YES_OPTION) {
                return false;
            }

        }

        final JWindow progressWindow = new JWindow();
        final ProgressPanel panel = new ProgressPanel(1, 3, "Shutting down");
        progressWindow.getContentPane().add(panel);
        progressWindow.pack();

        Point p = new Point(getLocation());
        p.move((int) getSize().getWidth() >> 1, (int) getSize().getHeight() >> 1);
        progressWindow.setLocation(p);
        progressWindow.setVisible(true);

        Runnable runnable =
            () -> {
                try {
                    int progress = 1;
                    final int delay = 25;

                    handler.close();
                    panel.setProgress(progress++);

                    Thread.sleep(delay);

                    pluginRegistry.stopAllPlugins();
                    panel.setProgress(progress++);

                    Thread.sleep(delay);

                    panel.setProgress(progress++);
                    Thread.sleep(delay);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                fireShutdownEvent();
                performShutdownAction();
                progressWindow.setVisible(false);
            };

        if (OSXIntegration.IS_OSX) {
            /**
             * or OSX we do it in the current thread because otherwise returning
             * will exit the process before it's had a chance to save things
             *
             */
            runnable.run();
        } else {
            new Thread(runnable).start();
        }
        return true;
    }

    /**
     * Ensures all the registered ShutdownListeners are notified.
     */
    private void fireShutdownEvent() {
        ShutdownListener[] listeners =
            shutdownListenerList.getListeners(
                ShutdownListener.class);

        for (ShutdownListener listener : listeners) {
            listener.shuttingDown();
        }
    }

    /**
     * Configures LogUI's with an action to execute when the user requests to
     * exit the application, the default action is to exit the VM. This Action is
     * called AFTER all the ShutdownListeners have been notified
     *
     * @param shutdownAction
     */
    public final void setShutdownAction(Action shutdownAction) {
        this.shutdownAction = shutdownAction;
    }

    /**
     * Using the current thread, calls the registed Shutdown action's
     * actionPerformed(...) method.
     */
    private void performShutdownAction() {
        MessageCenter.getInstance().getLogger().debug(
            "Calling the shutdown Action. Goodbye!");

        shutdownAction.actionPerformed(
            new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "Shutting Down"));
    }

    /**
     * Returns the currently selected LogPanel, if there is one, otherwise null
     *
     * @return current log panel
     */
    LogPanel getCurrentLogPanel() {
        Component selectedTab = getTabbedPane().getSelectedComponent();

        if (selectedTab instanceof LogPanel) {
            return (LogPanel) selectedTab;
        }

        return null;
    }

    /**
     * @param visible
     */
    private void setStatusBarVisible(final boolean visible) {
        MessageCenter.getInstance().getLogger().debug(
            "Setting StatusBar to " + visible);
        SwingUtilities.invokeLater(
            () -> statusBar.setVisible(visible));
    }

    boolean isStatusBarVisible() {
        return statusBar.isVisible();
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public String getActiveTabName() {
        int index = getTabbedPane().getSelectedIndex();

        if (index == -1) {
            return null;
        } else {
            return getTabbedPane().getTitleAt(index);
        }
    }

    /**
     * Causes the Welcome Panel to become visible, and shows the URL specified as
     * it's contents
     *
     * @param url for content to show
     */
    public void showHelp(URL url) {
        ensureWelcomePanelVisible();
        //    TODO ensure the Welcome Panel is the selected tab
        getWelcomePanel().setURL(url);
    }

    /**
     * DOCUMENT ME!
     *
     * @return welcome panel
     */
    private WelcomePanel getWelcomePanel() {
        return welcomePanel;
    }

    /**
     * DOCUMENT ME!
     *
     * @return log tree panel visible flag
     */
    public boolean isLogTreePanelVisible() {
        return getCurrentLogPanel() != null && getCurrentLogPanel().isLogTreeVisible();

    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public Map<String, Component> getPanelMap() {
        return panelMap;
    }

    //  public Map getLevelMap() {
    //    return levelMap;
    //  }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public SettingsManager getSettingsManager() {
        return sm;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public List<String> getFilterableColumns() {
        return filterableColumns;
    }

    /**
     * DOCUMENT ME!
     *
     * @param tbms DOCUMENT ME!
     */
    public void setToolBarAndMenus(ChainsawToolBarAndMenus tbms) {
        this.tbms = tbms;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public ChainsawToolBarAndMenus getToolBarAndMenus() {
        return tbms;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public Map getTableMap() {
        return tableMap;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public Map getTableModelMap() {
        return tableModelMap;
    }

    /**
     * DOCUMENT ME!
     *
     * @param tabbedPane DOCUMENT ME!
     */
    public void setTabbedPane(ChainsawTabbedPane tabbedPane) {
        this.tabbedPane = tabbedPane;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public ChainsawTabbedPane getTabbedPane() {
        return tabbedPane;
    }

    /**
     * @return Returns the applicationPreferenceModel.
     */
    public final ApplicationPreferenceModel getApplicationPreferenceModel() {
        return applicationPreferenceModel;
    }

    /**
     * DOCUMENT ME!
     */
    public void setupTutorial() {
        SwingUtilities.invokeLater(
            () -> {
                Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
                setLocation(0, getLocation().y);

                double chainsawwidth = 0.7;
                double tutorialwidth = 1 - chainsawwidth;
                setSize((int) (screen.width * chainsawwidth), getSize().height);
                invalidate();
                validate();

                Dimension size = getSize();
                Point loc = getLocation();
                tutorialFrame.setSize(
                    (int) (screen.width * tutorialwidth), size.height);
                tutorialFrame.setLocation(loc.x + size.width, loc.y);
                tutorialFrame.setVisible(true);
            });
    }

    private void buildLogPanel(
        boolean customExpression, final String ident, final List<LoggingEvent> events)
        throws IllegalArgumentException {
        final LogPanel thisPanel = new LogPanel(getStatusBar(), ident, cyclicBufferSize, allColorizers, applicationPreferenceModel);

        getSettingsManager().addSettingsListener(thisPanel);
        getSettingsManager().configure(thisPanel);


        /**
         * Now add the panel as a batch listener so it can handle it's own
         * batchs
         */
        if (customExpression) {
            handler.addCustomEventBatchListener(ident, thisPanel);
        } else {
            identifierPanels.add(thisPanel);
            handler.addEventBatchListener(thisPanel);
        }

        TabIconHandler iconHandler = new TabIconHandler(ident);
        thisPanel.addEventCountListener(iconHandler);


        tabbedPane.addChangeListener(iconHandler);

        PropertyChangeListener toolbarMenuUpdateListener =
            evt -> tbms.stateChange();

        thisPanel.addPropertyChangeListener(toolbarMenuUpdateListener);
        thisPanel.addPreferencePropertyChangeListener(toolbarMenuUpdateListener);

        thisPanel.addPropertyChangeListener(
            "docked",
            evt -> {
                LogPanel logPanel = (LogPanel) evt.getSource();

                if (logPanel.isDocked()) {
                    getPanelMap().put(logPanel.getIdentifier(), logPanel);
                    getTabbedPane().addANewTab(
                        logPanel.getIdentifier(), logPanel, null);
                    getTabbedPane().setSelectedTab(getTabbedPane().indexOfTab(logPanel.getIdentifier()));
                } else {
                    getTabbedPane().remove(logPanel);
                }
            });

        logger.debug("adding logpanel to tabbed pane: " + ident);

        //NOTE: tab addition is a very fragile process - if you modify this code,
        //verify the frames in the individual log panels initialize to their
        //correct sizes
        getTabbedPane().add(ident, thisPanel);
        getPanelMap().put(ident, thisPanel);

        /**
         * Let the new LogPanel receive this batch
         */

        SwingUtilities.invokeLater(
            () -> {
                getTabbedPane().addANewTab(
                    ident, thisPanel, new ImageIcon(ChainsawIcons.ANIM_RADIO_TOWER));
                thisPanel.layoutComponents();
                thisPanel.receiveEventBatch(ident, events);
                if (!getTabbedPane().tabSetting.isChainsawLog()) {
                    displayPanel("chainsaw-log", false);
                }
            });

        String msg = "added tab " + ident;
        MessageCenter.getInstance().getLogger().debug(msg);
    }


    public void createCustomExpressionLogPanel(String ident) {
        //collect events matching the rule from all of the tabs
        try {
            List<LoggingEvent> list = new ArrayList<>();
            Rule rule = ExpressionRule.getRule(ident);

            for (Object identifierPanel : identifierPanels) {
                LogPanel panel = (LogPanel) identifierPanel;

                for (Object o : panel.getMatchingEvents(rule)) {
                    LoggingEventWrapper e = (LoggingEventWrapper) o;
                    list.add(e.getLoggingEvent());
                }
            }

            buildLogPanel(true, ident, list);
        } catch (IllegalArgumentException iae) {
            MessageCenter.getInstance().getLogger().info(
                "Unable to add tab using expression: " + ident + ", reason: "
                    + iae.getMessage());
        }
    }

    /**
     * Loads the log4j configuration file specified by the url, using
     * the PluginClassLoader instance as a TCCL, but only replacing it temporarily, with the original
     * TCCL being restored in a finally block to ensure consitency.
     *
     * @param url
     */
    private void loadConfigurationUsingPluginClassLoader(final URL url) {
        ClassLoader classLoader = PluginClassLoaderFactory.getInstance().getClassLoader();
        ClassLoader previousTCCL = Thread.currentThread().getContextClassLoader();

        if (url != null) {
            try {
                // we temporarily swap the TCCL so that plugins can find resources
                Thread.currentThread().setContextClassLoader(classLoader);
                try {
                    DOMConfigurator.configure(url);
                } catch (Exception e) {
                    logger.warn("Unable to load configuration URL: " + url, e);
                }
            } finally {
                // now switch it back...
                Thread.currentThread().setContextClassLoader(previousTCCL);
            }
        }
        ensureChainsawAppenderHandlerAdded();
    }

    private static void loadLookAndFeelUsingPluginClassLoader(String lookAndFeelClassName) {
        ClassLoader classLoader = PluginClassLoaderFactory.getInstance().getClassLoader();
        ClassLoader previousTCCL = Thread.currentThread().getContextClassLoader();
        try {
            // we temporarily swap the TCCL so that plugins can find resources
            Thread.currentThread().setContextClassLoader(classLoader);
            UIManager.setLookAndFeel(lookAndFeelClassName);
            UIManager.getLookAndFeelDefaults().put("ClassLoader", classLoader);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // now switch it back...
            Thread.currentThread().setContextClassLoader(previousTCCL);
        }
    }

    /**
     * Makes sure that the LoggerRepository has the ChainsawAppenderHandler
     * added to the root logger so Chainsaw can receive all the events.
     */
    private void ensureChainsawAppenderHandlerAdded() {
        if (!LogManager.getLoggerRepository().getRootLogger().isAttached(handler)) {
            LogManager.getLoggerRepository().getRootLogger().addAppender(handler);
        }
    }

    /**
     * This class handles the recption of the Event batches and creates new
     * LogPanels if the identifier is not in use otherwise it ignores the event
     * batch.
     *
     * @author Paul Smith
     * &lt;psmith@apache.org&gt;
     */
    private class NewTabEventBatchReceiver implements EventBatchListener {
        /**
         * DOCUMENT ME!
         *
         * @param ident
         * @param events
         */
        public void receiveEventBatch(
            final String ident, final List<LoggingEvent> events) {
            if (events.size() == 0) {
                return;
            }

            if (!isGUIFullyInitialized) {
                synchronized (initializationLock) {
                    while (!isGUIFullyInitialized) {
                        System.out.println(
                            "Wanting to add a row, but GUI not initialized, waiting...");

                        /**
                         * Lets wait 1 seconds and recheck.
                         */
                        try {
                            initializationLock.wait(1000);
                            logger.debug("waiting for initialization to complete");
                        } catch (InterruptedException e) {
                        }
                    }
                    logger.debug("out of system initialization wait loop");
                }
            }

            if (!getPanelMap().containsKey(ident)) {
                logger.debug("panel " + ident + " does not exist - creating");
                try {
                    buildLogPanel(false, ident, events);
                } catch (IllegalArgumentException iae) {
                    logger.error("error creating log panel", iae);
                    //should not happen - not a custom expression panel
                }
            }
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.log4j.chainsaw.EventBatchListener#getInterestedIdentifier()
         */

        /**
         * DOCUMENT ME!
         *
         * @return DOCUMENT ME!
         */
        public String getInterestedIdentifier() {
            // we are interested in all batches so we can detect new identifiers
            return null;
        }
    }

    private class TabIconHandler implements EventCountListener, ChangeListener {
        //the tabIconHandler is associated with a new tab, and a new tab always
        //shows the 'new events' icon
        private boolean newEvents = true;
        private boolean seenEvents = false;
        private final String ident;
        ImageIcon NEW_EVENTS = new ImageIcon(ChainsawIcons.ANIM_RADIO_TOWER);
        ImageIcon HAS_EVENTS = new ImageIcon(ChainsawIcons.INFO);
        Icon SELECTED = LineIconFactory.createBlankIcon();

        public TabIconHandler(String identifier) {
            ident = identifier;

            new Thread(
                () -> {
                    while (true) {
                        //if this tab is active, remove the icon
                        //don't process undocked tabs
                        if (getTabbedPane().indexOfTab(ident) > -1 &&
                            getTabbedPane().getSelectedIndex() == getTabbedPane()
                                .indexOfTab(ident)) {
                            getTabbedPane().setIconAt(
                                getTabbedPane().indexOfTab(ident), SELECTED);
                            newEvents = false;
                            seenEvents = true;
                        } else if (getTabbedPane().indexOfTab(ident) > -1) {
                            if (newEvents) {
                                getTabbedPane().setIconAt(
                                    getTabbedPane().indexOfTab(ident), NEW_EVENTS);
                                newEvents = false;
                                seenEvents = false;
                            } else if (!seenEvents) {
                                getTabbedPane().setIconAt(
                                    getTabbedPane().indexOfTab(ident), HAS_EVENTS);
                            }
                        }

                        try {
                            Thread.sleep(handler.getQueueInterval() + 1000);
                        } catch (InterruptedException ie) {
                        }
                    }
                }).start();
        }

        /**
         * DOCUMENT ME!
         *
         * @param currentCount DOCUMENT ME!
         * @param totalCount   DOCUMENT ME!
         */
        public void eventCountChanged(int currentCount, int totalCount) {
            newEvents = true;
        }

        public void stateChanged(ChangeEvent event) {
            if (
                getTabbedPane().indexOfTab(ident) > -1 && getTabbedPane().indexOfTab(ident) == getTabbedPane().getSelectedIndex()) {
                getTabbedPane().setIconAt(getTabbedPane().indexOfTab(ident), SELECTED);
            }
        }
    }
}
