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