View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2010 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.util.ArrayList;
26  import java.util.Date;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Stack;
31  import java.util.StringTokenizer;
32  import java.util.concurrent.Future;
33  
34  import javax.swing.AbstractAction;
35  import javax.swing.AbstractListModel;
36  import javax.swing.BorderFactory;
37  import javax.swing.DefaultListCellRenderer;
38  import javax.swing.JCheckBoxMenuItem;
39  import javax.swing.JLabel;
40  import javax.swing.JList;
41  import javax.swing.JPanel;
42  import javax.swing.JPopupMenu;
43  import javax.swing.JScrollPane;
44  import javax.swing.SwingUtilities;
45  import javax.swing.text.SimpleAttributeSet;
46  import javax.swing.text.StyleConstants;
47  
48  import org.apache.commons.collections.list.TreeList;
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 final String title;
77  	private boolean released;
78  	private Future<?> future;
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 		JScrollPane scrollPane = new JScrollPane( logList );
108 		UISupport.addPreviewCorner( scrollPane, true );
109 		add( scrollPane, BorderLayout.CENTER );
110 
111 		requestAttributes = new SimpleAttributeSet();
112 		StyleConstants.setForeground( requestAttributes, Color.BLUE );
113 
114 		responseAttributes = new SimpleAttributeSet();
115 		StyleConstants.setForeground( responseAttributes, Color.GREEN );
116 
117 		try
118 		{
119 			maxRows = Long.parseLong( SoapUI.getSettings().getString( "JLogList#" + title, "1000" ) );
120 		}
121 		catch( NumberFormatException e )
122 		{
123 		}
124 	}
125 
126 	public void clear()
127 	{
128 		model.clear();
129 	}
130 
131 	public JList getLogList()
132 	{
133 		return logList;
134 	}
135 
136 	public long getMaxRows()
137 	{
138 		return maxRows;
139 	}
140 
141 	public void setMaxRows( long maxRows )
142 	{
143 		this.maxRows = maxRows;
144 	}
145 
146 	public synchronized void addLine( Object line )
147 	{
148 		if( !isEnabled() )
149 			return;
150 
151 		synchronized( model.lines )
152 		{
153 			if( line instanceof LoggingEvent )
154 			{
155 				LoggingEvent ev = ( LoggingEvent )line;
156 				linesToAdd.push( new LoggingEventWrapper( ev ) );
157 
158 				if( ev.getThrowableInformation() != null )
159 				{
160 					Throwable t = ev.getThrowableInformation().getThrowable();
161 					StringWriter sw = new StringWriter();
162 					PrintWriter pw = new PrintWriter( sw );
163 					t.printStackTrace( pw );
164 					StringTokenizer st = new StringTokenizer( sw.toString(), "\r\n" );
165 					while( st.hasMoreElements() )
166 						linesToAdd.push( "   " + st.nextElement() );
167 				}
168 			}
169 			else
170 			{
171 				linesToAdd.push( line );
172 			}
173 		}
174 		if( future == null )
175 		{
176 			released = false;
177 			future = SoapUI.getThreadPool().submit( model );
178 		}
179 	}
180 
181 	public void setEnabled( boolean enabled )
182 	{
183 		super.setEnabled( enabled );
184 		logList.setEnabled( enabled );
185 		enableMenuItem.setSelected( enabled );
186 	}
187 
188 	private static class LogAreaCellRenderer extends DefaultListCellRenderer
189 	{
190 		private Map<Level, Color> levelColors = new HashMap<Level, Color>();
191 
192 		private LogAreaCellRenderer()
193 		{
194 			levelColors.put( Level.ERROR, new Color( 192, 0, 0 ) );
195 			levelColors.put( Level.INFO, new Color( 0, 92, 0 ) );
196 			levelColors.put( Level.WARN, Color.ORANGE.darker().darker() );
197 			levelColors.put( Level.DEBUG, new Color( 0, 0, 128 ) );
198 		}
199 
200 		public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected,
201 				boolean cellHasFocus )
202 		{
203 			JLabel component = ( JLabel )super.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus );
204 
205 			if( value instanceof LoggingEventWrapper )
206 			{
207 				LoggingEventWrapper eventWrapper = ( LoggingEventWrapper )value;
208 
209 				if( levelColors.containsKey( eventWrapper.getLevel() ) )
210 					component.setForeground( levelColors.get( eventWrapper.getLevel() ) );
211 			}
212 
213 			// Limit the length of the tool tip, to prevent long delays.
214 			String toolTip = component.getText();
215 			if( toolTip != null && toolTip.length() > 1000 )
216 				toolTip = toolTip.substring( 0, 1000 );
217 			component.setToolTipText( toolTip );
218 
219 			return component;
220 		}
221 	}
222 
223 	private final static class LoggingEventWrapper
224 	{
225 		private final LoggingEvent loggingEvent;
226 		private String str;
227 
228 		public LoggingEventWrapper( LoggingEvent loggingEvent )
229 		{
230 			this.loggingEvent = loggingEvent;
231 		}
232 
233 		public Level getLevel()
234 		{
235 			return loggingEvent.getLevel();
236 		}
237 
238 		public String toString()
239 		{
240 			if( str == null )
241 			{
242 				StringBuilder builder = new StringBuilder();
243 				builder.append( new Date( loggingEvent.timeStamp ) );
244 				builder.append( ':' ).append( loggingEvent.getLevel() ).append( ':' ).append( loggingEvent.getMessage() );
245 				str = builder.toString();
246 			}
247 
248 			return str;
249 		}
250 	}
251 
252 	public void addLogger( String loggerName, boolean addAppender )
253 	{
254 		Logger logger = Logger.getLogger( loggerName );
255 		if( addAppender )
256 			logger.addAppender( internalLogAppender );
257 
258 		loggers.add( logger );
259 	}
260 
261 	public Logger[] getLoggers()
262 	{
263 		return loggers.toArray( new Logger[loggers.size()] );
264 	}
265 
266 	public void setLevel( Level level )
267 	{
268 		for( Logger logger : loggers )
269 		{
270 			logger.setLevel( level );
271 		}
272 	}
273 
274 	public Logger getLogger( String loggerName )
275 	{
276 		for( Logger logger : loggers )
277 		{
278 			if( logger.getName().equals( loggerName ) )
279 				return logger;
280 		}
281 
282 		return null;
283 	}
284 
285 	private class InternalLogAppender extends AppenderSkeleton
286 	{
287 		protected void append( LoggingEvent event )
288 		{
289 			addLine( event );
290 		}
291 
292 		public void close()
293 		{
294 		}
295 
296 		public boolean requiresLayout()
297 		{
298 			return false;
299 		}
300 	}
301 
302 	public boolean monitors( String loggerName )
303 	{
304 		for( Logger logger : loggers )
305 		{
306 			if( loggerName.startsWith( logger.getName() ) )
307 				return true;
308 		}
309 
310 		return false;
311 	}
312 
313 	public void removeLogger( String loggerName )
314 	{
315 		for( Logger logger : loggers )
316 		{
317 			if( loggerName.equals( logger.getName() ) )
318 			{
319 				logger.removeAppender( internalLogAppender );
320 			}
321 		}
322 	}
323 
324 	public boolean isTailing()
325 	{
326 		return tailing;
327 	}
328 
329 	public void setTailing( boolean tail )
330 	{
331 		this.tailing = tail;
332 	}
333 
334 	private class ClearAction extends AbstractAction
335 	{
336 		public ClearAction()
337 		{
338 			super( "Clear" );
339 		}
340 
341 		public void actionPerformed( ActionEvent e )
342 		{
343 			model.clear();
344 		}
345 	}
346 
347 	private class SetMaxRowsAction extends AbstractAction
348 	{
349 		public SetMaxRowsAction()
350 		{
351 			super( "Set Max Rows" );
352 		}
353 
354 		public void actionPerformed( ActionEvent e )
355 		{
356 			String val = UISupport.prompt( "Set maximum number of log rows to keep", "Set Max Rows", String
357 					.valueOf( maxRows ) );
358 			if( val != null )
359 			{
360 				try
361 				{
362 					maxRows = Long.parseLong( val );
363 					SoapUI.getSettings().setString( "JLogList#" + title, val );
364 				}
365 				catch( NumberFormatException e1 )
366 				{
367 					UISupport.beep();
368 				}
369 			}
370 		}
371 	}
372 
373 	private class ExportToFileAction extends AbstractAction
374 	{
375 		public ExportToFileAction()
376 		{
377 			super( "Export to File" );
378 		}
379 
380 		public void actionPerformed( ActionEvent e )
381 		{
382 			if( model.getSize() == 0 )
383 			{
384 				UISupport.showErrorMessage( "Log is empty; nothing to export" );
385 				return;
386 			}
387 
388 			File file = UISupport.getFileDialogs().saveAs( JLogList.this, "Save Log [] to File", "*.log", "*.log", null );
389 			if( file != null )
390 				saveToFile( file );
391 		}
392 	}
393 
394 	private class CopyAction extends AbstractAction
395 	{
396 		public CopyAction()
397 		{
398 			super( "Copy to clipboard" );
399 		}
400 
401 		public void actionPerformed( ActionEvent e )
402 		{
403 			Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
404 
405 			StringBuffer buf = new StringBuffer();
406 			int[] selectedIndices = logList.getSelectedIndices();
407 			if( selectedIndices.length == 0 )
408 			{
409 				for( int c = 0; c < logList.getModel().getSize(); c++ )
410 				{
411 					buf.append( logList.getModel().getElementAt( c ).toString() );
412 					buf.append( "\r\n" );
413 				}
414 			}
415 			else
416 			{
417 				for( int c = 0; c < selectedIndices.length; c++ )
418 				{
419 					buf.append( logList.getModel().getElementAt( selectedIndices[c] ).toString() );
420 					buf.append( "\r\n" );
421 				}
422 			}
423 
424 			StringSelection selection = new StringSelection( buf.toString() );
425 			clipboard.setContents( selection, selection );
426 		}
427 	}
428 
429 	private class EnableAction extends AbstractAction
430 	{
431 		public EnableAction()
432 		{
433 			super( "Enable" );
434 		}
435 
436 		public void actionPerformed( ActionEvent e )
437 		{
438 			JLogList.this.setEnabled( enableMenuItem.isSelected() );
439 		}
440 	}
441 
442 	/***
443 	 * Internal list model that for optimized storage and notifications
444 	 * 
445 	 * @author Ole.Matzura
446 	 */
447 
448 	@SuppressWarnings( "unchecked" )
449 	private final class LogListModel extends AbstractListModel implements Runnable
450 	{
451 		private List<Object> lines = new TreeList();
452 
453 		public int getSize()
454 		{
455 			synchronized( lines )
456 			{
457 				return lines.size();
458 			}
459 		}
460 
461 		public Object getElementAt( int index )
462 		{
463 			synchronized( lines )
464 			{
465 				return lines.get( index );
466 			}
467 		}
468 
469 		public void clear()
470 		{
471 			int sz = lines.size();
472 			if( sz == 0 )
473 				return;
474 
475 			synchronized( lines )
476 			{
477 				lines.clear();
478 				fireIntervalRemoved( this, 0, sz - 1 );
479 			}
480 		}
481 
482 		public void run()
483 		{
484 			Thread.currentThread().setName( "LogList Updater for " + title );
485 
486 			try
487 			{
488 				while( !released && !linesToAdd.isEmpty() )
489 				{
490 					try
491 					{
492 						if( !linesToAdd.isEmpty() )
493 						{
494 							SwingUtilities.invokeAndWait( new Runnable()
495 							{
496 								public void run()
497 								{
498 									try
499 									{
500 										synchronized( lines )
501 										{
502 											while( !linesToAdd.isEmpty() )
503 											{
504 												int sz = lines.size();
505 												lines.addAll( linesToAdd );
506 												linesToAdd.clear();
507 												fireIntervalAdded( LogListModel.this, sz, lines.size() - sz );
508 											}
509 
510 											int cnt = 0;
511 											while( lines.size() > maxRows )
512 											{
513 												lines.remove( 0 );
514 												cnt++ ;
515 											}
516 
517 											if( cnt > 0 )
518 												fireIntervalRemoved( LogListModel.this, 0, cnt - 1 );
519 
520 											if( tailing )
521 											{
522 												logList.ensureIndexIsVisible( lines.size() - 1 );
523 											}
524 										}
525 									}
526 									catch( Throwable e )
527 									{
528 										SoapUI.logError( e );
529 									}
530 								}
531 							} );
532 						}
533 
534 						Thread.sleep( 500 );
535 					}
536 					catch( Throwable e )
537 					{
538 						SoapUI.logError( e );
539 					}
540 				}
541 			}
542 			finally
543 			{
544 				future = null;
545 			}
546 		}
547 	}
548 
549 	public void release()
550 	{
551 		released = true;
552 	}
553 
554 	public void saveToFile( File file )
555 	{
556 		try
557 		{
558 			PrintWriter writer = new PrintWriter( file );
559 			for( int c = 0; c < model.getSize(); c++ )
560 			{
561 				writer.println( model.getElementAt( c ) );
562 			}
563 
564 			writer.close();
565 		}
566 		catch( Exception e )
567 		{
568 			UISupport.showErrorMessage( e );
569 		}
570 	}
571 }