View Javadoc

1   /*
2    * Copyright 2007 Sebastien Brunot (sbrunot@gmail.com)
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *   
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sourceforge.buildmonitor;
17  
18  import java.awt.CheckboxMenuItem;
19  import java.awt.Desktop;
20  import java.awt.Font;
21  import java.awt.Image;
22  import java.awt.Menu;
23  import java.awt.MenuItem;
24  import java.awt.PopupMenu;
25  import java.awt.SystemTray;
26  import java.awt.Toolkit;
27  import java.awt.TrayIcon;
28  import java.awt.TrayIcon.MessageType;
29  import java.awt.event.ActionEvent;
30  import java.awt.event.ActionListener;
31  import java.awt.event.ItemEvent;
32  import java.awt.event.ItemListener;
33  import java.io.IOException;
34  import java.net.URI;
35  import java.text.MessageFormat;
36  import java.text.SimpleDateFormat;
37  import java.util.ArrayList;
38  import java.util.Collections;
39  import java.util.Date;
40  import java.util.List;
41  import java.util.Locale;
42  import java.util.ResourceBundle;
43  
44  import javax.swing.ImageIcon;
45  import javax.swing.JOptionPane;
46  import javax.swing.UIManager;
47  
48  import net.sourceforge.buildmonitor.monitors.BambooMonitor;
49  import net.sourceforge.buildmonitor.monitors.Monitor;
50  
51  import org.joda.time.LocalDateTime;
52  import org.joda.time.Period;
53  
54  /**
55   * The main class of the application.
56   * @author sbrunot
57   *
58   */
59  public class BuildMonitorImpl implements Runnable, BuildMonitor
60  {
61  	//////////////////////////////
62  	// Constants
63  	//////////////////////////////
64  
65  	private static final String MESSAGES_BASE_NAME = "messages/GUIStrings";
66  
67  	private static final String IMAGE_MONITORING_EXCEPTION = "images/network-offline.png";
68  
69  	private static final String IMAGE_INITIAL_ICON = "images/utilities-system-monitor.png";
70  
71  	private static final String IMAGE_ABOUT_ICON = "images/about.png";
72  
73  	private static final String IMAGE_BUILD_SUCCESS = "images/green_up.png";
74  
75  	private static final String IMAGE_BUILD_FAILURE = "images/red_down.png";
76  
77  	// TODO: use relative font if possible
78  	private static final Font SUCCESSFULL_BUILD_MENUITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
79  
80  	// TODO: use relative font if possible
81  	private static final Font FAILED_BUILD_MENUITEM_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 14);
82  	
83  	private static final int TOOLTIP_MAX_LENGTH = 127;
84  
85  	private static final String OPTIONS_RELATED_MESSAGES_SUFFIX = " Double click here to edit Options.";
86  	
87  	private static final String TRUNCATED_MESSAGE_SUFFIX = " [...]";
88  
89  	private static final int SORT_BY_NAME = 1;
90  	
91  	private static final int SORT_BY_AGE = 2;
92  	
93  	///////////////////////////////////
94  	// Nested classes
95  	///////////////////////////////////
96  
97  	/**
98  	 * An ActionListener that opens an URI in a browser when the action is performed
99  	 */
100 	private class OpenURIInBrowserActionListener implements ActionListener
101 	{
102 		////////////////////////////
103 		// Instance attributes
104 		////////////////////////////
105 
106 		private URI uri = null;
107 		
108 		////////////////////////////
109 		// Constructor
110 		////////////////////////////
111 
112 		/**
113 		 * Create a new instance of the ActionListener.
114 		 * @param theURI the URI to open in a web browser when the action is performed
115 		 */
116 		public OpenURIInBrowserActionListener(URI theURI)
117 		{
118 			this.uri = theURI;
119 		}
120 		
121 		////////////////////////////
122 		// ActionListener implementation
123 		////////////////////////////
124 		
125 		/**
126 		 * {@inheritDoc}
127 		 */
128 		public void actionPerformed(ActionEvent e)
129 		{
130 			if (Desktop.isDesktopSupported())
131 			{
132 				try
133 				{
134 					Desktop.getDesktop().browse(this.uri);
135 				}
136 				catch (IOException err)
137 				{
138 					// Nothing can be done here...
139 				}
140 			}
141 		}
142 		
143 	}
144 	
145 	/**
146 	 * The thread to be registered as a shutdown hook for the application
147 	 */
148 	private class ShutdownThread extends Thread
149 	{
150 		public void run()
151 		{
152 			// Stop the monitoring thread if necessary
153 			if (monitor != null)
154 			{
155 				monitor.stop();
156 			}
157 		}
158 	}
159 
160 	/**
161 	 * A Runnable that can be launched with {@link javax.swing.SwingUtilities#invokeLater(Runnable)}
162 	 * to update the tray icon of the application.
163 	 * 
164 	 * @author sbrunot
165 	 *
166 	 */
167 	private class TrayIconUpdater implements Runnable
168 	{
169 		private Image newIcon;
170 		
171 		private String iconTooltip;
172 		
173 		private String messageToDisplay;
174 		
175 		private String messageCaption;
176 		
177 		private MessageType messageType;
178 		
179 		private ActionListener trayIconNewActionListener;
180 		
181 		public TrayIconUpdater(Image theNewIcon, String theNewIconTooltip, String theMessageCaption, String theMessageToDisplay, MessageType theMessageType, ActionListener theTrayIconNewActionListener)
182 		{
183 			this.newIcon = theNewIcon;
184 			this.iconTooltip = theNewIconTooltip;
185 			this.messageToDisplay = theMessageToDisplay;
186 			this.messageCaption = theMessageCaption;
187 			this.messageType = theMessageType;
188 			this.trayIconNewActionListener = theTrayIconNewActionListener;
189 		}
190 		
191 		public void run()
192 		{
193 			if (this.iconTooltip != null)
194 			{
195 				trayIcon.setToolTip(this.iconTooltip);
196 			}
197 			if (this.newIcon != null)
198 			{
199 				trayIcon.setImage(this.newIcon);
200 			}
201 			if (this.messageToDisplay != null)
202 			{
203 				trayIcon.displayMessage(this.messageCaption, this.messageToDisplay, this.messageType);
204 			}
205 			if (this.trayIconNewActionListener != null)
206 			{
207 				ActionListener[] listeners = trayIcon.getActionListeners();
208 				for (ActionListener listener : listeners)
209 				{
210 					trayIcon.removeActionListener(listener);
211 				}
212 				trayIcon.addActionListener(this.trayIconNewActionListener);
213 			}
214 		}
215 		
216 	}
217 
218 	/**
219 	 * A Runnable that can be launched with {@link javax.swing.SwingUtilities#invokeLater(Runnable)}
220 	 * to update the build status in the system tray icon of the application.
221 	 * It updates the tray icon and tooltip and the popup menu.
222 	 * 
223 	 * @author sbrunot
224 	 *
225 	 */
226 	private class BuildStatusUpdater implements Runnable
227 	{
228 		//////////////////////////
229 		// Instance attributes
230 		//////////////////////////
231 		
232 		List<BuildReport> listOfBuildReportsOrderedByName = null;
233 		int numberOfFailedBuilds = 0;
234 		
235 		//////////////////////////
236 		// Constuctor
237 		//////////////////////////
238 		
239 		/**
240 		 * Create a new instance of the updater
241 		 * @param theListOfBuildReportsOrderedByName the list of build reports to use
242 		 * to update the system tray icon. It MUST be ordered by names (as when sorted
243 		 * using the BuildReport.NameComparator comparator).
244 		 */
245 		public BuildStatusUpdater(List<BuildReport> theListOfBuildReportsOrderedByName)
246 		{
247 			this.listOfBuildReportsOrderedByName = theListOfBuildReportsOrderedByName;
248 		}
249 		
250 		//////////////////////////
251 		// Runnable implementation
252 		//////////////////////////
253 		
254 		/**
255 		 * {@inheritDoc}
256 		 */
257 		public void run()
258 		{
259 			PopupMenu trayIconPopupMenu = trayIcon.getPopupMenu();
260 			
261 			// If the build results menu entries exists, delete them all
262 			while (trayIconPopupMenu.getItemCount() > numberOfItemInEmptyTrayMenu)
263 			{
264 				trayIconPopupMenu.remove(indexOfTheFirstBuildResultMenuItem);
265 			}
266 			
267 			// Create the build results menu entries
268 			int newMenuItemIndex = indexOfTheFirstBuildResultMenuItem;
269 			for (BuildReport buildReport : this.listOfBuildReportsOrderedByName)
270 			{
271 				// Create a MenuItem for this build report
272 				MenuItem newMenuItem = createNewMenuItemForBuildReport(buildReport, FAILED_BUILD_MENUITEM_FONT, SUCCESSFULL_BUILD_MENUITEM_FONT);
273 
274 				// Insert the MenuItem into the popup menu
275 				trayIconPopupMenu.insert(newMenuItem, newMenuItemIndex);
276 				newMenuItemIndex++;
277 			}
278 			// Add the separator at the end
279 			trayIconPopupMenu.insertSeparator(newMenuItemIndex);
280 
281 			// update action listener (that might have been changed when previously reporting a monitoring exception)
282 			ActionListener[] listeners = trayIcon.getActionListeners();
283 			for (ActionListener listener : listeners)
284 			{
285 				trayIcon.removeActionListener(listener);
286 			}
287 			trayIcon.addActionListener(openBuildServerHomePageActionListener);
288 			
289 			// update icon and tooltip
290 			if (this.numberOfFailedBuilds > 0)
291 			{
292 				trayIcon.setImage(buildFailureIcon);
293 			}
294 			else
295 			{
296 				trayIcon.setImage(buildSuccessIcon);
297 			}
298 			SimpleDateFormat timeFormat = new SimpleDateFormat("HH'h'mm");
299 			trayIcon.setToolTip(monitor.getSystemTrayIconTooltipHeader() + "\nLast update at " + timeFormat.format(new Date()) + "\n" + this.numberOfFailedBuilds + " failed builds out of " + this.listOfBuildReportsOrderedByName.size());
300 		}
301 
302 		/////////////////////////////////
303 		// Private methods
304 		/////////////////////////////////
305 
306 		/**
307 		 * Create a new menu item for a build report
308 		 * @param theBuildReport the build report to create a menu item for
309 		 * @param theBuildFailedFont the Font to use for a menu item related to a failed build
310 		 * @param theBuildSucessFont the Font to use for a menu iteł related to a successfull build
311 		 * @return
312 		 */
313 		private MenuItem createNewMenuItemForBuildReport(BuildReport theBuildReport, Font theBuildFailedFont, Font theBuildSucessFont)
314 		{
315 			MenuItem newMenuItem = new MenuItem(getMenuItemLabelForBuildReport(theBuildReport));
316 			if (theBuildReport.hasFailed())
317 			{
318 				newMenuItem.setFont(theBuildFailedFont);
319 				this.numberOfFailedBuilds++;
320 			}
321 			else
322 			{
323 				newMenuItem.setFont(theBuildSucessFont);
324 			}
325 			newMenuItem.setActionCommand(theBuildReport.getId());
326 			newMenuItem.setName(theBuildReport.getName());
327 			ActionListener newMenuItemActionListener = new ActionListener() {
328 				public void actionPerformed(ActionEvent e)
329 				{
330 					if (Desktop.isDesktopSupported())
331 					{
332 						try
333 						{
334 							Desktop.getDesktop().browse(monitor.getBuildURI(e.getActionCommand()));
335 						}
336 						catch (IOException err)
337 						{
338 							// Nothing can be done here...
339 						}
340 					}
341 				}
342 			};
343 			newMenuItem.addActionListener(newMenuItemActionListener);		
344 			return newMenuItem;
345 		}
346 		
347 		/**
348 		 * Build a MenuItem label for a build report.
349 		 * @param theBuildReport the build report
350 		 * @return a MenuItem label for theBuildReport
351 		 */
352 		private String getMenuItemLabelForBuildReport(BuildReport theBuildReport)
353 		{
354 			String howLongAgo = null;
355 			Period ageOfTheBuild = new Period(new LocalDateTime(theBuildReport.getDate()), new LocalDateTime());
356 			// is it more than one year ago ?
357 			if (ageOfTheBuild.getYears() > 0)
358 			{
359 				if (ageOfTheBuild.getYears() > 1)
360 				{
361 					howLongAgo = ageOfTheBuild.getYears() + " years ago";					
362 				}
363 				else
364 				{
365 					howLongAgo = "1 year ago";					
366 				}
367 			}
368 			else if (ageOfTheBuild.getMonths() > 0)
369 			{
370 				if (ageOfTheBuild.getMonths() > 1)
371 				{
372 					howLongAgo = ageOfTheBuild.getMonths() + " months ago";											
373 				}
374 				else
375 				{
376 					howLongAgo = "1 month ago";											
377 				}
378 			}
379 			else if (ageOfTheBuild.getWeeks() > 0)
380 			{
381 				if (ageOfTheBuild.getWeeks() > 1)
382 				{
383 					howLongAgo = ageOfTheBuild.getWeeks() + " weeks ago";
384 				}
385 				else
386 				{
387 					howLongAgo = "1 week ago";
388 				}
389 			}
390 			else if (ageOfTheBuild.getDays() > 0)
391 			{
392 				if (ageOfTheBuild.getDays() > 1)
393 				{
394 					howLongAgo = ageOfTheBuild.getDays() + " days ago";
395 				}
396 				else
397 				{
398 					howLongAgo = "yesterday";
399 				}
400 			}
401 			else if (ageOfTheBuild.getHours() > 0)
402 			{
403 				if (ageOfTheBuild.getHours() > 1)
404 				{
405 					howLongAgo = ageOfTheBuild.getHours() + " hours ago";
406 				}
407 				else
408 				{
409 					howLongAgo = "one hour ago";
410 				}
411 			}
412 			else if (ageOfTheBuild.getMinutes() > 5)
413 			{
414 				howLongAgo = ageOfTheBuild.getMinutes() + " minutes ago";
415 			}
416 			else if (ageOfTheBuild.getMinutes() > 1)
417 			{
418 				howLongAgo = "a few minutes ago";
419 			}
420 			else
421 			{
422 				howLongAgo = "a few seconds ago";
423 			}
424 			return theBuildReport.getName() + "  (" + howLongAgo + ")";
425 		}
426 	}
427 
428 	//////////////////////////////
429 	// Instance attributes
430 	//////////////////////////////
431 	
432 	private ResourceBundle messages = null;
433 	
434 	private Monitor monitor = null;
435 	
436 	private Thread monitorThread = null;
437 	
438 	private TrayIcon trayIcon = null;
439 	
440 	private Image initialIcon = null;
441 
442 	private Image buildSuccessIcon = null;
443 	
444 	private Image buildFailureIcon = null;
445 
446 	private Image monitoringExceptionIcon = null;
447 
448 	private ImageIcon aboutIcon = null;
449 	
450 	private String currentlyReportedMonitoringException = null;
451 	
452 	private ActionListener openBuildServerHomePageActionListener = null;
453 	
454 	private ActionListener openOptionsDialogActionListener = null;
455 	
456 	private int currentSortOrder = SORT_BY_NAME;
457 
458 	private CheckboxMenuItem sortByNameMenuItem = null;
459 
460 	private CheckboxMenuItem sortByAgeMenuItem = null;
461 
462 	/**
463 	 * The number of menu items on the tray icon popup menu when it does not contains
464 	 * build results (this value is calculated when the menu is build the first time).
465 	 */
466 	private int numberOfItemInEmptyTrayMenu = -1;
467 	
468 	/**
469 	 * Index of the first build result menu item in the build menu. It is a constant,
470 	 * defined when the menu is build the first time.
471 	 */
472 	private int indexOfTheFirstBuildResultMenuItem = -1;
473 	
474 	/**
475 	 * The previous build reports (that we use to detect if the situation have changed)
476 	 */
477 	private List<BuildReport> previousBuildReports = new ArrayList<BuildReport>();
478 	
479 	//////////////////////////////
480 	// Runnable implementation
481 	//////////////////////////////
482 	
483 	/**
484 	 * TODO: DOCUMENTS ME !
485 	 */
486 	public void run()
487 	{
488 		try
489 		{
490 			// load messages resource file
491 			this.messages = ResourceBundle.getBundle(MESSAGES_BASE_NAME, Locale.getDefault(), this.getClass().getClassLoader());
492 
493 			// load image resources
494 			this.initialIcon = Toolkit.getDefaultToolkit().getImage(this.getClass().getClassLoader().getResource(IMAGE_INITIAL_ICON));
495 			this.monitoringExceptionIcon = Toolkit.getDefaultToolkit().getImage(this.getClass().getClassLoader().getResource(IMAGE_MONITORING_EXCEPTION));
496 			this.buildSuccessIcon = Toolkit.getDefaultToolkit().getImage(this.getClass().getClassLoader().getResource(IMAGE_BUILD_SUCCESS));
497 			this.buildFailureIcon = Toolkit.getDefaultToolkit().getImage(this.getClass().getClassLoader().getResource(IMAGE_BUILD_FAILURE));
498 			this.aboutIcon = new ImageIcon(Toolkit.getDefaultToolkit().getImage(this.getClass().getClassLoader().getResource(IMAGE_ABOUT_ICON)));
499 
500 			// create the system tray icon
501 			if (SystemTray.isSupported())
502 			{
503 				// Set platform look & feel
504 				UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
505 
506 				// create the monitor instance (TODO: PROMPT THE USER / USE A PROPERTY TO DEFINE THE KIND OF MONITOR TO CREATE ?
507 				this.monitor = new BambooMonitor(this);
508 				this.openBuildServerHomePageActionListener = new ActionListener()
509 				{
510 					public void actionPerformed(ActionEvent e)
511 					{
512 						if (Desktop.isDesktopSupported())
513 						{
514 							try
515 							{
516 								Desktop.getDesktop().browse(monitor.getMainPageURI());
517 							}
518 							catch (IOException err)
519 							{
520 								// Nothing can be done here...
521 							}
522 						}
523 					}
524 				};
525 
526 //				this.monitor = new CruiseControlRssMonitor(this, new URL("http://leo:9080/cruisecontrol/rss"), new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"), 30);
527 
528 				// create the tray icon
529 				SystemTray tray = SystemTray.getSystemTray();
530 
531 				// Popup menu for the tray icon
532 				PopupMenu trayMenu = new PopupMenu();
533 				
534 				// Build server home page menu item
535 				MenuItem buildServerHomePageMenuItem = new MenuItem(this.monitor.getMonitoredBuildSystemName() + " " + getMessage(MESSAGEKEY_TRAYICON_MENUITEM_BUILD_SERVER_HOME_PAGE_SUFFIX));
536 				buildServerHomePageMenuItem.addActionListener(this.openBuildServerHomePageActionListener);
537 				trayMenu.add(buildServerHomePageMenuItem);
538 				
539 				// Update status now menu item
540 				MenuItem updateStatusNowMenuItem = new MenuItem(getMessage(MESSAGEKEY_TRAYICON_MENUITEM_UPDATE_STATUS_NOW));
541 				ActionListener updateStatusNowMenuItemActionListener = new ActionListener()
542 				{
543 					public void actionPerformed(ActionEvent e)
544 					{
545 						monitorThread.interrupt();
546 					}
547 				};
548 				updateStatusNowMenuItem.addActionListener(updateStatusNowMenuItemActionListener);
549 				trayMenu.add(updateStatusNowMenuItem);
550 				
551 				// Sort sub menu
552 				Menu sortMenu = new Menu(getMessage(MESSAGEKEY_TRAYICON_MENU_SORT));
553 				this.sortByAgeMenuItem = new CheckboxMenuItem(getMessage(MESSAGEKEY_TRAYICON_MENUITEM_SORT_BY_AGE), true);
554 				this.currentSortOrder = SORT_BY_AGE;
555 				sortMenu.add(this.sortByAgeMenuItem);
556 				this.sortByNameMenuItem = new CheckboxMenuItem(getMessage(MESSAGEKEY_TRAYICON_MENUITEM_SORT_BY_NAME), false);
557 				sortMenu.add(this.sortByNameMenuItem);
558 				ItemListener sortByNameMenuItemActionListener = new ItemListener()
559 				{
560 					public void itemStateChanged(ItemEvent e)
561 					{
562 						if (e.getStateChange() == ItemEvent.SELECTED)
563 						{
564 							sortByNameMenuItem.setState(true);
565 							sortByAgeMenuItem.setState(false);
566 							currentSortOrder = SORT_BY_NAME;
567 							reportConfigurationUpdatedToBeTakenIntoAccountImmediately();
568 						}
569 						else
570 						{
571 							sortByNameMenuItem.setState(true);
572 							reportConfigurationUpdatedToBeTakenIntoAccountImmediately();
573 						}
574 						
575 					}
576 				};
577 				this.sortByNameMenuItem.addItemListener(sortByNameMenuItemActionListener);
578 				ItemListener sortByAgeMenuItemActionListener = new ItemListener()
579 				{
580 					public void itemStateChanged(ItemEvent e)
581 					{
582 						if (e.getStateChange() == ItemEvent.SELECTED)
583 						{
584 							sortByNameMenuItem.setState(false);
585 							sortByAgeMenuItem.setState(true);
586 							currentSortOrder = SORT_BY_AGE;
587 							reportConfigurationUpdatedToBeTakenIntoAccountImmediately();
588 						}
589 						else
590 						{
591 							sortByAgeMenuItem.setState(true);
592 							reportConfigurationUpdatedToBeTakenIntoAccountImmediately();
593 						}
594 						
595 					}
596 				};
597 				this.sortByAgeMenuItem.addItemListener(sortByAgeMenuItemActionListener);
598 				trayMenu.add(sortMenu);
599 
600 				// Options menu item
601 				MenuItem optionsMenuItem = new MenuItem(getMessage(MESSAGEKEY_TRAYICON_MENUITEM_OPTIONS));
602 				this.openOptionsDialogActionListener = new ActionListener() {
603 					public void actionPerformed(ActionEvent e)
604 					{
605 						try
606 						{
607 							monitor.displayOptionsDialog();
608 						}
609 						catch (Exception exc)
610 						{
611 							panic(exc);
612 						}
613 					}
614 				};
615 				optionsMenuItem.addActionListener(this.openOptionsDialogActionListener);
616 				trayMenu.add(optionsMenuItem);
617 				
618 				// Separator
619 				trayMenu.addSeparator();
620 
621 				// Here will be added the build results menu items by the monitor
622 				this.indexOfTheFirstBuildResultMenuItem = trayMenu.getItemCount();
623 
624 				// About menu item
625 				MenuItem aboutMenuItem = new MenuItem(getMessage(MESSAGEKEY_TRAYICON_MENUITEM_ABOUT));
626 				ActionListener aboutMenuItemActionListener = new ActionListener()
627 				{
628 					public void actionPerformed(ActionEvent e)
629 					{
630 						// FIXME: THE ABOUT MESSAGE SHOULD BE DYNAMICALY GENERATED AT BUILD TIME
631 						JOptionPane.showMessageDialog(null, "This is the preview version of build monitor, by sbrunot@gmail.com.\nBuild Revision: unknown\nCurrent monitor is the Bamboo monitor.\n\n", "About...", JOptionPane.INFORMATION_MESSAGE, aboutIcon);
632 					}
633 				};
634 				aboutMenuItem.addActionListener(aboutMenuItemActionListener);
635 				trayMenu.add(aboutMenuItem);
636 				
637 				// Exit sub menu
638 				Menu exitMenu = new Menu(getMessage(MESSAGEKEY_TRAYICON_MENU_EXIT));
639 				MenuItem exitMenuItem = new MenuItem(getMessage(MESSAGEKEY_TRAYICON_MENUITEM_EXIT));
640 				ActionListener exitMenuItemActionListener = new ActionListener() {
641 					public void actionPerformed(ActionEvent e)
642 					{
643 						System.exit(0);
644 					}
645 				};
646 				exitMenuItem.addActionListener(exitMenuItemActionListener);
647 				exitMenu.add(exitMenuItem);
648 				trayMenu.add(exitMenu);
649 				
650 				this.numberOfItemInEmptyTrayMenu = trayMenu.getItemCount();
651 				
652 				this.trayIcon = new TrayIcon(this.initialIcon, getMessage(MESSAGEKEY_TRAYICON_INITIAL_TOOLTIP), trayMenu);
653 				tray.add(trayIcon);
654 			}
655 			else
656 			{
657 				panic(getMessage(MESSAGEKEY_ERROR_SYSTEMTRAY_NOT_SUPPORTED));
658 			}
659 			
660 			// Register a shutdown hook thread to stop monitoring thread on exit
661 			Runtime.getRuntime().addShutdownHook(new ShutdownThread());
662 			
663 			// Start the monitor
664 			this.monitorThread = new Thread(this.monitor, "Bamboo monitor thread");
665 			this.monitorThread.start();
666 
667 			this.trayIcon.addActionListener(openBuildServerHomePageActionListener);
668 			
669 		}
670 		catch(Throwable t)
671 		{
672 			panic(t);
673 		}
674 	}
675 
676 	//////////////////////////////
677 	// BuildMonitor implementation
678 	//////////////////////////////
679 
680 	/**
681 	 * {@inheritDoc}
682 	 */
683 	public void panic(String theErrorMessage)
684 	{
685 		showErrorMessage(theErrorMessage);
686 		// TODO: OPEN NEW EMAIL WITH ERROR MESSAGE ?
687 		System.exit(1);
688 	}
689 
690 	/**
691 	 * {@inheritDoc}
692 	 */
693 	public void panic(Throwable theUnexpectedError)
694 	{
695 		MessageFormat errorMessage = new MessageFormat(getMessage(MESSAGEKEY_UNEXPECTED_ERROR_MESSAGE));
696 		panic(errorMessage.format(new Object[] {theUnexpectedError.getMessage(), getStackTrace(theUnexpectedError)}));
697 	}
698 
699 	/**
700 	 * {@inheritDoc}
701 	 */
702 	public String getMessage(String theMessageKey)
703 	{
704 		return this.messages.getString(theMessageKey);
705 	}
706 
707 	/**
708 	 * {@inheritDoc}
709 	 */
710 	public Image getDialogsDefaultIcon()
711 	{
712 		return this.initialIcon;
713 	}
714 
715 	/**
716 	 * {@inheritDoc}
717 	 */
718 	public void reportMonitoringException(MonitoringException theMonitoringException)
719 	{
720 		// We only display the message if it is a new one (not the one currently displayed)
721 		if ((this.currentlyReportedMonitoringException == null) || (!this.currentlyReportedMonitoringException.equals(theMonitoringException.getMessage())))
722 		{
723 			// We have two messages: the one to display in the alert bubble, and the one to display in the tray icon tooltip
724 			String messageToDisplayInAlertBubble = theMonitoringException.getMessage();
725 			String tooltipMessage = theMonitoringException.getMessage();
726 			// The default action listener to use for the tray icon when the error is displayed
727 			ActionListener trayIconNewActionListener = this.openBuildServerHomePageActionListener;
728 
729 			if (theMonitoringException.isOptionsRelated())
730 			{
731 				// The error is related to the options set by the end user: we inform the user in the displayed messages that he can double click the alert
732 				// bubble or the tray icon to open the options dialog
733 				messageToDisplayInAlertBubble += OPTIONS_RELATED_MESSAGES_SUFFIX;
734 				// There is a maximum length for a tooltip message: truncate it if necessary
735 				if (tooltipMessage != null && (tooltipMessage.length() + OPTIONS_RELATED_MESSAGES_SUFFIX.length()) > TOOLTIP_MAX_LENGTH)
736 				{
737 					tooltipMessage = tooltipMessage.substring(0, TOOLTIP_MAX_LENGTH - TRUNCATED_MESSAGE_SUFFIX.length() - OPTIONS_RELATED_MESSAGES_SUFFIX.length() - 1) + TRUNCATED_MESSAGE_SUFFIX + OPTIONS_RELATED_MESSAGES_SUFFIX;
738 				}
739 				else if (tooltipMessage != null)
740 				{
741 					tooltipMessage += OPTIONS_RELATED_MESSAGES_SUFFIX;
742 				}
743 				// Setup the tray icon action listener so that it opens the options dialog
744 				trayIconNewActionListener = this.openOptionsDialogActionListener;
745 			}
746 			else
747 			{
748 				// There is a maximum length for a tooltip message: truncate it if necessary
749 				if (tooltipMessage != null && tooltipMessage.length() > TOOLTIP_MAX_LENGTH)
750 				{
751 					tooltipMessage = tooltipMessage.substring(0, TOOLTIP_MAX_LENGTH - TRUNCATED_MESSAGE_SUFFIX.length() - 1) + TRUNCATED_MESSAGE_SUFFIX;
752 				}
753 				// If there is a related URI for the Exception, setup the tray icon action listener so that it opens it in a web browser
754 				if (theMonitoringException.getRelatedURI() != null)
755 				{
756 					trayIconNewActionListener = new OpenURIInBrowserActionListener(theMonitoringException.getRelatedURI());
757 				}
758 			}
759 			javax.swing.SwingUtilities.invokeLater(new TrayIconUpdater(this.monitoringExceptionIcon, tooltipMessage, "Build Monitor need your attention", messageToDisplayInAlertBubble, MessageType.ERROR, trayIconNewActionListener));
760 			this.currentlyReportedMonitoringException = theMonitoringException.getMessage();
761 		}
762 	}
763 
764 	/**
765 	 * {@inheritDoc}
766 	 */
767 	public void reportConfigurationUpdatedToBeTakenIntoAccountImmediately()
768 	{
769 		// Interrupt the monitor thread so that it takes into account its new configuration
770 		if (this.monitorThread != null)
771 		{
772 			this.monitorThread.interrupt();
773 		}
774 	}
775 
776 	/**
777 	 * TODO: TRES TRES CHAUD... A TESTER DE MANIERE AUTOMATISEE !!!!
778 	 * {@inheritDoc}
779 	 */
780 	public void updateBuildStatus(List<BuildReport> theBuildsStatus)
781 	{
782 		// 1) Sort the list of build reports according to user preferences (as set using the sort menu)
783 		// TODO: POUVOIR AUSSI TRIER LA LISTE PAR AGE, EN FONCTION DE L'OPTION CHOISIE PAR L'UTILISATEUR
784 		List<BuildReport> buildsStatus = new ArrayList<BuildReport>(theBuildsStatus);
785 		if (this.currentSortOrder == SORT_BY_NAME)
786 		{
787 			Collections.sort(buildsStatus, new BuildReport.NameComparator());
788 		}
789 		else if (this.currentSortOrder == SORT_BY_AGE)
790 		{
791 			Collections.sort(buildsStatus, new BuildReport.AgeComparator());
792 		}
793 		
794 		// 2) Update the tray menu
795 		BuildStatusUpdater updater = new BuildStatusUpdater(buildsStatus);
796 		javax.swing.SwingUtilities.invokeLater(updater);
797 		
798 		// 3) Status has been updated, so there is no current monitoring exception...
799 		this.currentlyReportedMonitoringException = null;
800 		
801 		// 4) If situation have changed, notify the end user !
802 		StringBuffer newFailingBuilds = new StringBuffer();
803 		StringBuffer fixedBuilds = new StringBuffer();
804 		for (BuildReport currentBuildReport : theBuildsStatus)
805 		{
806 			for (BuildReport previousBuildReport : this.previousBuildReports)
807 			{
808 				if (previousBuildReport.getId().equals(currentBuildReport.getId()))
809 				{
810 					if (previousBuildReport.getStatus() != currentBuildReport.getStatus())
811 					{
812 						if (currentBuildReport.getStatus() == BuildReport.Status.OK)
813 						{
814 							fixedBuilds.append(currentBuildReport.getName() + " is fixed.\n");
815 						}
816 						else
817 						{
818 							newFailingBuilds.append(currentBuildReport.getName() + " is failing.\n");
819 						}
820 					}
821 				}
822 			}
823 		}
824 		if ((newFailingBuilds.length() > 0) || (fixedBuilds.length() > 0))
825 		{
826 			MessageType messageType = MessageType.INFO;
827 			if (newFailingBuilds.length() > 0)
828 			{
829 				messageType = MessageType.WARNING;
830 			}
831 			javax.swing.SwingUtilities.invokeLater(new TrayIconUpdater(null, null, "Build situation have changed !", newFailingBuilds.toString() + fixedBuilds.toString() + "Right click the tray icon to display the detailed build status.", messageType, null));
832 		}
833 		this.previousBuildReports = buildsStatus;
834 	}
835 
836 
837 	/**
838 	 * displays an error message in a dialog.
839 	 * @param theErrorMessage the error message to display
840 	 */
841 	protected void showErrorMessage(String theErrorMessage)
842 	{
843 		if (this.messages != null)
844 		{
845 			JOptionPane.showMessageDialog(null, theErrorMessage, getMessage(MESSAGEKEY_ERROR_DIALOG_TITLE), JOptionPane.ERROR_MESSAGE);		
846 		}
847 		else
848 		{
849 			JOptionPane.showMessageDialog(null, theErrorMessage, "Arghhhhhhhhh !", JOptionPane.ERROR_MESSAGE);		
850 		}
851 	}
852 	
853 	/**
854 	 * Return the stack trace of a throwable as a String
855 	 * @param theThrowable
856 	 * @return
857 	 */
858 	protected String getStackTrace(Throwable theThrowable)
859 	{
860 		StringBuffer buffer = new StringBuffer();
861 		StackTraceElement[] stackElements = theThrowable.getStackTrace();
862 		for (int i=0; i < stackElements.length; i++)
863 		{
864 			buffer.append(stackElements[i] + "\n");
865 		}
866 		return buffer.toString();
867 	}
868 
869 }