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