View Javadoc

1   /*
2    * $HeadURL: $
3    * $Date: $
4    * $Revision: $
5    * $Author: $
6    * 
7    * Copyright (c) 2005 MindTree Consulting Ltd. 
8    * 
9    * This file is part of Insight.
10   * 
11   * Insight is free software: you can redistribute it and/or modify it under the 
12   * terms of the GNU General Public License as published by the Free Software 
13   * Foundation, either version 3 of the License, or (at your option) any later 
14   * version.
15   * 
16   * Insight is distributed in the hope that it will be useful, but 
17   * WITHOUT ANY WARRANTY; without even the implied warranty of 
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General 
19   * Public License for more details.
20   * 
21   * You should have received a copy of the GNU General Public License along with 
22   * Insight.  If not, see <http://www.gnu.org/licenses/>.
23   */
24  
25  package com.mindtree.techworks.insight.gui;
26  
27  import java.awt.BorderLayout;
28  import java.awt.Dimension;
29  import java.awt.Toolkit;
30  import java.awt.event.KeyAdapter;
31  import java.awt.event.KeyEvent;
32  import java.text.MessageFormat;
33  import java.util.Iterator;
34  import java.util.LinkedList;
35  import java.util.regex.Matcher;
36  import java.util.regex.Pattern;
37  
38  import javax.swing.BorderFactory;
39  import javax.swing.JComponent;
40  import javax.swing.JPanel;
41  import javax.swing.JScrollPane;
42  import javax.swing.JTextPane;
43  import javax.swing.UIManager;
44  import javax.swing.text.BadLocationException;
45  import javax.swing.text.DefaultHighlighter;
46  import javax.swing.text.Document;
47  import javax.swing.text.Highlighter;
48  
49  import com.mindtree.techworks.insight.Controller;
50  import com.mindtree.techworks.insight.InsightConstants;
51  import com.mindtree.techworks.insight.eventsearch.SearchCriteria;
52  import com.mindtree.techworks.insight.gui.action.FindAction;
53  import com.mindtree.techworks.insight.gui.widgets.StatusBar;
54  import com.mindtree.techworks.insight.pagination.IPage;
55  import com.mindtree.techworks.insight.spi.LogEvent;
56  
57  /**
58  *
59  * The <code>EventDetailsPresentation</code> class is a Presentation implementation
60  * that displays details of one LoggingEvent at a time. Uses a JEditorPane to display
61  * the results.   
62  *
63  * @author  Regunath B
64  * @version 1.0, 04/10/25
65  * @see     com.mindtree.techworks.insight.gui.Presentation
66  * @see     org.apache.log4j.spi.LoggingEvent
67  * @see     com.mindtree.techworks.insight.gui.EventListPresentation
68  */
69  
70  public class EventDetailsPresentation extends JPanel implements Presentation {
71  	
72  	/**
73  	 * Used for object serialization
74  	 */
75  	private static final long serialVersionUID = -3210768545540400816L;
76  	
77  	/**
78  	 * Useful constants for the preferred size of this Presentation 
79  	 */
80  	private static final int WIDTH = 900;
81  	private static final int HEIGHT = 300;	
82  	
83  	/**
84  	 * Useful constant that identifies the class name of the EventListPresentation
85  	 * that this Presentation is interested in for widget change notifications.
86  	 */
87  	private static final String EVENT_LIST_PRESENTATION = EventListPresentation.class.getName();
88  
89  	/**
90  	 * The Controller instance for this Presentation
91  	 */
92  	private Controller controller;	
93  	
94  	/**
95  	 * The LogEvent that is rendered by this Presentation
96  	 */
97  	private LogEvent event;
98  
99  	/**
100 	 * The MessageFormat instance used for formatting the event details display
101 	 */
102     private static final MessageFormat FORMATTER = new MessageFormat(
103             "<b>" + InsightConstants.getLiteral("NAMESPACE") + ":</b> <code>{0}</code>" +
104             "<br><b>" + InsightConstants.getLiteral("PRIORITY_LABEL") + ":</b> <code>{1}</code>" +
105             "<br><b>" + InsightConstants.getLiteral("THREAD_NAME_LABEL") + ":</b> <code>{2}</code>" +
106             "<br><b>" + InsightConstants.getLiteral("LOGGER_NAME") + ":</b> <code>{3}</code>" +
107             "<br><b>" + InsightConstants.getLiteral("MESSAGE_LABEL") + ":</b>" +
108             "<pre>{4}</pre>" +
109             "<b>" + InsightConstants.getLiteral("THROWABLE") + ":</b>" +
110             "<pre>{5}</pre>");
111 
112 	/**
113 	 * The JEditorPane instance used to render the the event details
114 	 */
115 	private JTextPane eventDetails;	
116     
117     /**
118      * Constructor for this class
119      * @param controller the Controller for this Presentation
120      */
121     public EventDetailsPresentation(Controller controller) {
122 	
123         setLayout(new BorderLayout());
124         setBorder(BorderFactory.createTitledBorder(InsightConstants.getLiteral("DETAILS")));
125 
126         this.eventDetails = new JTextPane();
127         this.eventDetails.setEditable(false);
128         this.eventDetails.setContentType("text/html");
129         
130         // add a key adapter to bring up the search text frame when Ctrl+F is pressed
131         // and there is an event being displayed by this Presentation
132         this.eventDetails.addKeyListener(new KeyAdapter() {
133         	public void keyPressed(KeyEvent event) {
134         		if (getEvent() != null && event.getKeyCode() == KeyEvent.VK_F && event.isControlDown()) {
135         			FindAction.getInstance(getController().getInsight()).showSearchTextFrame();
136         		}
137         	}
138         });
139         
140         add(new JScrollPane(eventDetails), BorderLayout.CENTER);
141         
142         this.setPreferredSize(getIdealPreferredSize());
143 		
144 		this.controller = controller;
145 		this.controller.registerPresentation(this);
146 		this.controller.registerWidgetChangeListener(controller.getPresentation(EVENT_LIST_PRESENTATION),this);
147 		
148 	}	
149 	
150 	/**
151 	 * Presentation interface method implementation. Returns the fully qualified class name
152 	 * of this Presentation
153 	 * @see com.mindtree.techworks.insight.gui.Presentation#getUID()
154 	 */
155 	public String getUID() {
156 		return this.getClass().getName();
157 	}
158 
159 	/**
160 	 * Presentation interface method implementation. Renders the display with the
161 	 * specified data. Type-casts the Object data as a org.apache.log4j.spi.LoggingEvent
162 	 * and renders its details. 
163 	 * @see com.mindtree.techworks.insight.gui.Presentation#notifyWidgetStateChange(com.mindtree.techworks.insight.gui.Presentation, int, java.lang.Object)
164 	 */
165 	public void notifyWidgetStateChange(Presentation presentation, int identifier, Object data) {
166 		this.event = (LogEvent)data;
167         final Object[] args =         {
168             event.getNamespace().getNamespaceAsString(),
169             event.getLevel(),
170             escape(event.getThreadName()),
171             escape(event.getLoggerName()),
172             escape(event.getMessage().toString()),
173             escape(getThrowableInfoAsString(event.getThrowableStrRepresentation())),
174         };
175         eventDetails.setText(FORMATTER.format(args));
176         eventDetails.setCaretPosition(0);		
177 	}
178 
179 	/**
180 	 * Presentation interface method implementation. Returns this Presentation.
181 	 * @see com.mindtree.techworks.insight.gui.Presentation#getViewComponent()
182 	 */
183 	public JComponent getViewComponent() {
184 		return this;
185 	}
186 
187 	/**
188 	 * Presentation interface method implementation. Returns false.
189 	 * @see com.mindtree.techworks.insight.gui.Presentation#doesProcessRealTimeUpdates()
190 	 */
191 	public boolean doesProcessRealTimeUpdates() {
192 		return false;
193 	}
194 
195 	/**
196 	 * Presentation interface method implementation. Does nothing. 
197 	 * @see com.mindtree.techworks.insight.gui.Presentation#processRealTimeUpdate(com.mindtree.techworks.insight.spi.LogEvent)
198 	 */
199 	public void processRealTimeUpdate(LogEvent logEvent) {
200 		// No Op
201 	}
202 
203 	/** 
204 	 * Interface method implementation
205 	 * @see com.mindtree.techworks.insight.gui.Presentation#resetWidgets()
206 	 */
207 	public void resetWidgets() {
208 		this.event = null;
209 		eventDetails.setText("");
210 	}
211 
212     /**
213      * Highlights text in the event details display that match the specified search text specified limited to
214      * the specified log event attributes
215      * @param searchText the text to search for
216      * @param searchType the search type. See SearchCriteria for type definitions
217      * @return true if atleast one macth is found, false otherwise
218      * @see com.mindtree.techworks.insight.eventsearch.SearchCriteria
219      */
220     public boolean highlightText(String searchText, int searchType) {
221 		int firstIndex = -1;
222 		try {	
223 			Document document = eventDetails.getDocument();									
224 			String sourceText = document.getText(0,document.getLength()).toUpperCase();
225 			
226 			LinkedList matchAttributeBoundaryList = new LinkedList();
227 			// Construct the AttributeBoundary list only when a highlight is requested as against when displaying
228 			// the text in notifyWidgetStateChange(). 
229 			constructMatchAttributeBoundaryList(sourceText, matchAttributeBoundaryList, searchType);
230 			Highlighter highlighter = eventDetails.getHighlighter();
231 			highlighter.removeAllHighlights();
232 			// Create the HighlightPainter using the PLAF highlight color
233 			Highlighter.HighlightPainter highlightPainter = 
234 				new DefaultHighlighter.DefaultHighlightPainter(UIManager.getDefaults().getColor(InsightConstants.TEXT_HIGHLIGHT));
235 			Pattern pattern = Pattern.compile(searchText, Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE
236 					| Pattern.CANON_EQ | Pattern.UNICODE_CASE);
237 			Matcher matcher = pattern.matcher(sourceText);
238             while (matcher.find()) {
239             	int searchIndex = matcher.start();
240             	Iterator iterator = matchAttributeBoundaryList.iterator();
241             	while(iterator.hasNext()) {
242             		AttributeBoundary ab = (AttributeBoundary)iterator.next();
243             		// Add the highlight only if inside the selected search type
244             		if (ab.containsEquals(searchType, searchIndex)) {
245                     	highlighter.addHighlight(searchIndex, matcher.end(), highlightPainter);            			
246             		}
247             	}
248             	if (firstIndex == -1 && searchIndex > -1) {
249             		firstIndex = searchIndex;
250             	}
251             }
252             if (firstIndex > -1) { // atleast one match has been found
253             	eventDetails.setCaretPosition(firstIndex);					
254             } else {
255         		StatusBar.getInstance().setDisplayText(0,InsightConstants.getLiteral("ERROR_FIND_FAILURE"), false);		
256             }
257 		} catch (BadLocationException ble) {
258 			ble.printStackTrace();
259 		}
260     	return ((firstIndex > -1));
261     }
262     
263 	/**
264 	 * Interface method implementation
265 	 * @see Presentation#displayPage(IPage, long)
266 	 */
267 	public void displayPage(IPage page, long eventSequenceNumber) {
268 		// reset the text ONLY if no event is selected.
269 		// If an event is selected, this Presentation will receive data via notifyWidgetStateChange 
270 		if (eventSequenceNumber < 0) {
271 			resetWidgets();
272 		}
273 	}
274 
275 	/**
276 	 * @return Returns the controller.
277 	 */
278 	public Controller getController() {
279 		return controller;
280 	}
281 	
282 	/**
283 	 * @return Returns the event.
284 	 */
285 	public LogEvent getEvent() {
286 		return event;
287 	}
288 		
289 	/**
290 	 * Presentation Interface method implementation
291 	 * @see com.mindtree.techworks.insight.gui.Presentation#setScrollLock(boolean)
292 	 */
293 	public void setScrollLock(boolean status) {
294 		// do nothing
295 	}
296 	
297 	/**
298 	 * Private helper method that returned a string that escapes the special
299 	 * characters in the specified string to a HTML compatible form 
300 	 * @param aStr the string whose characters need to be escaped for HTML rendering
301 	 * @return the escaped string
302 	 */
303     private String escape(String aStr) {
304         if (aStr == null) {
305             return null;
306         }
307         final StringBuffer buf = new StringBuffer();
308         for (int i = 0; i < aStr.length(); i++) {
309             char c = aStr.charAt(i);
310             switch (c) {
311             case '<':
312                 buf.append("&lt;");
313                 break;
314             case '>':
315                 buf.append("&gt;");
316                 break;
317             case '\"':
318                 buf.append("&quot;");
319                 break;
320             case '&':
321                 buf.append("&amp;");
322                 break;
323             default:
324                 buf.append(c);
325                 break;
326             }
327         }
328         return buf.toString();
329     }
330 
331     /**
332      * Private helper method that converts the specified throwable info String[]
333      * into a string with each String in the array as a new line
334      * @param trace String[] of throwable information
335      * @return single String that is a concatenated form of the throwable information
336      */
337     private String getThrowableInfoAsString(String[] trace) {
338     	StringBuffer buffer = new StringBuffer();
339         for (int i = 0; i < trace.length; i++) {
340         	buffer.append(trace[i]).append("\n");
341         }    	
342     	return buffer.toString();
343     }
344     
345 	/**
346 	 * Helper method that returns the best size between this component's preferred size and that ideal 
347 	 * for the display size
348 	 * @return Dimension most appropriate for this Presentation
349 	 */
350 	private Dimension getIdealPreferredSize() {
351 		Dimension dimension = null;
352 		Dimension screenDim = Toolkit.getDefaultToolkit().getScreenSize();
353 		dimension = new Dimension((int)Math.min(screenDim.getWidth() - 50, WIDTH),
354 				(int)Math.min(screenDim.getHeight()/3, HEIGHT));
355 		return dimension;		
356 	}        	
357 	    
358 	/**
359 	 * Helper method that constructs the list of AttributeBoundary instances based
360 	 * on the specified search type
361 	 * @param sourceText the text of the event details document object in uppercase
362 	 * @param matchAttributeBoundaryList list to populate the AttributeBoundary instances
363 	 * @param searchType the searchType as defined in SearchCriteria
364 	 * @see SearchCriteria
365 	 */
366 	private void constructMatchAttributeBoundaryList(String sourceText, LinkedList matchAttributeBoundaryList, int searchType) {
367     	if (this.event == null) { // return if no event has been selected yet
368     		return;
369     	}
370 		int attributeTextStartIndex = 0;
371 		
372 		// NOTE : Donot change this order of evaluation since
373 		// the AttributeBoundary indices are based on the order in which
374 		// log event details are displayed
375 		
376 		// Construct the AttributeBoundary for Namespace field
377 		int attributeIndex = sourceText.indexOf(this.event.getNamespace().getNamespaceAsString().toUpperCase(), attributeTextStartIndex);
378 		int attributeLength = this.event.getNamespace().getNamespaceAsString().length(); 
379 		if ((searchType & SearchCriteria.NAMESPACE_SEARCH) == SearchCriteria.NAMESPACE_SEARCH) {
380 			matchAttributeBoundaryList.add(new AttributeBoundary(SearchCriteria.NAMESPACE_SEARCH,
381 					attributeTextStartIndex, attributeIndex + attributeLength));
382 		}
383 		attributeTextStartIndex = attributeIndex + attributeLength;
384 		
385 		// Construct the AttributeBoundary for Priority field
386 		attributeIndex = sourceText.indexOf(this.event.getLevel().toString().toUpperCase(), attributeTextStartIndex);
387 		attributeLength = this.event.getLevel().toString().length(); 
388 		if ((searchType & SearchCriteria.PRIORITY_SEARCH) == SearchCriteria.PRIORITY_SEARCH) {
389 			matchAttributeBoundaryList.add(new AttributeBoundary(SearchCriteria.PRIORITY_SEARCH,
390 					attributeTextStartIndex, attributeIndex + attributeLength));
391 		}
392 		attributeTextStartIndex = attributeIndex + attributeLength;
393 		
394 		// Construct the AttributeBoundary for ThreadName field
395 		attributeIndex = sourceText.indexOf(this.event.getThreadName().toUpperCase(), attributeTextStartIndex);
396 		attributeLength = this.event.getThreadName().length();
397 		if ((searchType & SearchCriteria.THREAD_SEARCH) == SearchCriteria.THREAD_SEARCH) {
398 			matchAttributeBoundaryList.add(new AttributeBoundary(SearchCriteria.THREAD_SEARCH,
399 					attributeTextStartIndex, attributeIndex + attributeLength));
400 		}
401 		attributeTextStartIndex = attributeIndex + attributeLength;
402 		
403 		// Construct the AttributeBoundary for Category field
404 		attributeIndex = sourceText.indexOf(this.event.getLoggerName().toUpperCase(), attributeTextStartIndex);
405 		attributeLength = this.event.getLoggerName().length(); 
406 		if ((searchType & SearchCriteria.CATEGORY_SEARCH) == SearchCriteria.CATEGORY_SEARCH) {
407 			matchAttributeBoundaryList.add(new AttributeBoundary(SearchCriteria.CATEGORY_SEARCH,
408 					attributeTextStartIndex, attributeIndex + attributeLength));
409 		}
410 		attributeTextStartIndex = attributeIndex + attributeLength;
411 
412 		// Construct the AttributeBoundary for Message field. Add one for ExceptionName also since it might have been
413 		// interpreted from the Message
414 		// replace any "\r" ocurrences with "" as well while comparing 
415 		attributeIndex = sourceText.indexOf(this.event.getMessage().toString().toUpperCase().replaceAll("\r",""), attributeTextStartIndex);
416 		attributeLength = this.event.getMessage().toString().length(); 
417 		if (((searchType & SearchCriteria.MESSAGE_SEARCH) == SearchCriteria.MESSAGE_SEARCH) || 
418 			((searchType & SearchCriteria.EXCEPTION_CLASS_NAME_SEARCH) == SearchCriteria.EXCEPTION_CLASS_NAME_SEARCH)) {
419 			matchAttributeBoundaryList.add(new AttributeBoundary(SearchCriteria.MESSAGE_SEARCH,
420 					attributeTextStartIndex, attributeIndex + attributeLength));
421 			matchAttributeBoundaryList.add(new AttributeBoundary(SearchCriteria.EXCEPTION_CLASS_NAME_SEARCH,
422 					attributeTextStartIndex, attributeIndex + attributeLength));
423 		}
424 		attributeTextStartIndex = attributeIndex + attributeLength;
425 
426 		// Construct the AttributeBoundary for Throwable field. Add one for ExceptionName also since it might have been
427 		// interpreted from the Throwable's stack trace
428 		attributeIndex = sourceText.indexOf(getThrowableInfoAsString(this.event.getThrowableStrRepresentation()).toUpperCase(), attributeTextStartIndex);
429 		attributeLength = getThrowableInfoAsString(this.event.getThrowableStrRepresentation()).length(); 
430 		if (((searchType & SearchCriteria.THROWABLE_SEARCH) == SearchCriteria.THROWABLE_SEARCH) || 
431 			((searchType & SearchCriteria.EXCEPTION_CLASS_NAME_SEARCH) == SearchCriteria.EXCEPTION_CLASS_NAME_SEARCH)) {
432 			matchAttributeBoundaryList.add(new AttributeBoundary(SearchCriteria.THROWABLE_SEARCH,
433 					attributeTextStartIndex, attributeIndex + attributeLength));
434 			matchAttributeBoundaryList.add(new AttributeBoundary(SearchCriteria.EXCEPTION_CLASS_NAME_SEARCH,
435 					attributeTextStartIndex, attributeIndex + attributeLength));
436 		}		
437 	}
438 
439 	/**
440 	 * Helper class that contains the boundary details of a LogEvent 
441 	 * attribute such as start and end indices in the details display text 
442 	 */
443 	private class AttributeBoundary {
444 		/**
445 		 * Attribute type as defined in SearchCriteria
446 		 */ 
447 		private int attributeType;
448 		
449 		/**
450 		 * The start Caret position in the display for the LogEvent field/attribute
451 		 */
452 		private int startIndex = -1;
453 		
454 		/**
455 		 * The end Caret position in the display for the LogEvent field/attribute
456 		 */
457 		private int endIndex = -1;
458 		
459 		/**
460 		 * Constructor for this class
461 		 * @param attributeType valid attribute type as defined in SearchCriteria
462 		 * @param startIndex the start Caret position
463 		 * @param endIndex the end Caret position
464 		 * @see SearchCriteria
465 		 */
466 		public AttributeBoundary(int attributeType, int startIndex, int endIndex) {
467 			this.attributeType = attributeType;
468 			this.startIndex = startIndex;
469 			this.endIndex = endIndex;
470 		}
471 		
472 		/**
473 		 * Determines if the specified attributeType matches this AttributeBoundary type and index lies within it
474 		 * @param attributeType the valid attribute type as defined in SearchCriteria
475 		 * @param index the index to be verified for containment within this AttributeBoundary coordinates
476 		 * @return true if the type matches and the index is contained in this AttributeBoundary
477 		 */
478 		public boolean containsEquals(int attributeType, int index) {
479 			return (((attributeType & this.attributeType) == this.attributeType) 
480 					&& index >= startIndex 
481 					&& index <= endIndex);
482 		}
483 	}
484 	
485 }