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