001/*
002 * (C) Copyright 2011-2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Julien Carsique
016 *
017 */
018
019package org.nuxeo.launcher.gui;
020
021import java.awt.Color;
022import java.awt.Component;
023import java.awt.Desktop;
024import java.awt.Dimension;
025import java.awt.Font;
026import java.awt.Graphics;
027import java.awt.GridBagConstraints;
028import java.awt.GridBagLayout;
029import java.awt.HeadlessException;
030import java.awt.Image;
031import java.awt.Insets;
032import java.awt.Toolkit;
033import java.awt.event.ActionEvent;
034import java.awt.event.ComponentAdapter;
035import java.awt.event.ComponentEvent;
036import java.awt.event.WindowEvent;
037import java.awt.image.BufferedImage;
038import java.io.File;
039import java.io.IOException;
040import java.util.ArrayList;
041
042import javax.imageio.ImageIO;
043import javax.swing.AbstractAction;
044import javax.swing.Action;
045import javax.swing.BorderFactory;
046import javax.swing.BoxLayout;
047import javax.swing.Icon;
048import javax.swing.ImageIcon;
049import javax.swing.JButton;
050import javax.swing.JComponent;
051import javax.swing.JFrame;
052import javax.swing.JLabel;
053import javax.swing.JPanel;
054import javax.swing.JScrollPane;
055import javax.swing.JSeparator;
056import javax.swing.JTabbedPane;
057import javax.swing.ScrollPaneConstants;
058import javax.swing.SwingConstants;
059import javax.swing.WindowConstants;
060import javax.swing.event.ChangeEvent;
061import javax.swing.event.ChangeListener;
062
063import org.apache.commons.logging.Log;
064import org.apache.commons.logging.LogFactory;
065import org.apache.log4j.LogManager;
066import org.joda.time.DateTime;
067
068import org.nuxeo.launcher.config.ConfigurationGenerator;
069import org.nuxeo.log4j.Log4JHelper;
070import org.nuxeo.shell.Shell;
071import org.nuxeo.shell.cmds.Interactive;
072import org.nuxeo.shell.cmds.InteractiveShellHandler;
073import org.nuxeo.shell.swing.ConsolePanel;
074
075/**
076 * Launcher view for graphical user interface
077 *
078 * @author jcarsique
079 * @since 5.4.2
080 * @see NuxeoLauncherGUI
081 */
082public class NuxeoFrame extends JFrame {
083
084    /**
085     * @since 5.5
086     */
087    protected class LogsPanelListener extends ComponentAdapter {
088        private String logFile;
089
090        public LogsPanelListener(String logFile) {
091            this.logFile = logFile;
092        }
093
094        @Override
095        public void componentHidden(ComponentEvent e) {
096            controller.notifyLogsObserver(logFile, false);
097        }
098
099        @Override
100        public void componentShown(ComponentEvent e) {
101            controller.notifyLogsObserver(logFile, true);
102        }
103
104    }
105
106    protected final class ImagePanel extends JPanel {
107        private static final long serialVersionUID = 1L;
108
109        private Image backgroundImage;
110
111        public ImagePanel(Icon image, ImageIcon backgroundImage) {
112            this.backgroundImage = backgroundImage.getImage();
113            setOpaque(false);
114            add(new JLabel(image));
115        }
116
117        @Override
118        public void paintComponent(Graphics g) {
119            super.paintComponent(g);
120            g.drawImage(backgroundImage, 0, 0, this);
121        }
122    }
123
124    /**
125     * @since 5.5
126     */
127    protected Action startAction = new AbstractAction() {
128        private static final long serialVersionUID = 1L;
129
130        @Override
131        public void actionPerformed(ActionEvent e) {
132            mainButton.setEnabled(false);
133            controller.start();
134        }
135    };
136
137    protected boolean stopping = false;
138
139    /**
140     * @since 5.5
141     */
142    protected Action stopAction = new AbstractAction() {
143        private static final long serialVersionUID = 1L;
144
145        @Override
146        public void actionPerformed(ActionEvent e) {
147            mainButton.setEnabled(false);
148            controller.stop();
149        }
150    };
151
152    protected Action launchBrowserAction = new AbstractAction() {
153        private static final long serialVersionUID = 1L;
154
155        @Override
156        public void actionPerformed(ActionEvent event) {
157            try {
158                Desktop.getDesktop().browse(java.net.URI.create(controller.getLauncher().getURL()));
159            } catch (Exception e) {
160                setError("An error occurred while launching browser", e);
161            }
162        }
163
164    };
165
166    /**
167     * Log error and display its message in {@link #errorMessageLabel}
168     *
169     * @since 5.5
170     * @param message Message to log
171     * @param e Caught exception
172     */
173    public void setError(String message, Exception e) {
174        log.error(message, e);
175        errorMessageLabel.setText(NuxeoLauncherGUI.getMessage("error.occurred") + " <<" + e.getMessage() + ">>.");
176    }
177
178    /**
179     * Log error and display its message in {@link #errorMessageLabel}
180     *
181     * @since 5.5
182     * @param e Caught exception
183     */
184    public void setError(Exception e) {
185        log.error(e);
186        errorMessageLabel.setText(NuxeoLauncherGUI.getMessage("error.occurred") + " <<" + e.getMessage() + ">>.");
187    }
188
189    protected static final Log log = LogFactory.getLog(NuxeoFrame.class);
190
191    private static final long serialVersionUID = 1L;
192
193    protected static final int LOG_MAX_SIZE = 200000;
194
195    protected final ImageIcon startIcon = getImageIcon("icons/start.png");
196
197    protected final ImageIcon stopIcon = getImageIcon("icons/stop.png");
198
199    protected final ImageIcon appIcon = getImageIcon("icons/control_panel_icon_32.png");
200
201    protected JButton mainButton = null;
202
203    protected NuxeoLauncherGUI controller;
204
205    protected boolean logsShown = false;
206
207    protected JButton logsButton;
208
209    protected GridBagConstraints constraints;
210
211    protected NuxeoFrame contentPane;
212
213    protected Component filler;
214
215    protected JTabbedPane tabbedPanel;
216
217    protected ConsolePanel consolePanel;
218
219    protected JLabel summaryStatus;
220
221    protected JLabel summaryURL;
222
223    protected JButton launchBrowserButton;
224
225    protected JLabel errorMessageLabel;
226
227    private JTabbedPane logsTab;
228
229    /**
230     * @return JLabel for error display
231     * @since 5.5
232     */
233    public JLabel getErrorMessageLabel() {
234        return errorMessageLabel;
235    }
236
237    public NuxeoFrame(NuxeoLauncherGUI controller) throws HeadlessException {
238        super("NuxeoCtl");
239        setController(controller);
240
241        // Main frame
242        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
243        setIconImage(appIcon.getImage());
244        getContentPane().setBackground(new Color(35, 37, 59));
245        getContentPane().setLayout(new GridBagLayout());
246        constraints = new GridBagConstraints();
247
248        // Header (with main button inside)
249        constraints.fill = GridBagConstraints.HORIZONTAL;
250        constraints.gridx = 0;
251        constraints.anchor = GridBagConstraints.PAGE_START;
252        JComponent header = buildHeader();
253        header.setPreferredSize(new Dimension(480, 110));
254        getContentPane().add(header, constraints);
255
256        // Tabs
257        constraints.fill = GridBagConstraints.BOTH;
258        constraints.ipady = 100;
259        constraints.weightx = 1.0;
260        constraints.weighty = 1.0;
261        getContentPane().add(buildTabbedPanel(), constraints);
262
263        // Footer
264        constraints.fill = GridBagConstraints.NONE;
265        constraints.anchor = GridBagConstraints.PAGE_END;
266        constraints.ipady = 0;
267        constraints.weightx = 0;
268        constraints.weighty = 0;
269        constraints.insets = new Insets(10, 0, 0, 0);
270        getContentPane().add(buildFooter(), constraints);
271
272        // debug((JComponent) this.getContentPane());
273    }
274
275    protected Component buildConsolePanel() {
276        try {
277            consolePanel = new ConsolePanel();
278        } catch (Exception e) {
279            log.error(e);
280        }
281        Interactive.setConsoleReaderFactory(consolePanel.getConsole());
282        Interactive.setHandler(new InteractiveShellHandler() {
283            @Override
284            public void enterInteractiveMode() {
285                Interactive.reset();
286            }
287
288            @Override
289            public boolean exitInteractiveMode(int code) {
290                if (code == 1) {
291                    Interactive.reset();
292                    Shell.reset();
293                    return true;
294                } else {
295                    consolePanel.getConsole().reset();
296                    return false;
297                }
298            }
299        });
300        new Thread() {
301            @Override
302            public void run() {
303                try {
304                    Shell.get().main(new String[] { controller.launcher.getURL() + "site/automation" });
305                } catch (Exception e) {
306                    setError(e);
307                }
308            }
309        }.start();
310        return consolePanel;
311    }
312
313    protected JComponent buildFooter() {
314        JLabel label = new JLabel(NuxeoLauncherGUI.getMessage("footer.label", new DateTime().toString("Y")));
315        label.setForeground(Color.WHITE);
316        label.setPreferredSize(new Dimension(470, 16));
317        label.setFont(new Font(label.getFont().getName(), label.getFont().getStyle(), 9));
318        label.setHorizontalAlignment(SwingConstants.CENTER);
319        return label;
320    }
321
322    protected JComponent buildHeader() {
323        ImagePanel headerLogo = new ImagePanel(getImageIcon("img/nuxeo_control_panel_logo.png"),
324                getImageIcon("img/nuxeo_control_panel_bg.png"));
325        headerLogo.setLayout(new GridBagLayout());
326        // Main button (start/stop) (added to header)
327
328        GridBagConstraints headerConstraints = new GridBagConstraints();
329        headerConstraints.gridx = 0;
330        headerLogo.add(buildMainButton(), headerConstraints);
331        headerLogo.add(buildLaunchBrowserButton(), headerConstraints);
332        return headerLogo;
333    }
334
335    protected JComponent buildLaunchBrowserButton() {
336        launchBrowserButton = createButton(null);
337        launchBrowserButton.setAction(launchBrowserAction);
338        launchBrowserButton.setText(NuxeoLauncherGUI.getMessage("browser.button.text"));
339        updateLaunchBrowserButton();
340        return launchBrowserButton;
341    }
342
343    protected JTabbedPane buildLogsTab() {
344        JTabbedPane logsTabbedPane = new JTabbedPane(SwingConstants.TOP);
345        // Get Launcher log file(s)
346        ArrayList<String> logFiles = Log4JHelper.getFileAppendersFiles(LogManager.getLoggerRepository());
347        // Add nuxeoctl log file
348        File nuxeoctlLog = new File(controller.getConfigurationGenerator().getLogDir(), "nuxeoctl.log");
349        if (nuxeoctlLog.exists()) {
350            logFiles.add(nuxeoctlLog.getAbsolutePath());
351        }
352        // Get server log file(s)
353        logFiles.addAll(controller.getConfigurationGenerator().getLogFiles());
354        for (String logFile : logFiles) {
355            addFileToLogsTab(logsTabbedPane, logFile);
356        }
357        return logsTabbedPane;
358    }
359
360    protected void addFileToLogsTab(JTabbedPane logsTabbedPane, String logFile) {
361        if (!hideLogTab(logFile) && !controller.getLogsMap().containsKey(logFile)) {
362            logsTabbedPane.addTab(new File(logFile).getName(), buildLogPanel(logFile));
363        }
364    }
365
366    /**
367     * Called by buildLogsTab to know if a log file should be display. Can be overridden. Return false by default.
368     *
369     * @param logFile
370     * @return false
371     */
372    protected boolean hideLogTab(String logFile) {
373        return false;
374    }
375
376    /**
377     * @param logFile
378     */
379    protected JComponent buildLogPanel(String logFile) {
380        ColoredTextPane textArea = new ColoredTextPane();
381        textArea.setEditable(false);
382        textArea.setAutoscrolls(true);
383        textArea.setBackground(new Color(54, 55, 67));
384        textArea.setMaxSize(LOG_MAX_SIZE);
385
386        JScrollPane logsScroller = new JScrollPane(textArea);
387        logsScroller.setVisible(true);
388        logsScroller.setBorder(BorderFactory.createLineBorder(Color.BLACK));
389        logsScroller.setAutoscrolls(true);
390        logsScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
391        logsScroller.setWheelScrollingEnabled(true);
392        logsScroller.setPreferredSize(new Dimension(450, 160));
393
394        controller.initLogsManagement(logFile, textArea);
395        logsScroller.addComponentListener(new LogsPanelListener(logFile));
396        return logsScroller;
397    }
398
399    protected JComponent buildMainButton() {
400        mainButton = createButton(null);
401        updateMainButton();
402        return mainButton;
403    }
404
405    protected Component buildSummaryPanel() {
406        JPanel summaryPanel = new JPanel();
407        summaryPanel.setLayout(new BoxLayout(summaryPanel, BoxLayout.PAGE_AXIS));
408        summaryPanel.setBackground(new Color(35, 37, 59));
409        summaryPanel.setForeground(Color.WHITE);
410
411        summaryPanel.add(new JLabel("<html><font color=#ffffdd>" + NuxeoLauncherGUI.getMessage("summary.status.label")));
412        summaryStatus = new JLabel(controller.launcher.status());
413        summaryStatus.setForeground(Color.WHITE);
414        summaryPanel.add(summaryStatus);
415
416        summaryPanel.add(new JLabel("<html><font color=#ffffdd>" + NuxeoLauncherGUI.getMessage("summary.url.label")));
417        summaryURL = new JLabel(controller.launcher.getURL());
418        summaryURL.setForeground(Color.WHITE);
419        summaryPanel.add(summaryURL);
420
421        errorMessageLabel = new JLabel();
422        errorMessageLabel.setForeground(Color.RED);
423        summaryPanel.add(errorMessageLabel);
424
425        summaryPanel.add(new JSeparator());
426        ConfigurationGenerator config = controller.launcher.getConfigurationGenerator();
427        summaryPanel.add(new JLabel("<html><font color=#ffffdd>" + NuxeoLauncherGUI.getMessage("summary.homedir.label")));
428        summaryPanel.add(new JLabel("<html><font color=white>" + config.getNuxeoHome().getPath()));
429        summaryPanel.add(new JLabel("<html><font color=#ffffdd>"
430                + NuxeoLauncherGUI.getMessage("summary.nuxeoconf.label")));
431        summaryPanel.add(new JLabel("<html><font color=white>" + config.getNuxeoConf().getPath()));
432        summaryPanel.add(new JLabel("<html><font color=#ffffdd>" + NuxeoLauncherGUI.getMessage("summary.datadir.label")));
433        summaryPanel.add(new JLabel("<html><font color=white>" + config.getDataDir().getPath()));
434        return summaryPanel;
435    }
436
437    protected JComponent buildTabbedPanel() {
438        tabbedPanel = new JTabbedPane(SwingConstants.TOP);
439        tabbedPanel.addTab(NuxeoLauncherGUI.getMessage("tab.summary.title"), buildSummaryPanel());
440        logsTab = buildLogsTab();
441        tabbedPanel.addTab(NuxeoLauncherGUI.getMessage("tab.logs.title"), logsTab);
442        tabbedPanel.addTab(NuxeoLauncherGUI.getMessage("tab.shell.title"), buildConsolePanel());
443        tabbedPanel.addChangeListener(new ChangeListener() {
444            @Override
445            public void stateChanged(ChangeEvent e) {
446                JTabbedPane pane = (JTabbedPane) e.getSource();
447                if (pane.getSelectedIndex() == 2) {
448                    consolePanel.getConsole().requestFocusInWindow();
449                }
450            }
451        });
452        return tabbedPanel;
453    }
454
455    protected JButton createButton(ImageIcon icon) {
456        JButton button = new JButton();
457        button.setIcon(icon);
458        return button;
459    }
460
461    public void debug(JComponent parent) {
462        for (Component comp : parent.getComponents()) {
463            if (comp instanceof JComponent) {
464                ((JComponent) comp).setBorder(BorderFactory.createCompoundBorder(
465                        BorderFactory.createLineBorder(Color.red), ((JComponent) comp).getBorder()));
466                log.info(comp.getClass() + " size: " + ((JComponent) comp).getSize());
467            }
468        }
469    }
470
471    protected ImageIcon getImageIcon(String resourcePath) {
472        BufferedImage image = null;
473        try {
474            image = ImageIO.read(getClass().getClassLoader().getResource(resourcePath));
475        } catch (IOException e) {
476            log.error(e);
477        }
478        return new ImageIcon(image);
479    }
480
481    protected void updateMainButton() {
482        if (controller.launcher.isStarted()) {
483            mainButton.setAction(stopAction);
484            mainButton.setText(NuxeoLauncherGUI.getMessage("mainbutton.stop.text"));
485            mainButton.setToolTipText(NuxeoLauncherGUI.getMessage("mainbutton.stop.tooltip"));
486            mainButton.setIcon(stopIcon);
487        } else if (controller.launcher.isRunning()) {
488            if (stopping) {
489                mainButton.setAction(stopAction);
490                mainButton.setText(NuxeoLauncherGUI.getMessage("mainbutton.stop.inprogress"));
491            } else {
492                mainButton.setAction(stopAction);
493                mainButton.setText(NuxeoLauncherGUI.getMessage("mainbutton.start.inprogress"));
494            }
495            mainButton.setToolTipText(NuxeoLauncherGUI.getMessage("mainbutton.stop.tooltip"));
496            mainButton.setIcon(stopIcon);
497        } else {
498            mainButton.setAction(startAction);
499            mainButton.setText(NuxeoLauncherGUI.getMessage("mainbutton.start.text"));
500            mainButton.setToolTipText(NuxeoLauncherGUI.getMessage("mainbutton.start.tooltip"));
501            mainButton.setIcon(startIcon);
502        }
503        mainButton.setEnabled(true);
504        mainButton.validate();
505    }
506
507    /**
508     * @since 5.5
509     */
510    protected void updateLaunchBrowserButton() {
511        launchBrowserButton.setEnabled(controller.launcher.isStarted());
512    }
513
514    /**
515     * Update information displayed in summary tab
516     */
517    public void updateSummary() {
518        String errorMessageLabelStr = "";
519        Color summaryStatusFgColor = Color.WHITE;
520        if (!controller.getConfigurationGenerator().isWizardRequired() && controller.launcher.isStarted()) {
521            String startupSummary = controller.launcher.getStartupSummary();
522            if (!controller.launcher.wasStartupFine()) {
523                String[] lines = startupSummary.split("\n");
524                // extract line with summary informations
525                for (String line : lines) {
526                    if (line.contains("Component Loading Status")) {
527                        startupSummary = line;
528                        break;
529                    }
530                }
531                errorMessageLabelStr = "An error was detected during startup " + startupSummary + ".";
532                summaryStatusFgColor = Color.RED;
533            }
534        }
535        errorMessageLabel.setText(errorMessageLabelStr);
536        summaryStatus.setForeground(summaryStatusFgColor);
537        summaryStatus.setText(controller.launcher.status());
538        summaryURL.setText(controller.launcher.getURL());
539    }
540
541    /**
542     * Add Windows rotated console log
543     *
544     * @since 5.6
545     */
546    public void updateLogsTab(String consoleLogId) {
547        if (consoleLogId != null) {
548            addFileToLogsTab(logsTab, new File(controller.getConfigurationGenerator().getLogDir(), "console"
549                    + consoleLogId + ".log").getPath());
550        }
551    }
552
553    /**
554     * @since 5.5
555     * @return GUI controller
556     */
557    public NuxeoLauncherGUI getController() {
558        return controller;
559    }
560
561    public void setController(NuxeoLauncherGUI controller) {
562        this.controller = controller;
563    }
564
565    /**
566     * @since 5.6
567     */
568    public void close() {
569        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
570        Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
571    }
572
573}