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.Dimension;
024import java.awt.HeadlessException;
025import java.awt.Toolkit;
026import java.io.File;
027import java.io.FileReader;
028import java.io.IOException;
029import java.text.MessageFormat;
030import java.util.HashMap;
031import java.util.Locale;
032import java.util.Map;
033import java.util.MissingResourceException;
034import java.util.Properties;
035import java.util.ResourceBundle;
036import java.util.concurrent.ExecutorService;
037import java.util.concurrent.Executors;
038
039import javax.swing.SwingUtilities;
040
041import org.apache.commons.lang3.SystemUtils;
042import org.apache.commons.logging.Log;
043import org.apache.commons.logging.LogFactory;
044import org.apache.commons.vfs2.FileChangeEvent;
045import org.apache.commons.vfs2.FileListener;
046import org.apache.commons.vfs2.FileObject;
047import org.apache.commons.vfs2.FileSystemException;
048import org.apache.commons.vfs2.VFS;
049import org.apache.commons.vfs2.impl.DefaultFileMonitor;
050
051import org.nuxeo.connect.update.PackageException;
052import org.nuxeo.launcher.NuxeoLauncher;
053import org.nuxeo.launcher.config.ConfigurationGenerator;
054import org.nuxeo.launcher.daemon.DaemonThreadFactory;
055import org.nuxeo.launcher.gui.logs.LogsHandler;
056import org.nuxeo.launcher.gui.logs.LogsSource;
057import org.nuxeo.launcher.gui.logs.LogsSourceThread;
058
059/**
060 * Launcher controller for graphical user interface
061 *
062 * @author jcarsique
063 * @since 5.4.2
064 * @see NuxeoLauncher
065 */
066public class NuxeoLauncherGUI {
067    static final Log log = LogFactory.getLog(NuxeoLauncherGUI.class);
068
069    protected static final long UPDATE_FREQUENCY = 3000;
070
071    private ExecutorService executor = newExecutor();
072
073    /**
074     * @since 5.6
075     */
076    protected ExecutorService newExecutor() {
077        return Executors.newCachedThreadPool(new DaemonThreadFactory("NuxeoLauncherGUITask"));
078    }
079
080    protected NuxeoLauncher launcher;
081
082    protected NuxeoFrame nuxeoFrame;
083
084    protected HashMap<String, LogsSourceThread> logsMap = new HashMap<>();
085
086    /**
087     * @since 5.6
088     */
089    public final Map<String, LogsSourceThread> getLogsMap() {
090        return logsMap;
091    }
092
093    private DefaultFileMonitor dumpedConfigMonitor;
094
095    private Thread nuxeoFrameUpdater;
096
097    /**
098     * @param aLauncher Launcher being used in background
099     */
100    public NuxeoLauncherGUI(NuxeoLauncher aLauncher) {
101        launcher = aLauncher;
102        // Set OS-specific decorations
103        if (SystemUtils.IS_OS_MAC) {
104            System.setProperty("apple.laf.useScreenMenuBar", "true");
105            System.setProperty("com.apple.mrj.application.growbox.intrudes", "false");
106            System.setProperty("com.apple.mrj.application.live-resize", "true");
107            System.setProperty("com.apple.macos.smallTabs", "true");
108        }
109        initFrame();
110        dumpedConfigMonitor = new DefaultFileMonitor(new FileListener() {
111            @Override
112            public void fileDeleted(FileChangeEvent event) {
113                // Ignore
114            }
115
116            @Override
117            public void fileCreated(FileChangeEvent event) {
118                updateNuxeoFrame();
119            }
120
121            @Override
122            public void fileChanged(FileChangeEvent event) {
123                updateNuxeoFrame();
124            }
125
126            synchronized private void updateNuxeoFrame() {
127                waitForFrameLoaded();
128                log.debug("Configuration changed. Reloading frame...");
129                launcher.init();
130                updateServerStatus();
131                try {
132                    Properties props = new Properties();
133                    props.load(new FileReader(getConfigurationGenerator().getDumpedConfig()));
134                    nuxeoFrame.updateLogsTab(props.getProperty("log.id"));
135                } catch (IOException e) {
136                    log.error(e);
137                }
138            }
139        });
140        try {
141            dumpedConfigMonitor.setRecursive(false);
142            FileObject dumpedConfig = VFS.getManager().resolveFile(
143                    getConfigurationGenerator().getDumpedConfig().getPath());
144            dumpedConfigMonitor.addFile(dumpedConfig);
145            dumpedConfigMonitor.start();
146        } catch (FileSystemException e) {
147            throw new RuntimeException("Couldn't find " + getConfigurationGenerator().getNuxeoConf(), e);
148        }
149    }
150
151    protected void initFrame() {
152        final NuxeoLauncherGUI controller = this;
153        SwingUtilities.invokeLater(() -> {
154            try {
155                if (nuxeoFrame != null) {
156                    executor.shutdownNow();
157                    nuxeoFrame.close();
158                    executor = newExecutor();
159                }
160                nuxeoFrame = createNuxeoFrame(controller);
161                nuxeoFrame.pack();
162                // Center frame
163                Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
164                nuxeoFrame.setLocation(screenSize.width / 2 - (nuxeoFrame.getWidth() / 2), screenSize.height / 2
165                        - (nuxeoFrame.getHeight() / 2));
166                nuxeoFrame.setVisible(true);
167            } catch (HeadlessException e) {
168                log.error(e);
169            }
170        });
171        if (nuxeoFrameUpdater == null) {
172            nuxeoFrameUpdater = new Thread(() -> {
173                while (true) {
174                    updateServerStatus();
175                    try {
176                        Thread.sleep(UPDATE_FREQUENCY);
177                    } catch (InterruptedException e) {
178                        Thread.currentThread().interrupt();
179                        return;
180                    }
181                }
182            });
183            nuxeoFrameUpdater.start();
184        }
185    }
186
187    /**
188     * Instantiate a new {@link NuxeoFrame}. Can be overridden if needed.
189     */
190    protected NuxeoFrame createNuxeoFrame(NuxeoLauncherGUI controller) {
191        return new NuxeoFrame(controller);
192    }
193
194    public void initLogsManagement(String logFile, ColoredTextPane textArea) {
195        File file = new File(logFile);
196        LogsSource logsSource = new LogsSource(file);
197        logsSource.skip(file.length() - NuxeoFrame.LOG_MAX_SIZE);
198        logsSource.addObserver(new LogsHandler(textArea));
199        LogsSourceThread logsSourceThread = new LogsSourceThread(logsSource);
200        logsSourceThread.setDaemon(true);
201        executor.execute(logsSourceThread);
202        logsMap.put(logFile, logsSourceThread);
203    }
204
205    /**
206     * @see NuxeoLauncher#stop()
207     */
208    public void stop() {
209        waitForFrameLoaded();
210        nuxeoFrame.stopping = true;
211        nuxeoFrame.mainButton.setText(getMessage("mainbutton.stop.inprogress"));
212        nuxeoFrame.mainButton.setToolTipText(NuxeoLauncherGUI.getMessage("mainbutton.stop.tooltip"));
213        nuxeoFrame.mainButton.setIcon(nuxeoFrame.stopIcon);
214        executor.execute(() -> {
215            launcher.stop();
216            nuxeoFrame.stopping = false;
217            updateServerStatus();
218        });
219    }
220
221    /**
222     * Update interface information with current server status.
223     *
224     * @see NuxeoFrame#updateMainButton()
225     * @see NuxeoFrame#updateSummary()
226     */
227    public void updateServerStatus() {
228        waitForFrameLoaded();
229        nuxeoFrame.updateMainButton();
230        nuxeoFrame.updateLaunchBrowserButton();
231        nuxeoFrame.updateSummary();
232    }
233
234    /**
235     * Waits for the Launcher GUI frame being initialized. Should be called before any access to {@link NuxeoFrame} from
236     * this controller.
237     */
238    public void waitForFrameLoaded() {
239        while (nuxeoFrame == null) {
240            try {
241                Thread.sleep(500);
242            } catch (InterruptedException e) {
243                Thread.currentThread().interrupt();
244                throw new RuntimeException(e);
245            }
246        }
247    }
248
249    /**
250     * @see NuxeoLauncher#doStart() NuxeoLauncher#doStartAndWait()
251     */
252    public void start() {
253        waitForFrameLoaded();
254        nuxeoFrame.stopping = false;
255        nuxeoFrame.mainButton.setText(NuxeoLauncherGUI.getMessage("mainbutton.start.inprogress"));
256        nuxeoFrame.mainButton.setToolTipText(NuxeoLauncherGUI.getMessage("mainbutton.stop.tooltip"));
257        nuxeoFrame.mainButton.setIcon(nuxeoFrame.stopIcon);
258        executor.execute(() -> {
259            try {
260                launcher.doStartAndWait();
261            } catch (PackageException e) {
262                log.error("Could not initialize the packaging subsystem", e);
263                System.exit(launcher == null || launcher.getErrorValue() == NuxeoLauncher.EXIT_CODE_OK ? NuxeoLauncher.EXIT_CODE_INVALID
264                        : launcher.getErrorValue());
265            }
266            updateServerStatus();
267        });
268    }
269
270    /**
271     * @param logFile LogFile managed by the involved reader
272     * @param isActive Set logs reader active or not
273     */
274    public void notifyLogsObserver(String logFile, boolean isActive) {
275        LogsSourceThread logsSourceThread = logsMap.get(logFile);
276        if (isActive) {
277            logsSourceThread.getSource().resume();
278        } else {
279            logsSourceThread.getSource().pause();
280        }
281    }
282
283    /**
284     * @return Configuration generator used by {@link #launcher}
285     */
286    public ConfigurationGenerator getConfigurationGenerator() {
287        return launcher.getConfigurationGenerator();
288    }
289
290    /**
291     * Get internationalized message
292     *
293     * @param key Message key
294     * @return Localized message value
295     */
296    public static String getMessage(String key) {
297        String message;
298        try {
299            message = ResourceBundle.getBundle("i18n/messages").getString(key);
300        } catch (MissingResourceException e) {
301            log.debug(getMessage("missing.translation") + key);
302            message = ResourceBundle.getBundle("i18n/messages", Locale.ENGLISH).getString(key);
303        }
304        return message;
305    }
306
307    /**
308     * Get internationalized message with parameters
309     *
310     * @param key Message key
311     * @return Localized message value
312     * @since 5.9.2
313     */
314    public static String getMessage(String key, Object... params) {
315        return MessageFormat.format(getMessage(key), params);
316    }
317
318    /**
319     * @return the NuxeoLauncher managed by the current GUI
320     * @since 5.5
321     */
322    public NuxeoLauncher getLauncher() {
323        return launcher;
324    }
325
326}