View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2008 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 		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 		if( modelThread == null )
152 		{
153 			released = false;
154 			modelThread = new Thread( model, title + " LogListUpdater" );
155 			modelThread.start();
156 		}
157 
158 		if( line instanceof LoggingEvent )
159 		{
160 			LoggingEvent ev = ( LoggingEvent ) line;
161 			linesToAdd.push( new LoggingEventWrapper( ev ) );
162 
163 			if( ev.getThrowableInformation() != null )
164 			{
165 				Throwable t = ev.getThrowableInformation().getThrowable();
166 				StringWriter sw = new StringWriter();
167 				PrintWriter pw = new PrintWriter( sw );
168 				t.printStackTrace( pw );
169 				StringTokenizer st = new StringTokenizer( sw.toString(), "\r\n" );
170 				while( st.hasMoreElements() )
171 					linesToAdd.push( "   " +  st.nextElement() );
172 			}
173 		}
174 		else
175 		{
176 			linesToAdd.push( line );
177 		}
178 	}
179 
180 	public void setEnabled( boolean enabled )
181 	{
182 		super.setEnabled( enabled );
183 		logList.setEnabled( enabled );
184 		enableMenuItem.setSelected( enabled );
185 	}
186 
187 	private static class LogAreaCellRenderer extends DefaultListCellRenderer
188 	{
189 		private Map<Level, Color> levelColors = new HashMap<Level, Color>();
190 
191 		private LogAreaCellRenderer()
192 		{
193 			levelColors.put( Level.ERROR, new Color( 192, 0, 0 ) );
194 			levelColors.put( Level.INFO, new Color( 0, 92, 0 ) );
195 			levelColors.put( Level.WARN, Color.ORANGE.darker().darker() );
196 			levelColors.put( Level.DEBUG, new Color( 0, 0, 128 ) );
197 		}
198 
199 		public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected,
200 					boolean cellHasFocus )
201 		{
202 			JLabel component = ( JLabel ) super
203 						.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 	private final class LogListModel extends AbstractListModel implements Runnable
449 	{
450 		private List<Object> lines = new LinkedList<Object>();
451 
452 		public int getSize()
453 		{
454 			return lines.size();
455 		}
456 
457 		public Object getElementAt( int index )
458 		{
459 			return lines.get( index );
460 		}
461 
462 		public void clear()
463 		{
464 			int sz = lines.size();
465 			if( sz == 0 )
466 				return;
467 
468 			lines.clear();
469 			fireIntervalRemoved( this, 0, sz - 1 );
470 		}
471 
472 		public void run()
473 		{
474 			while( !released && !linesToAdd.isEmpty() )
475 			{
476 				try
477 				{
478 					if( !linesToAdd.isEmpty() )
479 					{
480 						SwingUtilities.invokeAndWait( new Runnable()
481 						{
482 							public void run()
483 							{
484 								while( !linesToAdd.isEmpty() )
485 								{
486 									int sz = lines.size();
487 									lines.addAll( linesToAdd );
488 									linesToAdd.clear();
489 									fireIntervalAdded( LogListModel.this, sz, lines.size() - sz );
490 								}
491 
492 								int cnt = 0;
493 								while( lines.size() > maxRows )
494 								{
495 									lines.remove( 0 );
496 									cnt++;
497 								}
498 
499 								if( cnt > 0 )
500 									fireIntervalRemoved( LogListModel.this, 0, cnt - 1 );
501 
502 								if( tailing )
503 								{
504 									logList.ensureIndexIsVisible( lines.size() - 1 );
505 								}
506 							}
507 						} );
508 					}
509 
510 					Thread.sleep( 500 );
511 				}
512 				catch( InterruptedException e )
513 				{
514 					SoapUI.logError( e );
515 				}
516 				catch( InvocationTargetException e )
517 				{
518 					SoapUI.logError( e );
519 				}
520 			}
521 
522 			modelThread = null;
523 		}
524 	}
525 
526 	public void release()
527 	{
528 		released = true;
529 	}
530 
531 	public void saveToFile( File file )
532 	{
533 		try
534 		{
535 			PrintWriter writer = new PrintWriter( file );
536 			for( int c = 0; c < model.getSize(); c++ )
537 			{
538 				writer.println( model.getElementAt( c ) );
539 			}
540 
541 			writer.close();
542 		}
543 		catch( Exception e )
544 		{
545 			UISupport.showErrorMessage( e );
546 		}
547 	}
548 }