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