LogArea.java
001 /*
002  * LogArea.java
003  
004  * Copyright (c) 1995-2013, The University of Sheffield. See the file
005  * COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
006  
007  * This file is part of GATE (see http://gate.ac.uk/), and is free software,
008  * licenced under the GNU Library General Public License, Version 2, June 1991
009  * (in the distribution as file licence.html, and also available at
010  * http://gate.ac.uk/gate/licence.html).
011  
012  * Cristian URSU, 26/03/2001
013  
014  * $Id: LogArea.java 17606 2014-03-09 12:12:49Z markagreenwood $
015  */
016 
017 package gate.gui;
018 
019 import gate.Gate;
020 import gate.swing.XJFileChooser;
021 import gate.swing.XJTextPane;
022 import gate.util.Err;
023 import gate.util.ExtensionFileFilter;
024 import gate.util.OptionsMap;
025 import gate.util.Out;
026 
027 import java.awt.AlphaComposite;
028 import java.awt.BorderLayout;
029 import java.awt.Color;
030 import java.awt.Graphics2D;
031 import java.awt.event.ActionEvent;
032 import java.awt.event.ActionListener;
033 import java.awt.event.MouseAdapter;
034 import java.awt.event.MouseEvent;
035 import java.awt.image.BufferedImage;
036 import java.io.BufferedWriter;
037 import java.io.File;
038 import java.io.FileWriter;
039 import java.io.IOException;
040 import java.io.OutputStream;
041 import java.io.OutputStreamWriter;
042 import java.io.PrintStream;
043 import java.io.PrintWriter;
044 import java.io.UnsupportedEncodingException;
045 
046 import javax.swing.AbstractAction;
047 import javax.swing.Box;
048 import javax.swing.Icon;
049 import javax.swing.ImageIcon;
050 import javax.swing.JButton;
051 import javax.swing.JCheckBox;
052 import javax.swing.JComponent;
053 import javax.swing.JFileChooser;
054 import javax.swing.JPanel;
055 import javax.swing.JPopupMenu;
056 import javax.swing.JScrollPane;
057 import javax.swing.JSpinner;
058 import javax.swing.JTextField;
059 import javax.swing.JToggleButton;
060 import javax.swing.JToolBar;
061 import javax.swing.SpinnerNumberModel;
062 import javax.swing.SwingUtilities;
063 import javax.swing.event.ChangeEvent;
064 import javax.swing.event.ChangeListener;
065 import javax.swing.text.BadLocationException;
066 import javax.swing.text.DefaultCaret;
067 import javax.swing.text.DefaultStyledDocument;
068 import javax.swing.text.Document;
069 import javax.swing.text.Position;
070 import javax.swing.text.Style;
071 import javax.swing.text.StyleConstants;
072 import javax.swing.text.StyledDocument;
073 
074 /**
075  * This class is used to log all messages from GATE. When an object of this
076  * class is created, it redirects the output of {@link gate.util.Out} &
077  {@link gate.util.Err}. The output from Err is written with <font
078  * color="red">red</font> and the one from Out is written in <b>black</b>.
079  */
080 @SuppressWarnings("serial")
081 public class LogArea extends XJTextPane {
082 
083   /** Field needed in inner classes */
084   protected LogArea thisLogArea = null;
085 
086   /** The popup menu with various actions */
087   protected JPopupMenu popup = null;
088 
089   /** Start position from the document. */
090   protected Position startPos;
091 
092   /** End position from the document. */
093   protected Position endPos;
094 
095   /** The original printstream on System.out */
096   protected PrintStream originalOut;
097 
098   /** The original printstream on System.err */
099   protected PrintStream originalErr;
100 
101   /** This fields defines the Select all behaviour */
102   protected SelectAllAction selectAllAction = null;
103 
104   /** This fields defines the copy behaviour */
105   protected CopyAction copyAction = null;
106 
107   /** This fields defines the clear all behaviour */
108   protected ClearAllAction clearAllAction = null;
109 
110   /**
111    * The component actually used in the GUI which includes other things not just
112    * the text area
113    */
114   private JComponent logTab = null;
115 
116   private JToggleButton btnScrollLock = null;
117 
118   private SpinnerNumberModel logSizeModel = null;
119 
120   private JCheckBox cboLogSize, cboAppend;
121 
122   private OptionsMap userConfig = Gate.getUserConfig();
123 
124   private PrintWriter logFileWriter = null;
125   
126   private File logFile;
127 
128   /**
129    * Constructs a LogArea object and captures the output from Err and Out. The
130    * output from System.out & System.err is not captured.
131    */
132   public LogArea() {
133     thisLogArea = this;
134     this.setEditable(false);
135 
136     DefaultCaret caret = new DefaultCaret();
137     caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
138     this.setCaret(caret);
139 
140     LogAreaOutputStream err = new LogAreaOutputStream(true);
141     LogAreaOutputStream out = new LogAreaOutputStream(false);
142 
143     // Redirecting Err
144     try {
145       Err.setPrintWriter(new UTF8PrintWriter(err, true));
146     catch(UnsupportedEncodingException uee) {
147       uee.printStackTrace();
148     }
149     // Redirecting Out
150     try {
151       Out.setPrintWriter(new UTF8PrintWriter(out, true));
152     catch(UnsupportedEncodingException uee) {
153       uee.printStackTrace();
154     }
155 
156     // Redirecting System.out
157     originalOut = System.out;
158     try {
159       System.setOut(new UTF8PrintStream(out, true));
160     catch(UnsupportedEncodingException uee) {
161       uee.printStackTrace();
162     }
163 
164     // Redirecting System.err
165     originalErr = System.err;
166     try {
167       System.setErr(new UTF8PrintStream(err, true));
168     catch(UnsupportedEncodingException uee) {
169       uee.printStackTrace(originalErr);
170     }
171     popup = new JPopupMenu();
172     selectAllAction = new SelectAllAction();
173     copyAction = new CopyAction();
174     clearAllAction = new ClearAllAction();
175     startPos = getDocument().getStartPosition();
176     endPos = getDocument().getEndPosition();
177 
178     popup.add(selectAllAction);
179     popup.add(copyAction);
180     popup.addSeparator();
181     popup.add(clearAllAction);
182     initListeners();
183   }// LogArea
184 
185   public JComponent getComponentToDisplay() {
186     // don't build the display more than once
187     if(logTab != nullreturn logTab;
188 
189     // create the main panel we will return so we can add things to it
190     logTab = new JPanel(new BorderLayout());
191 
192     JToolBar toolbar = new JToolBar(JToolBar.HORIZONTAL);
193     toolbar.setFloatable(false);
194 
195     JButton btnClear = new JButton(MainFrame.getIcon("ClearLog"));
196     btnClear.setToolTipText("Clear Log");
197     btnClear.addActionListener(clearAllAction);
198 
199     btnScrollLock =
200         new JToggleButton(MainFrame.getIcon("ScrollLock"),
201             userConfig.getBoolean("ScrollLock", Boolean.FALSE));
202     btnScrollLock.setToolTipText("Scroll Lock");
203     btnScrollLock.addActionListener(new ActionListener() {
204 
205       @Override
206       public void actionPerformed(ActionEvent e) {
207         userConfig.put("ScrollLock", btnScrollLock.isSelected());
208       }
209     });
210 
211     logSizeModel =
212         new SpinnerNumberModel(userConfig.getInt("LogSize"80000).intValue(),
213             0, Integer.MAX_VALUE, 1);
214     logSizeModel.addChangeListener(new ChangeListener() {
215 
216       @Override
217       public void stateChanged(ChangeEvent arg0) {
218         userConfig.put("LogSize", logSizeModel.getValue());
219       }
220     });
221 
222     final JSpinner spinLogSize = new JSpinner(logSizeModel);
223     if(spinLogSize.getEditor() instanceof JSpinner.DefaultEditor) {
224       JTextField textField =
225           ((JSpinner.DefaultEditor)spinLogSize.getEditor()).getTextField();
226       textField.setColumns(5);
227     }
228 
229     cboLogSize =
230         new JCheckBox("Max Log Size (chars)", userConfig.getBoolean(
231             "LimitLogSize", Boolean.TRUE));
232     cboLogSize.setOpaque(false);
233     spinLogSize.setEnabled(cboLogSize.isSelected());
234     cboLogSize.addActionListener(new ActionListener() {
235 
236       @Override
237       public void actionPerformed(ActionEvent arg0) {
238         spinLogSize.setEnabled(cboLogSize.isSelected());
239         userConfig.put("LimitLogSize", cboLogSize.isSelected());
240       }
241     });
242 
243     toolbar.add(cboLogSize);
244     toolbar.add(spinLogSize);
245     toolbar.addSeparator();
246 
247     Icon fileIcon = MainFrame.getIcon("OpenFile");
248     final JButton btnLogFile = new JButton(fileIcon);
249     
250     BufferedImage disabledIcon = new BufferedImage(fileIcon.getIconWidth(), fileIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
251     Graphics2D g2d = disabledIcon.createGraphics();
252     g2d.setComposite(AlphaComposite.SrcOver.derive(0.1f));
253     fileIcon.paintIcon(null, g2d, 00);
254     btnLogFile.setDisabledIcon(new ImageIcon(disabledIcon));
255     
256     btnLogFile.setToolTipText("Select Log File");
257     btnLogFile.setEnabled(userConfig.getBoolean("LogToFile", Boolean.FALSE));
258     
259     final JTextField txtLogFile = new JTextField(20);
260 
261     logFile = userConfig.getFile("LogFile");
262     if(logFile != null) {
263       try {
264         if (btnLogFile.isEnabled()) logFileWriter = new PrintWriter(new FileWriter(logFile, true));
265         txtLogFile.setText(logFile.getAbsolutePath());
266       catch(IOException ioe) {
267         logFile = null;
268       }
269     }    
270     
271     btnLogFile.addActionListener(new ActionListener() {
272 
273       @Override
274       public void actionPerformed(ActionEvent arg0) {
275         XJFileChooser fileChooser = MainFrame.getFileChooser();
276         ExtensionFileFilter filter =
277             new ExtensionFileFilter("Log Files (*.txt)""txt");
278         fileChooser.addChoosableFileFilter(filter);
279         fileChooser.setMultiSelectionEnabled(false);
280         fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
281         fileChooser.setDialogTitle("Log File");
282 
283         if(fileChooser.showSaveDialog(MainFrame.getInstance()) != JFileChooser.APPROVE_OPTION)
284           return;
285 
286         File logFile = fileChooser.getSelectedFile();
287         if(logFile == nullreturn;
288 
289         try {
290           logFileWriter = new PrintWriter(new FileWriter(logFile, true));
291           userConfig.put("LogFile", logFile);
292           txtLogFile.setText(logFile.getAbsolutePath());
293         catch(IOException ioe) {
294           logFile = null;
295           logFileWriter = null;
296           txtLogFile.setText("");
297           userConfig.remove("LogFile");
298           ioe.printStackTrace();
299         }
300       }
301     });
302 
303     txtLogFile.setEditable(false);
304     txtLogFile.setEnabled(btnLogFile.isEnabled());
305 
306     cboAppend = new JCheckBox("Append To", btnLogFile.isEnabled());
307     cboAppend.setOpaque(false);
308     cboAppend.addActionListener(new ActionListener() {
309 
310       @Override
311       public void actionPerformed(ActionEvent arg0) {
312         try {
313           logFileWriter = cboAppend.isSelected() && logFile != null new PrintWriter(new FileWriter(logFile, true)) null;
314           btnLogFile.setEnabled(cboAppend.isSelected());
315           txtLogFile.setEnabled(cboAppend.isSelected());
316           userConfig.put("LogToFile", cboAppend.isSelected());
317         }
318         catch (IOException e) {
319           logFile = null;
320           logFileWriter = null;
321           txtLogFile.setText("");
322           userConfig.remove("LogFile");
323           e.printStackTrace();
324         }
325       }
326     });
327 
328     toolbar.add(cboAppend);
329     toolbar.add(txtLogFile);
330     toolbar.add(btnLogFile);
331     toolbar.addSeparator();
332 
333     toolbar.add(Box.createHorizontalGlue());
334     toolbar.add(btnClear);
335     toolbar.add(btnScrollLock);
336 
337     // add the text area as the main component, inside a scroller
338     logTab.add(new JScrollPane(this), BorderLayout.CENTER);
339     logTab.add(toolbar, BorderLayout.SOUTH);
340 
341     // return the display
342     return logTab;
343   }
344 
345   /**
346    * Overridden to fetch new start and end Positions when the document is
347    * changed.
348    */
349   @Override
350   public void setDocument(Document d) {
351     super.setDocument(d);
352     startPos = d.getStartPosition();
353     endPos = d.getEndPosition();
354   }
355 
356   @Override
357   public void setStyledDocument(StyledDocument d) {
358     this.setDocument(d);
359   }
360 
361   /** Init all listeners for this object */
362   @Override
363   public void initListeners() {
364     super.initListeners();
365     this.addMouseListener(new MouseAdapter() {
366       @Override
367       public void mouseClicked(MouseEvent e) {
368         if(SwingUtilities.isRightMouseButton(e)) {
369           popup.show(thisLogArea, e.getPoint().x, e.getPoint().y);
370         }// End if
371       }// end mouseClicked()
372     });// End addMouseListener();
373   }
374 
375   /** Returns the original printstream on System.err */
376   public PrintStream getOriginalErr() {
377     return originalErr;
378   }
379 
380   /** Returns the original printstream on System.out */
381   public PrintStream getOriginalOut() {
382     return originalOut;
383   }// initListeners();
384 
385   /** Inner class that defines the behaviour of SelectAll action. */
386   protected class SelectAllAction extends AbstractAction {
387     public SelectAllAction() {
388       super("Select all");
389     }// SelectAll
390 
391     @Override
392     public void actionPerformed(ActionEvent e) {
393       thisLogArea.selectAll();
394     }// actionPerformed();
395   }// End class SelectAllAction
396 
397   /** Inner class that defines the behaviour of copy action. */
398   protected class CopyAction extends AbstractAction {
399     public CopyAction() {
400       super("Copy");
401     }// CopyAction
402 
403     @Override
404     public void actionPerformed(ActionEvent e) {
405       thisLogArea.copy();
406     }// actionPerformed();
407   }// End class CopyAction
408 
409   /**
410    * A runnable that adds a bit of text to the area; needed so we can write from
411    * the Swing thread.
412    */
413   protected class SwingWriter implements Runnable {
414     SwingWriter(String text, Style style) {
415       this.text = text;
416       this.style = style;
417     }
418 
419     @Override
420     public void run() {
421 
422       if(cboAppend.isSelected() && logFileWriter != null) {
423         // if logging to a file is enabled then do the logging
424         logFileWriter.print(text);
425         logFileWriter.flush();
426       }
427 
428       try {
429         // endPos is always one past the real end position because of the
430         // implicit newline character at the end of any Document
431         getDocument().insertString(endPos.getOffset() 1, text, style);
432 
433         if(cboLogSize.isSelected()
434             && getDocument().getLength() > logSizeModel.getNumber().intValue()) {
435           // if the document is now over the buffer size then trim it roughly to
436           // length by finding the first new line within the valid buffer size
437           // and cutting there or if there is no new line then just trim to
438           // length
439           int index =
440               getText().indexOf(
441                   "\n",
442                   getDocument().getLength()
443                       - logSizeModel.getNumber().intValue()) 1;
444           getDocument().remove(
445               0,
446               index != ? index : getDocument().getLength()
447                   - logSizeModel.getNumber().intValue());
448         }
449 
450         if(!btnScrollLock.isSelected()) {
451           setCaretPosition(getDocument().getLength());
452         }
453       catch(BadLocationException e) {
454         // a BLE here is a real problem
455         handleBadLocationException(e, text, style);
456       }// End try
457     }
458 
459     String text;
460 
461     Style style;
462   }
463 
464   /**
465    * Try and recover from a BadLocationException thrown when inserting a string
466    * into the log area. This method must only be called on the AWT event
467    * handling thread.
468    */
469   private void handleBadLocationException(BadLocationException e,
470       String textToInsert, Style style) {
471     originalErr.println("BadLocationException encountered when writing to "
472         "the log area: " + e);
473     originalErr.println("trying to recover...");
474 
475     Document newDocument = new DefaultStyledDocument();
476     try {
477       StringBuilder sb = new StringBuilder();
478       sb.append("An error occurred when trying to write a message to the log area.  The log\n");
479       sb.append("has been cleared to try and recover from this problem.\n\n");
480       sb.append(textToInsert);
481 
482       newDocument.insertString(0, sb.toString(), style);
483     catch(BadLocationException e2) {
484       // oh dear, all bets are off now...
485       e2.printStackTrace(originalErr);
486       return;
487     }
488     // replace the log area's document with the new one
489     setDocument(newDocument);
490   }
491 
492   /**
493    * A print writer that uses UTF-8 to convert from char[] to byte[]
494    */
495   public static class UTF8PrintWriter extends PrintWriter {
496     public UTF8PrintWriter(OutputStream out)
497         throws UnsupportedEncodingException {
498       this(out, true);
499     }
500 
501     public UTF8PrintWriter(OutputStream out, boolean autoFlush)
502         throws UnsupportedEncodingException {
503       super(new BufferedWriter(new OutputStreamWriter(out, "UTF-8")), autoFlush);
504     }
505   }
506 
507   /**
508    * A print writer that uses UTF-8 to convert from char[] to byte[]
509    */
510   public static class UTF8PrintStream extends PrintStream {
511     public UTF8PrintStream(OutputStream out)
512         throws UnsupportedEncodingException {
513       this(out, true);
514     }
515 
516     public UTF8PrintStream(OutputStream out, boolean autoFlush)
517         throws UnsupportedEncodingException {
518       super(out, autoFlush);
519     }
520 
521     /**
522      * Overriden so it uses UTF-8 when converting a string to byte[]
523      
524      @param s
525      *          the string to be printed
526      */
527     @Override
528     public void print(String s) {
529       try {
530         write((s == null "null" : s).getBytes("UTF-8"));
531       catch(UnsupportedEncodingException uee) {
532         // support for UTF-8 is guaranteed by the JVM specification
533       catch(IOException ioe) {
534         // print streams don't throw exceptions
535         setError();
536       }
537     }
538 
539     /**
540      * Overriden so it uses UTF-8 when converting a char[] to byte[]
541      
542      @param s
543      *          the string to be printed
544      */
545     @Override
546     public void print(char s[]) {
547       print(String.valueOf(s));
548     }
549   }
550 
551   /** Inner class that defines the behaviour of clear all action. */
552   protected class ClearAllAction extends AbstractAction {
553     public ClearAllAction() {
554       super("Clear all");
555     }// ClearAllAction
556 
557     @Override
558     public void actionPerformed(ActionEvent e) {
559       try {
560         thisLogArea.getDocument().remove(startPos.getOffset(),
561             endPos.getOffset() - startPos.getOffset() 1);
562       catch(BadLocationException e1) {
563         // it's OK to print this exception to the current log area
564         e1.printStackTrace(Err.getPrintWriter());
565       }// End try
566     }// actionPerformed();
567   }// End class ClearAllAction
568 
569   /**
570    * Inner class that defines the behaviour of an OutputStream that writes to
571    * the LogArea.
572    */
573   class LogAreaOutputStream extends OutputStream {
574     /** This field dictates the style on how to write */
575     private boolean isErr = false;
576 
577     /** Char style */
578     private Style style = null;
579 
580     /** Constructs an Out or Err LogAreaOutputStream */
581     public LogAreaOutputStream(boolean anIsErr) {
582       isErr = anIsErr;
583       if(isErr) {
584         style = addStyle("error", getStyle("default"));
585         StyleConstants.setForeground(style, Color.red);
586       else {
587         style = addStyle("out", getStyle("default"));
588         StyleConstants.setForeground(style, Color.black);
589       }// End if
590     }// LogAreaOutputStream
591 
592     /**
593      * Writes an int which must be a the code of a char, into the LogArea, using
594      * the style specified in constructor. The int is downcast to a byte.
595      */
596     @Override
597     public void write(int charCode) {
598       // charCode int must be a char. Let us be sure of that
599       charCode &= 0x000000FF;
600       // Convert the byte to a char before put it into the log area
601       char c = (char)charCode;
602       // Insert it in the log Area
603       SwingUtilities.invokeLater(new SwingWriter(String.valueOf(c), style));
604     }// write(int charCode)
605 
606     /**
607      * Writes an array of bytes into the LogArea, using the style specified in
608      * constructor.
609      */
610     @Override
611     public void write(byte[] data, int offset, int length) {
612       // Insert the string to the log area
613       try {
614         SwingUtilities.invokeLater(new SwingWriter(new String(data, offset,
615             length, "UTF-8"), style));
616       catch(UnsupportedEncodingException uee) {
617         // should never happen - all JREs are required to support UTF-8
618         uee.printStackTrace(originalErr);
619       }
620     }// write(byte[] data, int offset, int length)
621   }// //End class LogAreaOutputStream
622 }// End class LogArea