dc5968de838e17aac6e25e4a910b116ff3ed0807
[jalview.git] / src / jalview / gui / Console.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import java.awt.BorderLayout;
24 import java.awt.Color;
25 import java.awt.Dimension;
26 import java.awt.GraphicsEnvironment;
27 import java.awt.GridBagConstraints;
28 import java.awt.GridBagLayout;
29 import java.awt.Rectangle;
30 import java.awt.Toolkit;
31 import java.awt.datatransfer.Clipboard;
32 import java.awt.datatransfer.StringSelection;
33 import java.awt.event.ActionEvent;
34 import java.awt.event.ActionListener;
35 import java.awt.event.MouseAdapter;
36 import java.awt.event.MouseEvent;
37 import java.awt.event.WindowAdapter;
38 import java.awt.event.WindowEvent;
39 import java.awt.event.WindowListener;
40 import java.io.IOException;
41 import java.io.PipedInputStream;
42 import java.io.PipedOutputStream;
43 import java.io.PrintStream;
44
45 import javax.swing.JButton;
46 import javax.swing.JComboBox;
47 import javax.swing.JFrame;
48 import javax.swing.JLabel;
49 import javax.swing.JPanel;
50 import javax.swing.JScrollPane;
51 import javax.swing.JTextArea;
52
53 import org.apache.log4j.Level;
54 import org.apache.log4j.SimpleLayout;
55
56 import jalview.bin.Cache;
57 import jalview.util.MessageManager;
58
59 /**
60  * Simple Jalview Java Console. Version 1 - allows viewing of console output
61  * after desktop is created. Acquired with thanks from RJHM's site
62  * http://www.comweb.nl/java/Console/Console.html A simple Java Console for your
63  * application (Swing version) Requires Java 1.1.5 or higher Disclaimer the use
64  * of this source is at your own risk. Permision to use and distribute into your
65  * own applications RJHM van den Bergh , rvdb@comweb.nl
66  */
67
68 public class Console extends WindowAdapter
69         implements WindowListener, ActionListener, Runnable
70 {
71   private JFrame frame;
72
73   private JTextArea textArea;
74
75   /*
76    * unused - tally and limit for lines in console window int lines = 0;
77    * 
78    * int lim = 1000;
79    */
80   int byteslim = 102400, bytescut = 76800; // 100k and 75k cut point.
81
82   private Thread reader, reader2, textAppender;
83
84   private boolean quit;
85
86   private final PrintStream stdout = System.out, stderr = System.err;
87
88   private PipedInputStream pin = new PipedInputStream();
89
90   private PipedInputStream pin2 = new PipedInputStream();
91
92   private StringBuffer displayPipe = new StringBuffer();
93
94   Thread errorThrower; // just for testing (Throws an Exception at this Console
95
96   // are we attached to some parent Desktop
97   Desktop parent = null;
98
99   private int MIN_WIDTH = 300;
100
101   private int MIN_HEIGHT = 250;
102
103   private JComboBox logLevelCombo = new JComboBox();
104
105   public Console()
106   {
107     // create all components and add them
108     Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
109     frame = initFrame("Java Console", screenSize.width / 2,
110             screenSize.height / 2, -1, -1);
111     initConsole(true);
112   }
113
114   private void initConsole(boolean visible)
115   {
116     initConsole(visible, true);
117   }
118
119   /**
120    * 
121    * @param visible
122    *          - open the window
123    * @param redirect
124    *          - redirect std*
125    */
126   private void initConsole(boolean visible, boolean redirect)
127   {
128     // CutAndPasteTransfer cpt = new CutAndPasteTransfer();
129     // textArea = cpt.getTextArea();
130     textArea = new JTextArea();
131     textArea.setEditable(false);
132     JButton clearButton = new JButton(
133             MessageManager.getString("action.clear"));
134     JButton copyToClipboardButton = new JButton(
135             MessageManager.getString("label.copy_to_clipboard"));
136     copyToClipboardButton.addActionListener(new ActionListener()
137     {
138       public void actionPerformed(ActionEvent e)
139       {
140         copyConsoleTextToClipboard();
141       }
142     });
143     copyToClipboardButton.addMouseListener(new MouseAdapter()
144     {
145       private Color bg = textArea.getBackground();
146
147       public void mousePressed(MouseEvent e)
148       {
149         textArea.setBackground(textArea.getSelectionColor());
150       }
151
152       public void mouseReleased(MouseEvent e)
153       {
154         textArea.setBackground(bg);
155       }
156
157     });
158     copyToClipboardButton.setToolTipText(
159             MessageManager.getString("label.copy_to_clipboard_tooltip"));
160
161     JLabel logLevelLabel = new JLabel(
162             MessageManager.getString("label.log_level") + ":");
163
164     // logLevelCombo.addItem(Level.ALL);
165     logLevelCombo.addItem(Level.TRACE);
166     logLevelCombo.addItem(Level.DEBUG);
167     logLevelCombo.addItem(Level.INFO);
168     logLevelCombo.addItem(Level.WARN);
169     // logLevelCombo.addItem(Level.ERROR);
170     // logLevelCombo.addItem(Level.FATAL);
171     // logLevelCombo.addItem(Level.OFF);
172     setChosenLogLevelCombo();
173     logLevelCombo.addActionListener(new ActionListener()
174     {
175       public void actionPerformed(ActionEvent e)
176       {
177         if (Cache.log != null)
178         {
179           Cache.log.setLevel((Level) logLevelCombo.getSelectedItem());
180         }
181       }
182
183     });
184
185     // frame = cpt;
186     frame.getContentPane().setLayout(new BorderLayout());
187     frame.getContentPane().add(new JScrollPane(textArea),
188             BorderLayout.CENTER);
189     JPanel southPanel = new JPanel();
190     southPanel.setLayout(new GridBagLayout());
191
192     JPanel logLevelPanel = new JPanel();
193     logLevelPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
194     logLevelPanel.add(logLevelLabel);
195     logLevelPanel.add(logLevelCombo);
196     logLevelLabel.setToolTipText(
197             MessageManager.getString("label.log_level_tooltip"));
198     logLevelCombo.setToolTipText(
199             MessageManager.getString("label.log_level_tooltip"));
200
201     GridBagConstraints gbc = new GridBagConstraints();
202     gbc.gridx = 0;
203     gbc.gridy = 0;
204     gbc.gridwidth = 1;
205     gbc.gridheight = 1;
206     gbc.weightx = 0.1;
207     southPanel.add(logLevelPanel, gbc);
208
209     gbc.gridx++;
210     gbc.weightx = 0.8;
211     gbc.fill = GridBagConstraints.HORIZONTAL;
212     southPanel.add(clearButton, gbc);
213
214     gbc.gridx++;
215     gbc.weightx = 0.1;
216     gbc.fill = GridBagConstraints.NONE;
217     southPanel.add(copyToClipboardButton, gbc);
218
219     southPanel.setVisible(true);
220     frame.getContentPane().add(southPanel, BorderLayout.SOUTH);
221     frame.setVisible(visible);
222     updateConsole = visible;
223     frame.addWindowListener(this);
224     clearButton.addActionListener(this);
225
226     if (redirect)
227     {
228       redirectStreams();
229     }
230     else
231     {
232       unredirectStreams();
233     }
234     quit = false; // signals the Threads that they should exit
235
236     // Starting two seperate threads to read from the PipedInputStreams
237     //
238     reader = new Thread(this);
239     reader.setDaemon(true);
240     reader.start();
241     //
242     reader2 = new Thread(this);
243     reader2.setDaemon(true);
244     reader2.start();
245     // and a thread to append text to the textarea
246     textAppender = new Thread(this);
247     textAppender.setDaemon(true);
248     textAppender.start();
249   }
250
251   private void setChosenLogLevelCombo()
252   {
253     Level currentLogLevel = Cache.log == null ? Level.INFO
254             : Cache.log.getLevel();
255     logLevelCombo.setSelectedItem(currentLogLevel);
256     if (!logLevelCombo.getSelectedItem().equals(currentLogLevel)) // currentLogLevel
257                                                                   // not in list
258     {
259       if (currentLogLevel != null && currentLogLevel instanceof Level)
260       {
261         // add new item to list (might be set via .jalview_properties)
262         boolean added = false;
263         for (int i = 0; i < logLevelCombo.getItemCount(); i++)
264         {
265           Level l = (Level) logLevelCombo.getItemAt(i);
266           if (l.isGreaterOrEqual(currentLogLevel))
267           {
268             logLevelCombo.insertItemAt(currentLogLevel, i);
269             added = true;
270             break;
271           }
272         }
273         if (!added) // lower priority than others or some confusion -- add to
274                     // end of list
275         {
276           logLevelCombo.addItem(currentLogLevel);
277         }
278         logLevelCombo.setSelectedItem(currentLogLevel);
279       }
280       else
281       {
282         logLevelCombo.setSelectedItem(Level.INFO);
283       }
284     }
285   }
286
287   private void copyConsoleTextToClipboard()
288   {
289     String consoleText = textArea.getText();
290     StringSelection consoleTextSelection = new StringSelection(consoleText);
291     Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
292     cb.setContents(consoleTextSelection, null);
293   }
294
295   PipedOutputStream pout = null, perr = null;
296
297   public void redirectStreams()
298   {
299     if (pout == null)
300     {
301       try
302       {
303         pout = new PipedOutputStream(this.pin);
304         System.setOut(new PrintStream(pout, true));
305       } catch (java.io.IOException io)
306       {
307         textArea.append("Couldn't redirect STDOUT to this console\n"
308                 + io.getMessage());
309         io.printStackTrace(stderr);
310       } catch (SecurityException se)
311       {
312         textArea.append("Couldn't redirect STDOUT to this console\n"
313                 + se.getMessage());
314         se.printStackTrace(stderr);
315       }
316
317       try
318       {
319         perr = new PipedOutputStream(this.pin2);
320         System.setErr(new PrintStream(perr, true));
321       } catch (java.io.IOException io)
322       {
323         textArea.append("Couldn't redirect STDERR to this console\n"
324                 + io.getMessage());
325         io.printStackTrace(stderr);
326       } catch (SecurityException se)
327       {
328         textArea.append("Couldn't redirect STDERR to this console\n"
329                 + se.getMessage());
330         se.printStackTrace(stderr);
331       }
332     }
333   }
334
335   public void unredirectStreams()
336   {
337     if (pout != null)
338     {
339       try
340       {
341         System.setOut(stdout);
342         pout.flush();
343         pout.close();
344         pin = new PipedInputStream();
345         pout = null;
346       } catch (java.io.IOException io)
347       {
348         textArea.append("Couldn't unredirect STDOUT to this console\n"
349                 + io.getMessage());
350         io.printStackTrace(stderr);
351       } catch (SecurityException se)
352       {
353         textArea.append("Couldn't unredirect STDOUT to this console\n"
354                 + se.getMessage());
355         se.printStackTrace(stderr);
356       }
357
358       try
359       {
360         System.setErr(stderr);
361         perr.flush();
362         perr.close();
363         pin2 = new PipedInputStream();
364         perr = null;
365       } catch (java.io.IOException io)
366       {
367         textArea.append("Couldn't unredirect STDERR to this console\n"
368                 + io.getMessage());
369         io.printStackTrace(stderr);
370       } catch (SecurityException se)
371       {
372         textArea.append("Couldn't unredirect STDERR to this console\n"
373                 + se.getMessage());
374         se.printStackTrace(stderr);
375       }
376     }
377   }
378
379   public void test()
380   {
381     // testing part
382     // you may omit this part for your application
383     //
384
385     System.out.println("Hello World 2");
386     System.out.println("All fonts available to Graphic2D:\n");
387     GraphicsEnvironment ge = GraphicsEnvironment
388             .getLocalGraphicsEnvironment();
389     String[] fontNames = ge.getAvailableFontFamilyNames();
390     for (int n = 0; n < fontNames.length; n++)
391     {
392       System.out.println(fontNames[n]);
393     }
394     // Testing part: simple an error thrown anywhere in this JVM will be printed
395     // on the Console
396     // We do it with a seperate Thread becasue we don't wan't to break a Thread
397     // used by the Console.
398     System.out.println("\nLets throw an error on this console");
399     errorThrower = new Thread(this);
400     errorThrower.setDaemon(true);
401     errorThrower.start();
402   }
403
404   private JFrame initFrame(String string, int i, int j, int x, int y)
405   {
406     JFrame frame = new JFrame(string);
407     frame.setName(string);
408     if (x == -1)
409     {
410       x = i / 2;
411     }
412     if (y == -1)
413     {
414       y = j / 2;
415     }
416     frame.setBounds(x, y, i, j);
417     return frame;
418   }
419
420   /**
421    * attach a console to the desktop - the desktop will open it if requested.
422    * 
423    * @param desktop
424    */
425   public Console(Desktop desktop)
426   {
427     this(desktop, true);
428   }
429
430   /**
431    * attach a console to the desktop - the desktop will open it if requested.
432    * 
433    * @param desktop
434    * @param showjconsole
435    *          - if true, then redirect stdout immediately
436    */
437   public Console(Desktop desktop, boolean showjconsole)
438   {
439     parent = desktop;
440     // window name - get x,y,width, height possibly scaled
441     Rectangle bounds = desktop.getLastKnownDimensions("JAVA_CONSOLE_");
442     if (bounds == null)
443     {
444       frame = initFrame("Jalview Java Console", desktop.getWidth() / 2,
445               desktop.getHeight() / 4, desktop.getX(), desktop.getY());
446     }
447     else
448     {
449       frame = initFrame("Jalview Java Console", bounds.width, bounds.height,
450               bounds.x, bounds.y);
451     }
452     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
453     // desktop.add(frame);
454     initConsole(false);
455     JalviewAppender jappender = new JalviewAppender();
456     jappender.setLayout(new SimpleLayout());
457     JalviewAppender.setTextArea(textArea);
458     org.apache.log4j.Logger.getRootLogger().addAppender(jappender);
459   }
460
461   public synchronized void stopConsole()
462   {
463     quit = true;
464     this.notifyAll();
465     /*
466      * reader.notify(); reader2.notify(); if (errorThrower!=null)
467      * errorThrower.notify(); // stop all threads if (textAppender!=null)
468      * textAppender.notify();
469      */
470     if (pout != null)
471     {
472       try
473       {
474         reader.join(10);
475         pin.close();
476       } catch (Exception e)
477       {
478       }
479       try
480       {
481         reader2.join(10);
482         pin2.close();
483       } catch (Exception e)
484       {
485       }
486       try
487       {
488         textAppender.join(10);
489       } catch (Exception e)
490       {
491       }
492     }
493     if (!frame.isVisible())
494     {
495       frame.dispose();
496     }
497     // System.exit(0);
498   }
499
500   @Override
501   public synchronized void windowClosed(WindowEvent evt)
502   {
503     frame.setVisible(false);
504     closeConsoleGui();
505   }
506
507   private void closeConsoleGui()
508   {
509     updateConsole = false;
510     if (parent == null)
511     {
512
513       stopConsole();
514     }
515     else
516     {
517       parent.showConsole(false);
518     }
519   }
520
521   @Override
522   public synchronized void windowClosing(WindowEvent evt)
523   {
524     frame.setVisible(false); // default behaviour of JFrame
525     closeConsoleGui();
526
527     // frame.dispose();
528   }
529
530   @Override
531   public synchronized void actionPerformed(ActionEvent evt)
532   {
533     trimBuffer(true);
534     // textArea.setText("");
535   }
536
537   @Override
538   public synchronized void run()
539   {
540     try
541     {
542       while (Thread.currentThread() == reader)
543       {
544         if (pin == null || pin.available() == 0)
545         {
546           try
547           {
548             this.wait(100);
549             if (pin.available() == 0)
550             {
551               trimBuffer(false);
552             }
553           } catch (InterruptedException ie)
554           {
555           }
556         }
557
558         while (pin.available() != 0)
559         {
560           String input = this.readLine(pin);
561           stdout.print(input);
562           long time = System.nanoTime();
563           appendToTextArea(input);
564           // stderr.println("Time taken to stdout append:\t"
565           // + (System.nanoTime() - time) + " ns");
566           // lines++;
567         }
568         if (quit)
569         {
570           return;
571         }
572       }
573
574       while (Thread.currentThread() == reader2)
575       {
576         if (pin2.available() == 0)
577         {
578           try
579           {
580             this.wait(100);
581             if (pin2.available() == 0)
582             {
583               trimBuffer(false);
584             }
585           } catch (InterruptedException ie)
586           {
587           }
588         }
589         while (pin2.available() != 0)
590         {
591           String input = this.readLine(pin2);
592           stderr.print(input);
593           long time = System.nanoTime();
594           appendToTextArea(input);
595           // stderr.println("Time taken to stderr append:\t"
596           // + (System.nanoTime() - time) + " ns");
597           // lines++;
598         }
599         if (quit)
600         {
601           return;
602         }
603       }
604       while (Thread.currentThread() == textAppender)
605       {
606         if (updateConsole)
607         {
608           // check string buffer - if greater than console, clear console and
609           // replace with last segment of content, otherwise, append all to
610           // content.
611           long count;
612           while (displayPipe.length() > 0)
613           {
614             count = 0;
615             StringBuffer tmp = new StringBuffer(), replace;
616             synchronized (displayPipe)
617             {
618               replace = displayPipe;
619               displayPipe = tmp;
620             }
621             // simply append whole buffer
622             textArea.append(replace.toString());
623             count += replace.length();
624             if (count > byteslim)
625             {
626               trimBuffer(false);
627             }
628           }
629           if (displayPipe.length() == 0)
630           {
631             try
632             {
633               this.wait(100);
634               if (displayPipe.length() == 0)
635               {
636                 trimBuffer(false);
637               }
638             } catch (InterruptedException e)
639             {
640             }
641           }
642         }
643         else
644         {
645           try
646           {
647             this.wait(100);
648           } catch (InterruptedException e)
649           {
650
651           }
652         }
653         if (quit)
654         {
655           return;
656         }
657
658       }
659     } catch (Exception e)
660     {
661       textArea.append("\nConsole reports an Internal error.");
662       textArea.append("The error is: " + e.getMessage());
663       // Need to uncomment this to ensure that line tally is synched.
664       // lines += 2;
665       stderr.println(
666               "Console reports an Internal error.\nThe error is: " + e);
667     }
668
669     // just for testing (Throw a Nullpointer after 1 second)
670     if (Thread.currentThread() == errorThrower)
671     {
672       try
673       {
674         this.wait(1000);
675       } catch (InterruptedException ie)
676       {
677       }
678       throw new NullPointerException(
679               MessageManager.getString("exception.application_test_npe"));
680     }
681   }
682
683   private void appendToTextArea(final String input)
684   {
685     if (updateConsole == false)
686     {
687       // do nothing;
688       return;
689     }
690     long time = System.nanoTime();
691     javax.swing.SwingUtilities.invokeLater(new Runnable()
692     {
693       @Override
694       public void run()
695       {
696         displayPipe.append(input); // change to stringBuffer
697         // displayPipe.flush();
698
699       }
700     });
701     // stderr.println("Time taken to Spawnappend:\t" + (System.nanoTime() -
702     // time)
703     // + " ns");
704   }
705
706   private String header = null;
707
708   private boolean updateConsole = false;
709
710   private synchronized void trimBuffer(boolean clear)
711   {
712     if (header == null && textArea.getLineCount() > 5)
713     {
714       try
715       {
716         header = textArea.getText(0, textArea.getLineStartOffset(5))
717                 + "\nTruncated...\n";
718       } catch (Exception e)
719       {
720         e.printStackTrace();
721       }
722     }
723     // trim the buffer
724     int tlength = textArea.getDocument().getLength();
725     if (header != null)
726     {
727       if (clear || (tlength > byteslim))
728       {
729         try
730         {
731           if (!clear)
732           {
733             long time = System.nanoTime();
734             textArea.replaceRange(header, 0, tlength - bytescut);
735             // stderr.println("Time taken to cut:\t"
736             // + (System.nanoTime() - time) + " ns");
737           }
738           else
739           {
740             textArea.setText(header);
741           }
742         } catch (Exception e)
743         {
744           e.printStackTrace();
745         }
746         // lines = textArea.getLineCount();
747       }
748     }
749
750   }
751
752   public synchronized String readLine(PipedInputStream in)
753           throws IOException
754   {
755     String input = "";
756     int lp = -1;
757     do
758     {
759       int available = in.available();
760       if (available == 0)
761       {
762         break;
763       }
764       byte b[] = new byte[available];
765       in.read(b);
766       input = input + new String(b, 0, b.length);
767       // counts lines - we don't do this for speed.
768       // while ((lp = input.indexOf("\n", lp + 1)) > -1)
769       // {
770       // lines++;
771       // }
772     } while (!input.endsWith("\n") && !input.endsWith("\r\n") && !quit);
773     return input;
774   }
775
776   /**
777    * @j2sIgnore
778    * @param arg
779    */
780   public static void main(String[] arg)
781   {
782     new Console().test(); // create console with not reference
783
784   }
785
786   public void setVisible(boolean selected)
787   {
788     frame.setVisible(selected);
789     if (selected == true)
790     {
791       setChosenLogLevelCombo();
792       redirectStreams();
793       updateConsole = true;
794       frame.toFront();
795     }
796     else
797     {
798       // reset log level to user preference
799       if (Cache.log != null)
800       {
801         String userLogLevel = Cache.getDefault("logs.Jalview.level",
802                 Level.INFO.toString());
803         Cache.log.setLevel(Level.toLevel(userLogLevel));
804       }
805
806       unredirectStreams();
807       updateConsole = false;
808     }
809   }
810
811   public Rectangle getBounds()
812   {
813     if (frame != null)
814     {
815       return frame.getBounds();
816     }
817     return null;
818   }
819
820   /**
821    * set the banner that appears at the top of the console output
822    * 
823    * @param string
824    */
825   public void setHeader(String string)
826   {
827     header = string;
828     if (header.charAt(header.length() - 1) != '\n')
829     {
830       header += "\n";
831     }
832     textArea.insert(header, 0);
833   }
834
835   /**
836    * get the banner
837    * 
838    * @return
839    */
840   public String getHeader()
841   {
842     return header;
843   }
844 }