View Javadoc

1   /*
2    * This software was designed and created by Jason Carroll.
3    * Copyright (c) 2002, 2003, 2004 Jason Carroll.
4    * The author can be reached at jcarroll@cowsultants.com
5    * ITracker website: http://www.cowsultants.com
6    * ITracker forums: http://www.cowsultants.com/phpBB/index.php
7    *
8    * This program is free software; you can redistribute it and/or modify
9    * it only under the terms of the GNU General Public License as published by
10   * the Free Software Foundation; either version 2 of the License, or
11   * (at your option) any later version.
12   *
13   * This program is distributed in the hope that it will be useful,
14   * but WITHOUT ANY WARRANTY; without even the implied warranty of
15   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   * GNU General Public License for more details.
17   */
18  
19  package org.itracker.services.implementations;
20  
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.Date;
25  import java.util.HashSet;
26  import java.util.Hashtable;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Set;
32  
33  import javax.mail.internet.InternetAddress;
34  
35  import org.apache.log4j.Logger;
36  import org.itracker.core.resources.ITrackerResources;
37  import org.itracker.model.Component;
38  import org.itracker.model.Issue;
39  import org.itracker.model.IssueActivity;
40  import org.itracker.model.IssueHistory;
41  import org.itracker.model.Notification;
42  import org.itracker.model.Project;
43  import org.itracker.model.User;
44  import org.itracker.model.Version;
45  import org.itracker.model.Notification.Role;
46  import org.itracker.model.Notification.Type;
47  import org.itracker.persistence.dao.IssueActivityDAO;
48  import org.itracker.persistence.dao.IssueDAO;
49  import org.itracker.persistence.dao.NotificationDAO;
50  import org.itracker.services.IssueService;
51  import org.itracker.services.NotificationService;
52  import org.itracker.services.ProjectService;
53  import org.itracker.services.util.EmailService;
54  import org.itracker.services.util.HTMLUtilities;
55  import org.itracker.services.util.IssueUtilities;
56  import org.itracker.services.util.ProjectUtilities;
57  import org.itracker.services.util.UserUtilities;
58  import org.itracker.web.util.ServletContextUtils;
59  
60  public class NotificationServiceImpl implements NotificationService {
61  
62  	// TODO: Cleanup this file, go through all issues, todos, etc.
63  	
64  	private EmailService emailService;
65  	private NotificationDAO notificationDao;
66  	private ProjectService projectService;
67  	private IssueActivityDAO issueActivityDao;
68  	private IssueDAO issueDao;
69  
70  	private static final Logger logger = Logger
71  			.getLogger(NotificationServiceImpl.class);
72  
73  	public NotificationServiceImpl() {
74  
75  		this.emailService = null;
76  		this.projectService = null;
77  		this.notificationDao = null;
78  	}
79  
80  	public NotificationServiceImpl(EmailService emailService,
81  			ProjectService projectService, NotificationDAO notificationDao, IssueActivityDAO issueActivityDao, IssueDAO issueDao) {
82  		this();
83  		this.setEmailService(emailService);
84  		this.setProjectService(projectService);
85  		this.setNotificationDao(notificationDao);
86  		this.setIssueActivityDao(issueActivityDao);
87  		this.setIssueDao(issueDao);
88  	}
89  
90  	public void sendNotification(Notification notification, Type type,
91  			String url) {
92  
93  		if (logger.isDebugEnabled()) {
94  			logger.debug("sendNotification: called with notification: "
95  					+ notification + ", type: " + url + ", url: " + url);
96  		}
97  		if (null == notification) {
98  			throw new IllegalArgumentException("notification must not be null");
99  		}
100 		if (null == this.emailService || null == this.notificationDao) {
101 			throw new IllegalStateException("service not initialized yet");
102 		}
103 		if (type == Type.SELF_REGISTER) {
104 			this.handleSelfRegistrationNotification(notification.getUser()
105 					.getLogin(), notification.getUser().getEmailAddress(), url);
106 		} else {
107 			handleIssueNotification(notification.getIssue(), type, url);
108 
109 		}
110 
111 	}
112 
113 	public void sendNotification(Issue issue, Type type, String baseURL) {
114 		if (logger.isDebugEnabled()) {
115 			logger.debug("sendNotification: called with issue: " + issue
116 					+ ", type: " + type + ", baseURL: " + baseURL);
117 		}
118 		handleIssueNotification(issue, type, baseURL);
119 
120 	}
121 
122 	public void setEmailService(EmailService emailService) {
123 
124 		if (null == emailService)
125 			throw new IllegalArgumentException("email service must not be null");
126 
127 		if (null != this.emailService) {
128 			throw new IllegalStateException("email service allready set");
129 		}
130 		this.emailService = emailService;
131 
132 	}
133 
134 	/**
135 	 * 
136 	 * @param notificationMsg
137 	 * @param url
138 	 */
139 	private void handleSelfRegistrationNotification(String login,
140 			InternetAddress toAddress, String url) {
141 		if (logger.isDebugEnabled()) {
142 			logger
143 					.debug("handleSelfRegistrationNotification: called with login: "
144 							+ login
145 							+ ", toAddress"
146 							+ toAddress
147 							+ ", url: "
148 							+ url);
149 		}
150 		try {
151 
152 			if (toAddress != null && !"".equals(toAddress.getAddress())) {
153 				String subject = ITrackerResources
154 						.getString("itracker.email.selfreg.subject");
155 				String msgText = ITrackerResources.getString(
156 						"itracker.email.selfreg.body", ITrackerResources
157 								.getDefaultLocale(), new Object[] { login,
158 								url + "/login.do" });
159 				emailService.sendEmail(toAddress, subject, msgText);
160 			} else {
161 				throw new IllegalArgumentException(
162 						"To-address must be set for self registration notification.");
163 			}
164 		} catch (RuntimeException e) {
165 			logger.error("failed to handle self registration notification for "
166 					+ toAddress, e);
167 			throw e;
168 		}
169 	}
170 
171 	/**
172 	 * Method for internal sending of a notification of specific type.
173 	 * 
174 	 * @param notificationMsg
175 	 * @param type
176 	 * @param url
177 	 */
178 	private void handleIssueNotification(Issue issue, Type type, String url) {
179 
180 		if (logger.isDebugEnabled()) {
181 			logger.debug("handleIssueNotification: called with issue: " + issue
182 					+ ", type: " + type + "url: " + url);
183 		}
184 		this.handleIssueNotification(issue, type, url, null, null);
185 	}
186 
187 	/**
188 	 * Method for internal sending of a notification of specific type.
189 	 * 
190 	 * @param notificationMsg
191 	 * @param type
192 	 * @param url
193 	 */
194 	private void handleIssueNotification(Issue issue, Type type, String url,
195 			InternetAddress[] receipients, Integer lastModifiedDays) {
196 		try {
197 
198 			if (logger.isDebugEnabled()) {
199 				logger
200 						.debug("handleIssueNotificationhandleIssueNotification: called with issue: "
201 								+ issue
202 								+ ", type: "
203 								+ type
204 								+ "url: "
205 								+ url
206 								+ ", receipients: "
207 								+ (null == receipients ? "<null>" : String
208 										.valueOf(Arrays.asList(receipients)))
209 								+ ", lastModifiedDays: " + lastModifiedDays);
210 			}
211 			List<Notification> notifications;
212 
213 			if (issue == null) {
214 				logger
215 						.warn("handleIssueNotification: issue was null. Notification will not be handled");
216 				return;
217 			}
218 
219 			if (lastModifiedDays == null || lastModifiedDays.intValue() < 0) {
220 				lastModifiedDays = Integer
221 						.valueOf(org.itracker.web.scheduler.tasks.ReminderNotification.DEFAULT_ISSUE_AGE);
222 			}
223 
224 			if (receipients == null) {
225 				ArrayList<InternetAddress> recList = new ArrayList<InternetAddress>();
226 				notifications = this.getIssueNotifications(issue);
227 				Iterator<Notification> it = notifications.iterator();
228 				User currentUser;
229 				while (it.hasNext()) {
230 					currentUser = it.next().getUser();
231 					if (null != currentUser
232 							&& null != currentUser.getEmailAddress()
233 							&& null != currentUser.getEmail()
234 							&& (!recList
235 									.contains(currentUser.getEmailAddress()))
236 							&& currentUser.getEmail().indexOf('@') >= 0) {
237 
238 						recList.add(currentUser.getEmailAddress());
239 					}
240 				}
241 				receipients = recList.toArray(new InternetAddress[] {});
242 			}
243 
244 			List<IssueActivity> activity = getIssueService().getIssueActivity(
245 					issue.getId(), false);
246 			issue.getActivities();
247 			List<IssueHistory> histories = issue.getHistory();
248 			Iterator<IssueHistory> it = histories.iterator();
249 			IssueHistory history = null, currentHistory;
250 			history = getIssueService().getLastIssueHistory(issue.getId());
251 
252 			Integer historyId = 0;
253 			// find history with greatest id
254 			while (it.hasNext()) {
255 				currentHistory = (IssueHistory) it.next();
256 				if (logger.isDebugEnabled()) {
257 					logger.debug("handleIssueNotification: found history: "
258 							+ currentHistory.getDescription() + " (time: "
259 							+ currentHistory.getCreateDate());
260 				}
261 				if (currentHistory.getId() > historyId) {
262 					historyId = currentHistory.getId();
263 					history = currentHistory;
264 				}
265 			}
266 			if (logger.isDebugEnabled() && null != history) {
267 				logger
268 						.debug("handleIssueNotification: got most recent history: "
269 								+ history
270 								+ " ("
271 								+ history.getDescription()
272 								+ ")");
273 			}
274 
275 			List<Component> components = issue.getComponents();
276 			// issueService
277 			// .getIssueComponents(issue.getId());
278 			
279 			List<Version> versions = issue.getVersions();
280 			// issueService.getIssueVersions(issue.getId());
281 			
282 			if (receipients.length > 0) {
283 				String subject = "";
284 				if (type == Type.CREATED) {
285 					subject = ITrackerResources.getString(
286 							"itracker.email.issue.subject.created",
287 							new Object[] { issue.getId(),
288 									issue.getProject().getName(),
289 									lastModifiedDays });
290 				} else if (type == Type.ASSIGNED) {
291 					subject = ITrackerResources.getString(
292 							"itracker.email.issue.subject.assigned",
293 							new Object[] { issue.getId(),
294 									issue.getProject().getName(),
295 									lastModifiedDays });
296 				} else if (type == Type.CLOSED) {
297 					subject = ITrackerResources.getString(
298 							"itracker.email.issue.subject.closed",
299 							new Object[] { issue.getId(),
300 									issue.getProject().getName(),
301 									lastModifiedDays });
302 				} else if (type == Type.ISSUE_REMINDER) {
303 					subject = ITrackerResources.getString(
304 							"itracker.email.issue.subject.reminder",
305 							new Object[] { issue.getId(),
306 									issue.getProject().getName(),
307 									lastModifiedDays });
308 				} else {
309 					subject = ITrackerResources.getString(
310 							"itracker.email.issue.subject.updated",
311 							new Object[] { issue.getId(),
312 									issue.getProject().getName(),
313 									lastModifiedDays });
314 				}
315 
316 				String activityString;
317 				String componentString = "";
318 				String versionString = "";
319 				StringBuffer sb = new StringBuffer();
320 				for (int i = 0; i < activity.size(); i++) {
321 					sb.append(
322 							IssueUtilities.getActivityName(activity.get(i)
323 									.getActivityType())).append(": ").append(
324 							activity.get(i).getDescription()).append("\n");
325 
326 				}
327 				activityString = sb.toString();
328 				for (int i = 0; i < components.size(); i++) {
329 					componentString += (i != 0 ? ", " : "")
330 							+ components.get(i).getName();
331 				}
332 				for (int i = 0; i < versions.size(); i++) {
333 					versionString += (i != 0 ? ", " : "")
334 							+ versions.get(i).getNumber();
335 				}
336 
337 				String msgText = "";
338 				if (type == Type.ISSUE_REMINDER) {
339 					msgText = ITrackerResources
340 							.getString(
341 									"itracker.email.issue.body.reminder",
342 									new Object[] {
343 											url
344 													+ "/module-projects/view_issue.do?id="
345 													+ issue.getId(),
346 											issue.getProject().getName(),
347 											issue.getDescription(),
348 											IssueUtilities.getStatusName(issue
349 													.getStatus()),
350 											IssueUtilities
351 													.getSeverityName(issue
352 															.getSeverity()),
353 											(issue.getOwner().getFirstName() != null ? issue
354 													.getOwner().getFirstName()
355 													: "")
356 													+ " "
357 													+ (issue.getOwner()
358 															.getLastName() != null ? issue
359 															.getOwner()
360 															.getLastName()
361 															: ""),
362 											componentString,
363 											(history == null ? "" : history
364 													.getUser().getFirstName()
365 													+ " "
366 													+ history.getUser()
367 															.getLastName()),
368 											(history == null ? ""
369 													: HTMLUtilities
370 															.removeMarkup(history
371 																	.getDescription())),
372 											lastModifiedDays, activityString });
373 				} else {
374 					String resolution = (issue.getResolution() == null ? ""
375 							: issue.getResolution());
376 					if (!resolution.equals("")
377 							&& ProjectUtilities
378 									.hasOption(
379 											ProjectUtilities.OPTION_PREDEFINED_RESOLUTIONS,
380 											issue.getProject().getOptions())) {
381 						resolution = IssueUtilities.getResolutionName(
382 								resolution, ITrackerResources.getLocale());
383 					}
384 
385 					msgText = ITrackerResources
386 							.getString(
387 									"itracker.email.issue.body.standard",
388 									new Object[] {
389 											new StringBuffer(url).append("/module-projects/view_issue.do?id=").append(issue.getId()).toString(),
390 											issue.getProject().getName(),
391 											issue.getDescription(),
392 											IssueUtilities.getStatusName(issue
393 													.getStatus()),
394 											resolution,
395 											IssueUtilities
396 													.getSeverityName(issue
397 															.getSeverity()),
398 											(null != issue.getOwner() && null != issue.getOwner().getFirstName() ? issue
399 													.getOwner().getFirstName()
400 													: "")
401 													+ " "
402 													+ (null != issue.getOwner() && null != issue.getOwner()
403 															.getLastName() ? issue
404 															.getOwner()
405 															.getLastName()
406 															: ""),
407 											componentString,
408 											(history == null ? "" : history
409 													.getUser().getFirstName()
410 													+ " "
411 													+ history.getUser()
412 															.getLastName()),
413 											(history == null ? ""
414 													: HTMLUtilities
415 															.removeMarkup(history
416 																	.getDescription())),
417 											activityString });
418 				}
419 				emailService.sendEmail(receipients, subject, msgText);
420 
421 				updateIssueActivityNotification(issue, true);
422 			}
423 
424 		} catch (Exception e) {
425 			logger
426 					.error(
427 							"handleIssueNotification: unexpected exception caught, throwing runtime exception",
428 							e);
429 			throw new RuntimeException(e);
430 		}
431 	}
432 
433 
434 	/**
435 	 * Method for internal sending of a notification of specific type.
436 	 * 
437 	 * TODO: final debugging/integration/implementation
438 	 * TODO: Decide if this code is really needed and document for what
439 	 * 
440 	 * @param notificationMsg
441 	 * @param type
442 	 * @param url
443 	 */
444 	
445 	@SuppressWarnings("unused")
446 	private void handleLocalizedIssueNotification(final Issue issue, final Type type, final String url,
447 			final InternetAddress[] receipients, Integer lastModifiedDays) {
448 		try {
449 			
450 			if (logger.isDebugEnabled()) {
451 				logger
452 						.debug("handleIssueNotificationhandleIssueNotification: running as thread, called with issue: "
453 								+ issue
454 								+ ", type: "
455 								+ type
456 								+ "url: "
457 								+ url
458 								+ ", receipients: "
459 								+ (null == receipients ? "<null>" : String
460 										.valueOf(Arrays.asList(receipients)))
461 								+ ", lastModifiedDays: " + lastModifiedDays);
462 			}
463 			
464 			final Integer notModifiedSince;
465 			
466 			if (lastModifiedDays == null || lastModifiedDays.intValue() < 0) {
467 				notModifiedSince = Integer
468 						.valueOf(org.itracker.web.scheduler.tasks.ReminderNotification.DEFAULT_ISSUE_AGE);
469 			} else {
470 				notModifiedSince = lastModifiedDays;
471 			}
472 
473 					try {
474 						if (logger.isDebugEnabled()) {
475 							logger
476 									.debug("handleIssueNotificationhandleIssueNotification.run: running as thread, called with issue: "
477 											+ issue
478 											+ ", type: "
479 											+ type
480 											+ "url: "
481 											+ url
482 											+ ", receipients: "
483 											+ (null == receipients ? "<null>" : String
484 													.valueOf(Arrays.asList(receipients)))
485 											+ ", notModifiedSince: " + notModifiedSince);
486 						}
487 						final List<Notification> notifications;
488 						if (issue == null) {
489 							logger
490 									.warn("handleIssueNotification: issue was null. Notification will not be handled");
491 							return;
492 						}
493 						Map<InternetAddress, Locale> localeMapping = null;
494 			
495 						if (receipients == null) {
496 							
497 							
498 							notifications = this.getIssueNotifications(issue);
499 							
500 							localeMapping = new Hashtable<InternetAddress, Locale>(notifications.size());
501 							Iterator<Notification> it = notifications.iterator();
502 							User currentUser;
503 							while (it.hasNext()) {
504 								currentUser = it.next().getUser();
505 								if (null != currentUser
506 										&& null != currentUser.getEmailAddress()
507 										&& null != currentUser.getEmail()
508 										&& (!localeMapping.keySet()
509 												.contains(currentUser.getEmailAddress()))) {
510 									
511 									try {
512 										localeMapping.put(currentUser.getEmailAddress(), ITrackerResources.getLocale(currentUser.getPreferences().getUserLocale()));
513 									} catch (RuntimeException re) {
514 										localeMapping.put(currentUser.getEmailAddress(), ITrackerResources.getLocale());
515 									}
516 								}
517 							}
518 						} else {
519 							localeMapping = new Hashtable<InternetAddress, Locale>(1);
520 							Locale locale = ITrackerResources.getLocale();
521 							Iterator<InternetAddress> it = Arrays.asList(receipients).iterator();
522 							while (it.hasNext()) {
523 								InternetAddress internetAddress = (InternetAddress) it
524 										.next();
525 								localeMapping.put(internetAddress, locale);
526 							}
527 						}
528 			
529 						this.handleNotification(issue, type, notModifiedSince, localeMapping, url);
530 					} catch (Exception e) {
531 						logger.error("run: failed to process notification", e);
532 					}	
533 
534 		} catch (Exception e) {
535 			logger
536 					.error(
537 							"handleIssueNotification: unexpected exception caught, throwing runtime exception",
538 							e);
539 			throw new RuntimeException(e);
540 		}
541 	}
542 	
543 	/**
544 	 * Send notifications to mapped addresses by locale.
545 	 * @param issue
546 	 * @param type
547 	 * @param notModifiedSince
548 	 * @param recipientsLocales
549 	 * @param url
550 	 */
551 	private void handleNotification(Issue issue, Type type, Integer notModifiedSince, Map<InternetAddress, Locale> recipientsLocales, final String url) {
552 		Set<InternetAddress> recipients = recipientsLocales.keySet();
553 		Map<Locale, Set<InternetAddress>> localeRecipients = new Hashtable<Locale, Set<InternetAddress>>();
554 
555 		List<Component> components = issue.getComponents();
556 		List<Version> versions = issue.getVersions();
557 		
558 		List<IssueActivity> activity = getIssueService().getIssueActivity(
559 				issue.getId(), false);
560 		issue.getActivities();
561 		List<IssueHistory> histories = issue.getHistory();
562 		Iterator<IssueHistory> it = histories.iterator();
563 		IssueHistory history = null, currentHistory;
564 		history = getIssueService().getLastIssueHistory(issue.getId());
565 
566 		Integer historyId = 0;
567 		// find history with greatest id
568 		while (it.hasNext()) {
569 			currentHistory = (IssueHistory) it.next();
570 			if (logger.isDebugEnabled()) {
571 				logger.debug("handleIssueNotification: found history: "
572 						+ currentHistory.getDescription() + " (time: "
573 						+ currentHistory.getCreateDate());
574 			}
575 			if (currentHistory.getId() > historyId) {
576 				historyId = currentHistory.getId();
577 				history = currentHistory;
578 			}
579 		}
580 		if (logger.isDebugEnabled() && null != history) {
581 			logger
582 					.debug("handleIssueNotification: got most recent history: "
583 							+ history
584 							+ " ("
585 							+ history.getDescription()
586 							+ ")");
587 		}
588 		
589 		Iterator<InternetAddress> iaIt = recipientsLocales.keySet().iterator();
590 		while (iaIt.hasNext()) {
591 			InternetAddress internetAddress = (InternetAddress) iaIt.next();
592 			if (localeRecipients.keySet().contains(recipientsLocales.get(internetAddress))) {
593 				localeRecipients.get(recipientsLocales.get(internetAddress)).add(internetAddress);
594 			} else {
595 				Set<InternetAddress> addresses = new HashSet<InternetAddress>();
596 				localeRecipients.put(recipientsLocales.get(internetAddress), addresses);
597 			}
598 		}
599 		
600 		Iterator<Locale> localesIt = localeRecipients.keySet().iterator();
601 		try {
602 			while (localesIt.hasNext()) {
603 				Locale currentLocale = (Locale) localesIt.next();
604 				recipients = localeRecipients.get(currentLocale);
605 				
606 				
607 				if (recipients.size() > 0) {
608 					String subject = "";
609 					if (type == Type.CREATED) {
610 						subject = ITrackerResources.getString(
611 								"itracker.email.issue.subject.created",
612 								currentLocale,
613 								new Object[] { issue.getId(),
614 										issue.getProject().getName(),
615 										notModifiedSince });
616 					} else if (type == Type.ASSIGNED) {
617 						subject = ITrackerResources.getString(
618 								"itracker.email.issue.subject.assigned",
619 								currentLocale,
620 								new Object[] { issue.getId(),
621 										issue.getProject().getName(),
622 										notModifiedSince });
623 					} else if (type == Type.CLOSED) {
624 						subject = ITrackerResources.getString(
625 								"itracker.email.issue.subject.closed",
626 								currentLocale,
627 								new Object[] { issue.getId(),
628 										issue.getProject().getName(),
629 										notModifiedSince });
630 					} else if (type == Type.ISSUE_REMINDER) {
631 						subject = ITrackerResources.getString(
632 								"itracker.email.issue.subject.reminder",
633 								currentLocale,
634 								new Object[] { issue.getId(),
635 										issue.getProject().getName(),
636 										notModifiedSince });
637 					} else {
638 						subject = ITrackerResources.getString(
639 								"itracker.email.issue.subject.updated",
640 								currentLocale,
641 								new Object[] { issue.getId(),
642 										issue.getProject().getName(),
643 										notModifiedSince });
644 					}
645 	
646 					String activityString;
647 					String componentString = "";
648 					String versionString = "";
649 					StringBuffer sb = new StringBuffer();
650 					for (int i = 0; i < activity.size(); i++) {
651 						sb.append(
652 								IssueUtilities.getActivityName(activity.get(i)
653 										.getActivityType(), currentLocale)).append(": ").append(
654 								activity.get(i).getDescription()).append("\n");
655 	
656 					}
657 					
658 					activityString = sb.toString();
659 					// TODO localize..
660 					for (int i = 0; i < components.size(); i++) {
661 						componentString += (i != 0 ? ", " : "")
662 								+ components.get(i).getName();
663 					}
664 					for (int i = 0; i < versions.size(); i++) {
665 						versionString += (i != 0 ? ", " : "")
666 								+ versions.get(i).getNumber();
667 					}
668 	
669 					String msgText = "";
670 					if (type == Type.ISSUE_REMINDER) {
671 						msgText = ITrackerResources
672 								.getString(
673 										"itracker.email.issue.body.reminder",
674 										currentLocale,
675 										new Object[] {
676 												url
677 														+ "/module-projects/view_issue.do?id="
678 														+ issue.getId(),
679 												issue.getProject().getName(),
680 												issue.getDescription(),
681 												IssueUtilities.getStatusName(issue
682 														.getStatus()),
683 												IssueUtilities
684 														.getSeverityName(issue
685 																.getSeverity()),
686 												(issue.getOwner().getFirstName() != null ? issue
687 														.getOwner().getFirstName()
688 														: "")
689 														+ " "
690 														+ (issue.getOwner()
691 																.getLastName() != null ? issue
692 																.getOwner()
693 																.getLastName()
694 																: ""),
695 												componentString,
696 												(history == null ? "" : history
697 														.getUser().getFirstName()
698 														+ " "
699 														+ history.getUser()
700 																.getLastName()),
701 												(history == null ? ""
702 														: HTMLUtilities
703 																.removeMarkup(history
704 																		.getDescription())),
705 																		notModifiedSince, activityString });
706 					} else {
707 						String resolution = (issue.getResolution() == null ? ""
708 								: issue.getResolution());
709 						if (!resolution.equals("")
710 								&& ProjectUtilities
711 										.hasOption(
712 												ProjectUtilities.OPTION_PREDEFINED_RESOLUTIONS,
713 												issue.getProject().getOptions())) {
714 							resolution = IssueUtilities.getResolutionName(
715 									resolution, ITrackerResources.getLocale());
716 						}
717 	
718 						msgText = ITrackerResources
719 								.getString(
720 										"itracker.email.issue.body.standard",
721 										currentLocale,
722 										new Object[] {
723 												new StringBuffer(url).append("/module-projects/view_issue.do?id=").append(issue.getId()).toString(),
724 												issue.getProject().getName(),
725 												issue.getDescription(),
726 												IssueUtilities.getStatusName(issue
727 														.getStatus()),
728 												resolution,
729 												IssueUtilities
730 														.getSeverityName(issue
731 																.getSeverity()),
732 												(null != issue.getOwner() && null != issue.getOwner().getFirstName() ? issue
733 														.getOwner().getFirstName()
734 														: "")
735 														+ " "
736 														+ (null != issue.getOwner() && null != issue.getOwner()
737 																.getLastName() ? issue
738 																.getOwner()
739 																.getLastName()
740 																: ""),
741 												componentString,
742 												(history == null ? "" : history
743 														.getUser().getFirstName()
744 														+ " "
745 														+ history.getUser()
746 																.getLastName()),
747 												(history == null ? ""
748 														: HTMLUtilities
749 																.removeMarkup(history
750 																		.getDescription())),
751 												activityString });
752 					}
753 					
754 					if (logger.isInfoEnabled()) {
755 						logger.info(new StringBuilder("handleNotification: sending notification for ").append(issue).append(" (").append(type).append(") to ").append(currentLocale).append("-users (").append(recipients + ")").toString());
756 						
757 					}
758 					
759 					emailService.sendEmail(recipients, subject, msgText);
760 	
761 					if (logger.isDebugEnabled()) {
762 						logger.debug("handleNotification: sent notification for " + issue);
763 					}
764 				}
765 	
766 				updateIssueActivityNotification(issue, true);
767 				if (logger.isDebugEnabled()) {
768 					logger.debug("handleNotification: sent notification for locales " + localeRecipients.keySet() + " recipients: " + localeRecipients.values());
769 				}
770 			}
771 		} catch (RuntimeException e) {
772 			logger.error("handleNotification: failed to notify: " + issue + " (locales: " + localeRecipients.keySet() + ")", e);
773 		
774 		}
775 		
776 		
777 	}
778 	private IssueService getIssueService() {
779 		return ServletContextUtils.getItrackerServices().getIssueService();
780 	}
781 
782 	public void updateIssueActivityNotification(Issue issue,
783 			Boolean notificationSent) {
784 		if (logger.isDebugEnabled()) {
785 			logger.debug("updateIssueActivityNotification: called with "
786 					+ issue + ", notificationSent: " + notificationSent);
787 		}
788 
789 		Collection<IssueActivity> activity = getIssueActivityDao()
790 				.findByIssueId(issue.getId());
791 
792 		for (Iterator<IssueActivity> iter = activity.iterator(); iter.hasNext();) {
793 
794 			((IssueActivity) iter.next()).setNotificationSent(notificationSent);
795 
796 		}
797 	}
798 
799 	/**
800 	 */
801 	public boolean addIssueNotification(Notification notification) {
802 		if (logger.isDebugEnabled()) {
803 			logger.debug("addIssueNotification: called with notification: "
804 					+ notification);
805 		}
806 		Issue issue = notification.getIssue();
807 		if (!issue.getNotifications().contains(notification)) {
808 			if (notification.getCreateDate() == null) {
809 				notification.setCreateDate(new Date());
810 			}
811 			if (notification.getLastModifiedDate() == null) {
812 				notification.setLastModifiedDate(new Date());
813 			}
814 	//		List<Notification> notifications = new ArrayList<Notification>();
815 			// TODO: check these 3 lines - do we need them?:
816 	//		notifications.addAll(issue.getNotifications());
817 	//		notifications.add(notification);
818 	//		issue.setNotifications(notifications);
819 			getNotificationDao().save(notification);
820 			// TODO: is it needed to update issue too?
821 			issue.getNotifications().add(notification);
822 			getIssueDao().merge(issue);
823 			
824 			return true;
825 		}
826 		if (logger.isDebugEnabled()) {
827 			logger.debug("addIssueNotification: attempted to add duplicate notification " + notification + " for issue: " + issue);
828 		}
829 		return false;
830 	}
831 
832 	/**
833 	 * 
834 	 */
835 	public List<Notification> getIssueNotifications(Issue issue,
836 			boolean primaryOnly, boolean activeOnly) {
837 		if (logger.isDebugEnabled()) {
838 			logger.debug("getIssueNotifications: called with issue: " + issue
839 					+ ", primaryOnly: " + primaryOnly + ", activeOnly: "
840 					+ activeOnly);
841 		}
842 		List<Notification> issueNotifications = new ArrayList<Notification>();
843 		if (issue == null) {
844 			logger.warn("getIssueNotifications: no issue, throwing exception");
845 			throw new IllegalArgumentException("issue must not be null");
846 		}
847 		if (!primaryOnly) {
848 			List<Notification> notifications = getNotificationDao()
849 					.findByIssueId(issue.getId());
850 
851 			for (Iterator<Notification> iterator = notifications.iterator(); iterator
852 					.hasNext();) {
853 				Notification notification = iterator.next();
854 				User notificationUser = notification.getUser();
855 
856 				if (!activeOnly
857 						|| notificationUser.getStatus() == UserUtilities.STATUS_ACTIVE) {
858 					issueNotifications.add(notification);
859 				}
860 			}
861 		}
862 
863 		// Now add in other notifications like owner, creator, project owners,
864 		// etc...
865 
866 		boolean hasOwner = false;
867 		// getIssueDAO().findByPrimaryKey(issueId);
868 		if (issue != null) {
869 			if (issue.getOwner() != null) {
870 				User ownerModel = issue.getOwner();
871 
872 				if (ownerModel != null
873 						&& (!activeOnly || ownerModel.getStatus() == UserUtilities.STATUS_ACTIVE)) {
874 					issueNotifications.add(new Notification(ownerModel, issue,
875 							Role.OWNER));
876 					hasOwner = true;
877 				}
878 			}
879 
880 			if (!primaryOnly || !hasOwner) {
881 				User creatorModel = issue.getCreator();
882 
883 				if (creatorModel != null
884 						&& (!activeOnly || creatorModel.getStatus() == UserUtilities.STATUS_ACTIVE)) {
885 					issueNotifications.add(new Notification(creatorModel,
886 							issue, Role.CREATOR));
887 				}
888 			}
889 
890 			Project project = getProjectService().getProject(
891 					issue.getProject().getId());
892 			Collection<User> projectOwners = project.getOwners();
893 
894 			for (Iterator<User> iterator = projectOwners.iterator(); iterator
895 					.hasNext();) {
896 				User projectOwner = (User) iterator.next();
897 
898 				if (projectOwner != null
899 						&& (!activeOnly || projectOwner.getStatus() == UserUtilities.STATUS_ACTIVE)) {
900 					issueNotifications.add(new Notification(projectOwner,
901 							issue, Role.PO));
902 				}
903 			}
904 		}
905 
906 		if (logger.isDebugEnabled()) {
907 			logger.debug("getIssueNotifications: returning "
908 					+ issueNotifications);
909 		}
910 		return issueNotifications;
911 	}
912 
913 	public List<Notification> getIssueNotifications(Issue issue) {
914 		if (logger.isDebugEnabled()) {
915 			logger.debug("getIssueNotifications: called with: " + issue);
916 		}
917 		return this.getIssueNotifications(issue, false, true);
918 	}
919 
920 	public List<Notification> getPrimaryIssueNotifications(Issue issue) {
921 		if (logger.isDebugEnabled()) {
922 			logger.debug("getPrimaryIssueNotifications: called with: " + issue);
923 		}
924 		return this.getIssueNotifications(issue, true, false);
925 	}
926 
927 	public boolean hasIssueNotification(Issue issue, Integer userId) {
928 		if (logger.isDebugEnabled()) {
929 			logger.debug("hasIssueNotification: called with: " + issue
930 					+ ", userId: " + userId);
931 		}
932 		return hasIssueNotification(issue, userId, Role.ANY);
933 	}
934 
935 	/**
936 	 * @param issueId
937 	 * @param userId
938 	 * @param role
939 	 * @return
940 	 */
941 	public boolean hasIssueNotification(Issue issue, Integer userId, Role role) {
942 
943 		if (issue != null && userId != null) {
944 
945 			List<Notification> notifications = getIssueNotifications(issue,
946 					false, false);
947 
948 			for (int i = 0; i < notifications.size(); i++) {
949 
950 				if (role == Role.ANY || notifications.get(i).getRole() == role) {
951 
952 					if (notifications.get(i).getUser().getId().equals(userId)) {
953 
954 						return true;
955 
956 					}
957 
958 				}
959 
960 			}
961 
962 		}
963 
964 		return false;
965 
966 	}
967 
968 	public boolean removeIssueNotification(Integer notificationId) {
969 		Notification notification = this.getNotificationDao().findById(
970 				notificationId);
971 		getNotificationDao().delete(notification);
972 		return true;
973 	}
974 
975 	public void sendNotification(Issue issue, Type type, String baseURL,
976 			InternetAddress[] receipients, Integer lastModifiedDays) {
977 		this.handleIssueNotification(issue, type, baseURL, receipients,
978 				lastModifiedDays);
979 
980 	}
981 
982 
983 
984 	/**
985 	 * @return the emailService
986 	 */
987 	public EmailService getEmailService() {
988 		return emailService;
989 	}
990 
991 	/**
992 	 * @return the notificationDao
993 	 */
994 	private NotificationDAO getNotificationDao() {
995 		return notificationDao;
996 	}
997 
998 	/**
999 	 * @return the projectService
1000 	 */
1001 	public ProjectService getProjectService() {
1002 		return projectService;
1003 	}
1004 
1005 	/**
1006 	 * @param projectService
1007 	 *            the projectService to set
1008 	 */
1009 	public void setProjectService(ProjectService projectService) {
1010 		this.projectService = projectService;
1011 	}
1012 
1013 	/**
1014 	 * @param notificationDao
1015 	 *            the notificationDao to set
1016 	 */
1017 	public void setNotificationDao(NotificationDAO notificationDao) {
1018 		if (null == notificationDao) {
1019 			throw new IllegalArgumentException(
1020 					"notification dao must not be null");
1021 		}
1022 		if (null != this.notificationDao) {
1023 			throw new IllegalStateException("notification dao allready set");
1024 		}
1025 		this.notificationDao = notificationDao;
1026 	}
1027 
1028 	/**
1029 	 * TODO url should be automatically generated by configuration (baseurl) and
1030 	 * notification (issue-id).
1031 	 * 
1032 	 * @param notificationId
1033 	 * @param url
1034 	 */
1035 	public void sendNotification(Integer notificationId, Type type, String url) {
1036 
1037 		Notification notification = notificationDao.findById(notificationId);
1038 		this.sendNotification(notification, type, url);
1039 
1040 	}
1041 
1042 	/**
1043 	 * @return the issueActivityDao
1044 	 */
1045 	public IssueActivityDAO getIssueActivityDao() {
1046 		return issueActivityDao;
1047 	}
1048 
1049 	/**
1050 	 * @param issueActivityDao
1051 	 *            the issueActivityDao to set
1052 	 */
1053 	public void setIssueActivityDao(IssueActivityDAO issueActivityDao) {
1054 		this.issueActivityDao = issueActivityDao;
1055 	}
1056 
1057 	/**
1058 	 * @return the issueDao
1059 	 */
1060 	public IssueDAO getIssueDao() {
1061 		return issueDao;
1062 	}
1063 
1064 	/**
1065 	 * @param issueDao
1066 	 *            the issueDao to set
1067 	 */
1068 	public void setIssueDao(IssueDAO issueDao) {
1069 		this.issueDao = issueDao;
1070 	}
1071 
1072 }