View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2007 eviware.com 
3    *
4    *  soapUI is free software; you can redistribute it and/or modify it under the 
5    *  terms of version 2.1 of the GNU Lesser General Public License as published by 
6    *  the Free Software Foundation.
7    *
8    *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
9    *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
10   *  See the GNU Lesser General Public License for more details at gnu.org.
11   */
12  
13  package com.eviware.soapui.support.log;
14  
15  import java.awt.BorderLayout;
16  import java.awt.Color;
17  import java.awt.Component;
18  import java.awt.Toolkit;
19  import java.awt.datatransfer.Clipboard;
20  import java.awt.datatransfer.StringSelection;
21  import java.awt.event.ActionEvent;
22  import java.io.File;
23  import java.io.PrintWriter;
24  import java.io.StringWriter;
25  import java.lang.reflect.InvocationTargetException;
26  import java.util.ArrayList;
27  import java.util.Date;
28  import java.util.HashMap;
29  import java.util.LinkedList;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Stack;
33  import java.util.StringTokenizer;
34  
35  import javax.swing.AbstractAction;
36  import javax.swing.AbstractListModel;
37  import javax.swing.BorderFactory;
38  import javax.swing.DefaultListCellRenderer;
39  import javax.swing.JCheckBoxMenuItem;
40  import javax.swing.JLabel;
41  import javax.swing.JList;
42  import javax.swing.JPanel;
43  import javax.swing.JPopupMenu;
44  import javax.swing.JScrollPane;
45  import javax.swing.SwingUtilities;
46  import javax.swing.text.SimpleAttributeSet;
47  import javax.swing.text.StyleConstants;
48  
49  import org.apache.log4j.AppenderSkeleton;
50  import org.apache.log4j.Level;
51  import org.apache.log4j.Logger;
52  import org.apache.log4j.spi.LoggingEvent;
53  
54  import com.eviware.soapui.SoapUI;
55  import com.eviware.soapui.support.UISupport;
56  
57  /***
58   * Component for displaying log entries
59   * 
60   * @author Ole.Matzura
61   */
62  
63  public class JLogList extends JPanel
64  {
65  	private long maxRows = 1000;
66  	private JList logList;
67  	private SimpleAttributeSet requestAttributes;
68  	private SimpleAttributeSet responseAttributes;
69  	private LogListModel model;
70  	private List<Logger> loggers = new ArrayList<Logger>();
71  	private InternalLogAppender internalLogAppender = new InternalLogAppender();
72  	private boolean tailing = true;
73  	private Stack<Object> linesToAdd = new Stack<Object>();
74  	private EnableAction enableAction;
75  	private JCheckBoxMenuItem enableMenuItem;
76  	private Thread modelThread;
77  	private final String title;
78  	private boolean released;
79  
80  	public JLogList( String title )
81  	{
82  		super( new BorderLayout() );
83  		this.title = title;
84  
85  		model = new LogListModel();
86  		logList = new JList( model );
87  		logList.setToolTipText( title );
88  		logList.setCellRenderer( new LogAreaCellRenderer() );
89  		logList.setPrototypeCellValue( "Testing 123" );
90  		logList.setFixedCellWidth( -1 );
91  
92  		JPopupMenu listPopup = new JPopupMenu();
93  		listPopup.add( new ClearAction() );
94  		enableAction = new EnableAction();
95  		enableMenuItem = new JCheckBoxMenuItem( enableAction );
96  		enableMenuItem.setSelected( true );
97  		listPopup.add( enableMenuItem );
98  		listPopup.addSeparator();
99  		listPopup.add( new CopyAction() );
100 		listPopup.add( new SetMaxRowsAction() );
101 		listPopup.addSeparator();
102 		listPopup.add( new ExportToFileAction() );
103 
104 		logList.setComponentPopupMenu( listPopup );
105 
106 		setBorder( BorderFactory.createEmptyBorder( 3, 3, 3, 3 ) );
107 		add( new JScrollPane( logList ), BorderLayout.CENTER );
108 
109 		requestAttributes = new SimpleAttributeSet();
110 		StyleConstants.setForeground( requestAttributes, Color.BLUE );
111 
112 		responseAttributes = new SimpleAttributeSet();
113 		StyleConstants.setForeground( responseAttributes, Color.GREEN );
114 
115 		try
116 		{
117 			maxRows = Long.parseLong( SoapUI.getSettings().getString( "JLogList#" + title, "1000" ) );
118 		}
119 		catch( NumberFormatException e )
120 		{
121 		}
122 	}
123 
124 	public void clear()
125 	{
126 		model.clear();
127 	}
128 
129 	public JList getLogList()
130 	{
131 		return logList;
132 	}
133 
134 	public long getMaxRows()
135 	{
136 		return maxRows;
137 	}
138 
139 	public void setMaxRows( long maxRows )
140 	{
141 		this.maxRows = maxRows;
142 	}
143 
144 	public synchronized void addLine( Object line )
145 	{
146 		if( !isEnabled() )
147 			return;
148 
149 		if( modelThread == null )
150 		{
151 			released = false;
152 			modelThread = new Thread( model, title + " LogListUpdater" );
153 			modelThread.start();
154 		}
155 
156 		if( line instanceof LoggingEvent )
157 		{
158 			LoggingEvent ev = ( LoggingEvent ) line;
159 			linesToAdd.push( new LoggingEventWrapper( ev ) );
160 
161 			if( ev.getThrowableInformation() != null )
162 			{
163 				Throwable t = ev.getThrowableInformation().getThrowable();
164 				StringWriter sw = new StringWriter();
165 				PrintWriter pw = new PrintWriter( sw );
166 				t.printStackTrace( pw );
167 				StringTokenizer st = new StringTokenizer( sw.toString(), "\r\n" );
168 				while( st.hasMoreElements() )
169 					linesToAdd.push( "   " +  st.nextElement() );
170 			}
171 		}
172 		else
173 		{
174 			linesToAdd.push( line );
175 		}
176 	}
177 
178 	public void setEnabled( boolean enabled )
179 	{
180 		super.setEnabled( enabled );
181 		logList.setEnabled( enabled );
182 		enableMenuItem.setSelected( enabled );
183 	}
184 
185 	private static class LogAreaCellRenderer extends DefaultListCellRenderer
186 	{
187 		private Map<Level, Color> levelColors = new HashMap<Level, Color>();
188 
189 		private LogAreaCellRenderer()
190 		{
191 			levelColors.put( Level.ERROR, new Color( 192, 0, 0 ) );
192 			levelColors.put( Level.INFO, new Color( 0, 92, 0 ) );
193 			levelColors.put( Level.WARN, Color.ORANGE.darker().darker() );
194 			levelColors.put( Level.DEBUG, new Color( 0, 0, 128 ) );
195 		}
196 
197 		public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected,
198 					boolean cellHasFocus )
199 		{
200 			JLabel component = ( JLabel ) super
201 						.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus );
202 
203 			if( value instanceof LoggingEventWrapper )
204 			{
205 				LoggingEventWrapper eventWrapper = ( LoggingEventWrapper ) value;
206 
207 				if( levelColors.containsKey( eventWrapper.getLevel() ) )
208 					component.setForeground( levelColors.get( eventWrapper.getLevel() ) );
209 			}
210 
211 			component.setToolTipText( component.getText() );
212 
213 			return component;
214 		}
215 	}
216 
217 	private final static class LoggingEventWrapper
218 	{
219 		private final LoggingEvent loggingEvent;
220 		private String str;
221 
222 		public LoggingEventWrapper( LoggingEvent loggingEvent )
223 		{
224 			this.loggingEvent = loggingEvent;
225 		}
226 
227 		public Level getLevel()
228 		{
229 			return loggingEvent.getLevel();
230 		}
231 
232 		public String toString()
233 		{
234 			if( str == null )
235 			{
236 				StringBuilder builder = new StringBuilder();
237 				builder.append( new Date( loggingEvent.timeStamp ) );
238 				builder.append( ':' ).append( loggingEvent.getLevel() ).append( ':' ).append( loggingEvent.getMessage() );
239 				str = builder.toString();
240 			}
241 
242 			return str;
243 		}
244 	}
245 
246 	public void addLogger( String loggerName, boolean addAppender )
247 	{
248 		Logger logger = Logger.getLogger( loggerName );
249 		if( addAppender )
250 			logger.addAppender( internalLogAppender );
251 
252 		loggers.add( logger );
253 	}
254 
255 	public void setLevel( Level level )
256 	{
257 		for( Logger logger : loggers )
258 		{
259 			logger.setLevel( level );
260 		}
261 	}
262 
263 	private class InternalLogAppender extends AppenderSkeleton
264 	{
265 		protected void append( LoggingEvent event )
266 		{
267 			addLine( event );
268 		}
269 
270 		public void close()
271 		{
272 		}
273 
274 		public boolean requiresLayout()
275 		{
276 			return false;
277 		}
278 	}
279 
280 	public boolean monitors( String loggerName )
281 	{
282 		for( Logger logger : loggers )
283 		{
284 			if( loggerName.startsWith( logger.getName() ) )
285 				return true;
286 		}
287 
288 		return false;
289 	}
290 
291 	public void removeLogger( String loggerName )
292 	{
293 		for( Logger logger : loggers )
294 		{
295 			if( loggerName.equals( logger.getName() ) )
296 			{
297 				logger.removeAppender( internalLogAppender );
298 			}
299 		}
300 	}
301 
302 	public boolean isTailing()
303 	{
304 		return tailing;
305 	}
306 
307 	public void setTailing( boolean tail )
308 	{
309 		this.tailing = tail;
310 	}
311 
312 	private class ClearAction extends AbstractAction
313 	{
314 		public ClearAction()
315 		{
316 			super( "Clear" );
317 		}
318 
319 		public void actionPerformed( ActionEvent e )
320 		{
321 			model.clear();
322 		}
323 	}
324 
325 	private class SetMaxRowsAction extends AbstractAction
326 	{
327 		public SetMaxRowsAction()
328 		{
329 			super( "Set Max Rows" );
330 		}
331 
332 		public void actionPerformed( ActionEvent e )
333 		{
334 			String val = UISupport.prompt( "Set maximum number of log rows to keep", "Set Max Rows", String
335 						.valueOf( maxRows ) );
336 			if( val != null )
337 			{
338 				try
339 				{
340 					maxRows = Long.parseLong( val );
341 					SoapUI.getSettings().setString( "JLogList#" + title, val );
342 				}
343 				catch( NumberFormatException e1 )
344 				{
345 					UISupport.beep();
346 				}
347 			}
348 		}
349 	}
350 
351 	private class ExportToFileAction extends AbstractAction
352 	{
353 		public ExportToFileAction()
354 		{
355 			super( "Export to File" );
356 		}
357 
358 		public void actionPerformed( ActionEvent e )
359 		{
360 			if( model.getSize() == 0 )
361 			{
362 				UISupport.showErrorMessage( "Log is empty; nothing to export" );
363 				return;
364 			}
365 
366 			File file = UISupport.getFileDialogs().saveAs( JLogList.this, "Save Log [] to File", "*.log", "*.log", null );
367 			if( file != null )
368 				saveToFile( file );
369 		}
370 	}
371 
372 	private class CopyAction extends AbstractAction
373 	{
374 		public CopyAction()
375 		{
376 			super( "Copy to clipboard" );
377 		}
378 
379 		public void actionPerformed( ActionEvent e )
380 		{
381 			Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
382 
383 			StringBuffer buf = new StringBuffer();
384 			int[] selectedIndices = logList.getSelectedIndices();
385 			if( selectedIndices.length == 0 )
386 			{
387 				for( int c = 0; c < logList.getModel().getSize(); c++ )
388 				{
389 					buf.append( logList.getModel().getElementAt( c ).toString() );
390 					buf.append( "\r\n" );
391 				}
392 			}
393 			else
394 			{
395 				for( int c = 0; c < selectedIndices.length; c++ )
396 				{
397 					buf.append( logList.getModel().getElementAt( selectedIndices[c] ).toString() );
398 					buf.append( "\r\n" );
399 				}
400 			}
401 
402 			StringSelection selection = new StringSelection( buf.toString() );
403 			clipboard.setContents( selection, selection );
404 		}
405 	}
406 
407 	private class EnableAction extends AbstractAction
408 	{
409 		public EnableAction()
410 		{
411 			super( "Enable" );
412 		}
413 
414 		public void actionPerformed( ActionEvent e )
415 		{
416 			JLogList.this.setEnabled( enableMenuItem.isSelected() );
417 		}
418 	}
419 
420 	/***
421 	 * Internal list model that for optimized storage and notifications
422 	 * 
423 	 * @author Ole.Matzura
424 	 */
425 
426 	private final class LogListModel extends AbstractListModel implements Runnable
427 	{
428 		private List<Object> lines = new LinkedList<Object>();
429 
430 		public int getSize()
431 		{
432 			return lines.size();
433 		}
434 
435 		public Object getElementAt( int index )
436 		{
437 			return lines.get( index );
438 		}
439 
440 		public void clear()
441 		{
442 			int sz = lines.size();
443 			if( sz == 0 )
444 				return;
445 
446 			lines.clear();
447 			fireIntervalRemoved( this, 0, sz - 1 );
448 		}
449 
450 		public void run()
451 		{
452 			while( !released && !linesToAdd.isEmpty() )
453 			{
454 				try
455 				{
456 					if( !linesToAdd.isEmpty() )
457 					{
458 						SwingUtilities.invokeAndWait( new Runnable()
459 						{
460 							public void run()
461 							{
462 								while( !linesToAdd.isEmpty() )
463 								{
464 									int sz = lines.size();
465 									lines.addAll( linesToAdd );
466 									linesToAdd.clear();
467 									fireIntervalAdded( LogListModel.this, sz, lines.size() - sz );
468 								}
469 
470 								int cnt = 0;
471 								while( lines.size() > maxRows )
472 								{
473 									lines.remove( 0 );
474 									cnt++;
475 								}
476 
477 								if( cnt > 0 )
478 									fireIntervalRemoved( LogListModel.this, 0, cnt - 1 );
479 
480 								if( tailing )
481 								{
482 									logList.ensureIndexIsVisible( lines.size() - 1 );
483 								}
484 							}
485 						} );
486 					}
487 
488 					Thread.sleep( 500 );
489 				}
490 				catch( InterruptedException e )
491 				{
492 					SoapUI.logError( e );
493 				}
494 				catch( InvocationTargetException e )
495 				{
496 					SoapUI.logError( e );
497 				}
498 			}
499 
500 			modelThread = null;
501 		}
502 	}
503 
504 	public void release()
505 	{
506 		released = true;
507 	}
508 
509 	public void saveToFile( File file )
510 	{
511 		try
512 		{
513 			PrintWriter writer = new PrintWriter( file );
514 			for( int c = 0; c < model.getSize(); c++ )
515 			{
516 				writer.println( model.getElementAt( c ) );
517 			}
518 
519 			writer.close();
520 		}
521 		catch( Exception e )
522 		{
523 			UISupport.showErrorMessage( e );
524 		}
525 	}
526 }