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