001package apps;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.BorderLayout;
006import java.awt.Color;
007import java.awt.Font;
008import java.awt.datatransfer.Clipboard;
009import java.awt.datatransfer.StringSelection;
010import java.awt.event.ActionEvent;
011import java.awt.event.MouseAdapter;
012import java.awt.event.MouseEvent;
013import java.awt.event.MouseListener;
014import java.io.IOException;
015import java.io.OutputStream;
016import java.io.PrintStream;
017import java.lang.reflect.InvocationTargetException;
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.Map;
021import java.util.ResourceBundle;
022
023import javax.swing.ButtonGroup;
024import javax.swing.JButton;
025import javax.swing.JCheckBox;
026import javax.swing.JFrame;
027import javax.swing.JMenu;
028import javax.swing.JMenuItem;
029import javax.swing.JPanel;
030import javax.swing.JPopupMenu;
031import javax.swing.JRadioButtonMenuItem;
032import javax.swing.JScrollPane;
033import javax.swing.JSeparator;
034import javax.swing.SwingUtilities;
035
036import jmri.UserPreferencesManager;
037import jmri.util.JmriJFrame;
038import jmri.util.swing.TextAreaFIFO;
039
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/**
044 * Class to direct standard output and standard error to a ( JTextArea ) TextAreaFIFO .
045 * This allows for easier clipboard operations etc.
046 * <hr>
047 * This file is part of JMRI.
048 * <p>
049 * JMRI is free software; you can redistribute it and/or modify it under the
050 * terms of version 2 of the GNU General Public License as published by the Free
051 * Software Foundation. See the "COPYING" file for a copy of this license.
052 * <p>
053 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
054 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
055 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
056 *
057 * @author Matthew Harris copyright (c) 2010, 2011, 2012
058 */
059public final class SystemConsole {
060
061    /**
062     * Get current SystemConsole instance.
063     * If one doesn't yet exist, create it.
064     * @return current SystemConsole instance
065     */
066    public static SystemConsole getInstance() {
067        return InstanceHolder.INSTANCE;
068    }
069
070    private static class InstanceHolder {
071        private static final SystemConsole INSTANCE;
072
073        static {
074            SystemConsole instance = null;
075            try {
076                instance = new SystemConsole();
077            } catch (RuntimeException ex) {
078                log.error("failed to complete Console redirection", ex);
079            }
080            INSTANCE = instance;
081        }
082    }
083
084    static final ResourceBundle rbc = ResourceBundle.getBundle("apps.AppsConfigBundle"); // NOI18N
085
086    private static final int STD_ERR = 1;
087    private static final int STD_OUT = 2;
088
089    private final TextAreaFIFO console;
090
091    private final PrintStream originalOut;
092    private final PrintStream originalErr;
093
094    private final PrintStream outputStream;
095    private final PrintStream errorStream;
096
097    private JmriJFrame frame = null;
098
099    private final JPopupMenu popup = new JPopupMenu();
100
101    private JMenuItem copySelection = null;
102
103    private JMenu wrapMenu = null;
104    private ButtonGroup wrapGroup = null;
105
106    private JMenu schemeMenu = null;
107    private ButtonGroup schemeGroup = null;
108
109    private ArrayList<Scheme> schemes;
110
111    private int scheme = 0; // Green on Black
112
113    private int fontSize = 12;
114
115    private int fontStyle = Font.PLAIN;
116
117    private final String fontFamily = "Monospaced";  // NOI18N
118
119    public static final int WRAP_STYLE_NONE = 0x00;
120    public static final int WRAP_STYLE_LINE = 0x01;
121    public static final int WRAP_STYLE_WORD = 0x02;
122
123    private int wrapStyle = WRAP_STYLE_WORD;
124
125    private UserPreferencesManager pref;
126
127    private JCheckBox autoScroll;
128    private JCheckBox alwaysOnTop;
129
130    private final String alwaysScrollCheck = this.getClass().getName() + ".alwaysScroll"; // NOI18N
131    private final String alwaysOnTopCheck = this.getClass().getName() + ".alwaysOnTop";   // NOI18N
132
133    final public int MAX_CONSOLE_LINES = 5000;  // public, not static so can be modified via a script
134
135    /**
136     * Initialise the system console ensuring both System.out and System.err
137     * streams are re-directed to the consoles JTextArea
138     */
139
140    @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING",
141            justification = "Can only be called from the same instance so default encoding OK")
142    private SystemConsole() {
143        // Record current System.out and System.err
144        // so that we can still send to them
145        originalOut = System.out;
146        originalErr = System.err;
147
148        // Create the console text area
149        console = new TextAreaFIFO(MAX_CONSOLE_LINES);
150
151        // Setup the console text area
152        console.setRows(20);
153        console.setColumns(120);
154        console.setFont(new Font(fontFamily, fontStyle, fontSize));
155        console.setEditable(false);
156        setScheme(scheme);
157        setWrapStyle(wrapStyle);
158
159        this.outputStream = new PrintStream(outStream(STD_OUT), true);
160        this.errorStream = new PrintStream(outStream(STD_ERR), true);
161
162        // Then redirect to it
163        redirectSystemStreams(outputStream, errorStream);
164    }
165
166    /**
167     * Return the JFrame containing the console
168     *
169     * @return console JFrame
170     */
171    public static JFrame getConsole() {
172        return SystemConsole.getInstance().getFrame();
173    }
174
175    public JFrame getFrame() {
176
177        // Check if we've created the frame and do so if not
178        if (frame == null) {
179            log.debug("Creating frame for console");
180            // To avoid possible locks, frame layout should be
181            // performed on the Swing thread
182            if (SwingUtilities.isEventDispatchThread()) {
183                createFrame();
184            } else {
185                try {
186                    // Use invokeAndWait method as we don't want to
187                    // return until the frame layout is completed
188                    SwingUtilities.invokeAndWait(this::createFrame);
189                } catch (InterruptedException | InvocationTargetException ex) {
190                    log.error("Exception creating system console frame", ex);
191                }
192            }
193            log.debug("Frame created");
194        }
195
196        return frame;
197    }
198
199    /**
200     * Layout the console frame
201     */
202    private void createFrame() {
203        // Use a JmriJFrame to ensure that we fit on the screen
204        frame = new JmriJFrame(Bundle.getMessage("TitleConsole"));
205
206        pref = jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class);
207
208        // Add Help menu (Windows menu automaitically added)
209        frame.addHelpMenu("package.apps.SystemConsole", true); // NOI18N
210
211        // Grab a reference to the system clipboard
212        final Clipboard clipboard = frame.getToolkit().getSystemClipboard();
213
214        // Setup the scroll pane
215        JScrollPane scroll = new JScrollPane(console);
216        frame.add(scroll, BorderLayout.CENTER);
217
218
219        JPanel p = new JPanel();
220
221        // Add button to clear display
222        JButton clear = new JButton(Bundle.getMessage("ButtonClear"));
223        clear.addActionListener((ActionEvent event) -> {
224            console.setText("");
225        });
226        clear.setToolTipText(Bundle.getMessage("ButtonClearTip"));
227        p.add(clear);
228
229        // Add button to allow copy to clipboard
230        JButton copy = new JButton(Bundle.getMessage("ButtonCopyClip"));
231        copy.addActionListener((ActionEvent event) -> {
232            StringSelection text = new StringSelection(console.getText());
233            clipboard.setContents(text, text);
234        });
235        p.add(copy);
236
237        // Add button to allow console window to be closed
238        JButton close = new JButton(Bundle.getMessage("ButtonClose"));
239        close.addActionListener((ActionEvent event) -> {
240            frame.setVisible(false);
241            console.dispose();
242            frame.dispose();
243        });
244        p.add(close);
245
246        JButton stackTrace = new JButton(Bundle.getMessage("ButtonStackTrace"));
247        stackTrace.addActionListener((ActionEvent event) -> {
248            performStackTrace();
249        });
250        p.add(stackTrace);
251
252        // Add checkbox to enable/disable auto-scrolling
253        // Use the inverted SimplePreferenceState to default as enabled
254        p.add(autoScroll = new JCheckBox(Bundle.getMessage("CheckBoxAutoScroll"),
255                !pref.getSimplePreferenceState(alwaysScrollCheck)));
256        console.setAutoScroll(autoScroll.isSelected());
257        autoScroll.addActionListener((ActionEvent event) -> {
258            console.setAutoScroll(autoScroll.isSelected());
259            pref.setSimplePreferenceState(alwaysScrollCheck, !autoScroll.isSelected());
260        });
261
262        // Add checkbox to enable/disable always on top
263        p.add(alwaysOnTop = new JCheckBox(Bundle.getMessage("CheckBoxOnTop"),
264                pref.getSimplePreferenceState(alwaysOnTopCheck)));
265        alwaysOnTop.setVisible(true);
266        alwaysOnTop.setToolTipText(Bundle.getMessage("ToolTipOnTop"));
267        alwaysOnTop.addActionListener((ActionEvent event) -> {
268            frame.setAlwaysOnTop(alwaysOnTop.isSelected());
269            pref.setSimplePreferenceState(alwaysOnTopCheck, alwaysOnTop.isSelected());
270        });
271
272        frame.setAlwaysOnTop(alwaysOnTop.isSelected());
273
274        // Define the pop-up menu
275        copySelection = new JMenuItem(Bundle.getMessage("MenuItemCopy"));
276        copySelection.addActionListener((ActionEvent event) -> {
277            StringSelection text = new StringSelection(console.getSelectedText());
278            clipboard.setContents(text, text);
279        });
280        popup.add(copySelection);
281
282        JMenuItem menuItem = new JMenuItem(Bundle.getMessage("ButtonCopyClip"));
283        menuItem.addActionListener((ActionEvent event) -> {
284            StringSelection text = new StringSelection(console.getText());
285            clipboard.setContents(text, text);
286        });
287        popup.add(menuItem);
288
289        popup.add(new JSeparator());
290
291        JRadioButtonMenuItem rbMenuItem;
292
293        // Define the colour scheme sub-menu
294        schemeMenu = new JMenu(rbc.getString("ConsoleSchemeMenu"));
295        schemeGroup = new ButtonGroup();
296        for (final Scheme s : schemes) {
297            rbMenuItem = new JRadioButtonMenuItem(s.description);
298            rbMenuItem.addActionListener((ActionEvent event) -> {
299                setScheme(schemes.indexOf(s));
300            });
301            rbMenuItem.setSelected(getScheme() == schemes.indexOf(s));
302            schemeMenu.add(rbMenuItem);
303            schemeGroup.add(rbMenuItem);
304        }
305        popup.add(schemeMenu);
306
307        // Define the wrap style sub-menu
308        wrapMenu = new JMenu(rbc.getString("ConsoleWrapStyleMenu"));
309        wrapGroup = new ButtonGroup();
310        rbMenuItem = new JRadioButtonMenuItem(rbc.getString("ConsoleWrapStyleNone"));
311        rbMenuItem.addActionListener((ActionEvent event) -> {
312            setWrapStyle(WRAP_STYLE_NONE);
313        });
314        rbMenuItem.setSelected(getWrapStyle() == WRAP_STYLE_NONE);
315        wrapMenu.add(rbMenuItem);
316        wrapGroup.add(rbMenuItem);
317
318        rbMenuItem = new JRadioButtonMenuItem(rbc.getString("ConsoleWrapStyleLine"));
319        rbMenuItem.addActionListener((ActionEvent event) -> {
320            setWrapStyle(WRAP_STYLE_LINE);
321        });
322        rbMenuItem.setSelected(getWrapStyle() == WRAP_STYLE_LINE);
323        wrapMenu.add(rbMenuItem);
324        wrapGroup.add(rbMenuItem);
325
326        rbMenuItem = new JRadioButtonMenuItem(rbc.getString("ConsoleWrapStyleWord"));
327        rbMenuItem.addActionListener((ActionEvent event) -> {
328            setWrapStyle(WRAP_STYLE_WORD);
329        });
330        rbMenuItem.setSelected(getWrapStyle() == WRAP_STYLE_WORD);
331        wrapMenu.add(rbMenuItem);
332        wrapGroup.add(rbMenuItem);
333
334        popup.add(wrapMenu);
335
336        // Bind pop-up to objects
337        MouseListener popupListener = new PopupListener();
338        console.addMouseListener(popupListener);
339        frame.addMouseListener(popupListener);
340
341        // Add the button panel to the frame & then arrange everything
342        frame.add(p, BorderLayout.SOUTH);
343        frame.pack();
344    }
345
346    /**
347     * Add text to the console
348     *
349     * @param text  the text to add
350     * @param which the stream that this text is for
351     */
352    private void updateTextArea(final String text, final int which) {
353        // Append message to the original System.out / System.err streams
354        if (which == STD_OUT) {
355            originalOut.append(text);
356        } else if (which == STD_ERR) {
357            originalErr.append(text);
358        }
359
360        // Now append to the JTextArea
361        SwingUtilities.invokeLater(() -> {
362            synchronized (SystemConsole.this) {
363                console.append(text);            }
364        });
365
366    }
367
368    /**
369     * Creates a new OutputStream for the specified stream
370     *
371     * @param which the stream, either STD_OUT or STD_ERR
372     * @return the new OutputStream
373     */
374    private OutputStream outStream(final int which) {
375        return new OutputStream() {
376            @Override
377            public void write(int b) throws IOException {
378                updateTextArea(String.valueOf((char) b), which);
379            }
380
381            @Override
382            @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING",
383                    justification = "Can only be called from the same instance so default encoding OK")
384            public void write(byte[] b, int off, int len) throws IOException {
385                updateTextArea(new String(b, off, len), which);
386            }
387
388            @Override
389            public void write(byte[] b) throws IOException {
390                write(b, 0, b.length);
391            }
392        };
393    }
394
395    /**
396     * Method to redirect the system streams to the console
397     */
398    @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING",
399            justification = "Can only be called from the same instance so default encoding OK")
400    private void redirectSystemStreams(PrintStream out, PrintStream err) {
401        System.setOut(out);
402        System.setErr(err);
403    }
404
405    /**
406     * Set the console wrapping style to one of the following:
407     *
408     * @param style one of the defined style attributes - one of
409     * <ul>
410     * <li>{@link #WRAP_STYLE_NONE} No wrapping
411     * <li>{@link #WRAP_STYLE_LINE} Wrap at end of line
412     * <li>{@link #WRAP_STYLE_WORD} Wrap by word boundaries
413     * </ul>
414     */
415    public void setWrapStyle(int style) {
416        wrapStyle = style;
417        console.setLineWrap(style != WRAP_STYLE_NONE);
418        console.setWrapStyleWord(style == WRAP_STYLE_WORD);
419
420        if (wrapGroup != null) {
421            wrapGroup.setSelected(wrapMenu.getItem(style).getModel(), true);
422        }
423    }
424
425    /**
426     * Retrieve the current console wrapping style
427     *
428     * @return current wrapping style - one of
429     * <ul>
430     * <li>{@link #WRAP_STYLE_NONE} No wrapping
431     * <li>{@link #WRAP_STYLE_LINE} Wrap at end of line
432     * <li>{@link #WRAP_STYLE_WORD} Wrap by word boundaries (default)
433     * </ul>
434     */
435    public int getWrapStyle() {
436        return wrapStyle;
437    }
438
439    /**
440     * Set the console font size
441     *
442     * @param size point size of font between 6 and 24 point
443     */
444    public void setFontSize(int size) {
445        updateFont(fontFamily, fontStyle, (fontSize = size < 6 ? 6 : size > 24 ? 24 : size));
446    }
447
448    /**
449     * Retrieve the current console font size (default 12 point)
450     *
451     * @return selected font size in points
452     */
453    public int getFontSize() {
454        return fontSize;
455    }
456
457    /**
458     * Set the console font style
459     *
460     * @param style one of
461     *              {@link Font#BOLD}, {@link Font#ITALIC}, {@link Font#PLAIN}
462     *              (default)
463     */
464    public void setFontStyle(int style) {
465
466        if (style == Font.BOLD || style == Font.ITALIC || style == Font.PLAIN || style == (Font.BOLD | Font.ITALIC)) {
467            fontStyle = style;
468        } else {
469            fontStyle = Font.PLAIN;
470        }
471        updateFont(fontFamily, fontStyle, fontSize);
472    }
473
474    /**
475     * Retrieve the current console font style
476     *
477     * @return selected font style - one of
478     *         {@link Font#BOLD}, {@link Font#ITALIC}, {@link Font#PLAIN}
479     *         (default)
480     */
481    public int getFontStyle() {
482        return fontStyle;
483    }
484
485    /**
486     * Update the system console font with the specified parameters
487     *
488     * @param style font style
489     * @param size  font size
490     */
491    private void updateFont(String family, int style, int size) {
492        console.setFont(new Font(family, style, size));
493    }
494
495    /**
496     * Method to define console colour schemes
497     */
498    private void defineSchemes() {
499        schemes = new ArrayList<>();
500        schemes.add(new Scheme(rbc.getString("ConsoleSchemeGreenOnBlack"), Color.GREEN, Color.BLACK));
501        schemes.add(new Scheme(rbc.getString("ConsoleSchemeOrangeOnBlack"), Color.ORANGE, Color.BLACK));
502        schemes.add(new Scheme(rbc.getString("ConsoleSchemeWhiteOnBlack"), Color.WHITE, Color.BLACK));
503        schemes.add(new Scheme(rbc.getString("ConsoleSchemeBlackOnWhite"), Color.BLACK, Color.WHITE));
504        schemes.add(new Scheme(rbc.getString("ConsoleSchemeWhiteOnBlue"), Color.WHITE, Color.BLUE));
505        schemes.add(new Scheme(rbc.getString("ConsoleSchemeBlackOnLightGray"), Color.BLACK, Color.LIGHT_GRAY));
506        schemes.add(new Scheme(rbc.getString("ConsoleSchemeBlackOnGray"), Color.BLACK, Color.GRAY));
507        schemes.add(new Scheme(rbc.getString("ConsoleSchemeWhiteOnGray"), Color.WHITE, Color.GRAY));
508        schemes.add(new Scheme(rbc.getString("ConsoleSchemeWhiteOnDarkGray"), Color.WHITE, Color.DARK_GRAY));
509        schemes.add(new Scheme(rbc.getString("ConsoleSchemeGreenOnDarkGray"), Color.GREEN, Color.DARK_GRAY));
510        schemes.add(new Scheme(rbc.getString("ConsoleSchemeOrangeOnDarkGray"), Color.ORANGE, Color.DARK_GRAY));
511    }
512
513    private Map<Thread, StackTraceElement[]> traces;
514
515    @SuppressWarnings("deprecation")    // The method getId() from the type Thread is deprecated since version 19
516                                        // The replacement Thread.threadId() isn't available before version 19
517    private void performStackTrace() {
518        System.out.println("----------- Begin Stack Trace -----------"); //NO18N
519        System.out.println("-----------------------------------------"); //NO18N
520        traces = new HashMap<>(Thread.getAllStackTraces());
521        for (Thread thread : traces.keySet()) {
522            System.out.println("[" + thread.getId() + "] " + thread.getName());
523            for (StackTraceElement el : thread.getStackTrace()) {
524                System.out.println("  " + el);
525            }
526            System.out.println("-----------------------------------------"); //NO18N
527        }
528        System.out.println("-----------  End Stack Trace  -----------"); //NO18N
529    }
530
531    /**
532     * Set the console colour scheme
533     *
534     * @param which the scheme to use
535     */
536    public void setScheme(int which) {
537        scheme = which;
538
539        if (schemes == null) {
540            defineSchemes();
541        }
542
543        Scheme s;
544
545        try {
546            s = schemes.get(which);
547        } catch (IndexOutOfBoundsException ex) {
548            s = schemes.get(0);
549            scheme = 0;
550        }
551
552        console.setForeground(s.foreground);
553        console.setBackground(s.background);
554
555        if (schemeGroup != null) {
556            schemeGroup.setSelected(schemeMenu.getItem(scheme).getModel(), true);
557        }
558    }
559
560    public PrintStream getOutputStream() {
561        return this.outputStream;
562    }
563
564    public PrintStream getErrorStream() {
565        return this.errorStream;
566    }
567
568    /**
569     * Stop logging System output and error streams to the console.
570     */
571    public void close() {
572        redirectSystemStreams(originalOut, originalErr);
573    }
574
575    /**
576     * Start logging System output and error streams to the console.
577     */
578    public void open() {
579        redirectSystemStreams(getOutputStream(), getErrorStream());
580    }
581
582    /**
583     * Retrieve the current console colour scheme
584     *
585     * @return selected colour scheme
586     */
587    public int getScheme() {
588        return scheme;
589    }
590
591    public Scheme[] getSchemes() {
592        return this.schemes.toArray(new Scheme[this.schemes.size()]);
593    }
594
595    /**
596     * Class holding details of each scheme
597     */
598    public static final class Scheme {
599
600        public Color foreground;
601        public Color background;
602        public String description;
603
604        Scheme(String description, Color foreground, Color background) {
605            this.foreground = foreground;
606            this.background = background;
607            this.description = description;
608        }
609    }
610
611    /**
612     * Class to deal with handling popup menu
613     */
614    public final class PopupListener extends MouseAdapter {
615
616        @Override
617        public void mousePressed(MouseEvent e) {
618            maybeShowPopup(e);
619        }
620
621        @Override
622        public void mouseReleased(MouseEvent e) {
623            maybeShowPopup(e);
624        }
625
626        private void maybeShowPopup(MouseEvent e) {
627            if (e.isPopupTrigger()) {
628                copySelection.setEnabled(console.getSelectionStart() != console.getSelectionEnd());
629                popup.show(e.getComponent(), e.getX(), e.getY());
630            }
631        }
632    }
633
634    private static final Logger log = LoggerFactory.getLogger(SystemConsole.class);
635
636}