1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.itracker.services.implementations;
20
21 import org.apache.commons.lang.StringUtils;
22 import org.apache.log4j.Logger;
23 import org.itracker.core.resources.ITrackerResources;
24 import org.itracker.model.*;
25 import org.itracker.model.Notification.Role;
26 import org.itracker.model.Notification.Type;
27 import org.itracker.model.util.IssueUtilities;
28 import org.itracker.model.util.ProjectUtilities;
29 import org.itracker.model.util.UserUtilities;
30 import org.itracker.persistence.dao.IssueActivityDAO;
31 import org.itracker.persistence.dao.IssueDAO;
32 import org.itracker.persistence.dao.NotificationDAO;
33 import org.itracker.services.EmailService;
34 import org.itracker.services.IssueService;
35 import org.itracker.services.NotificationService;
36 import org.itracker.services.ProjectService;
37 import org.itracker.util.HTMLUtilities;
38 import org.springframework.beans.BeansException;
39 import org.springframework.context.ApplicationContext;
40 import org.springframework.context.ApplicationContextAware;
41
42 import javax.mail.internet.InternetAddress;
43 import java.net.MalformedURLException;
44 import java.util.*;
45
46 public class NotificationServiceImpl implements NotificationService, ApplicationContextAware {
47 public static final Integer DEFAULT_ISSUE_AGE = 30;
48
49
50 private EmailService emailService;
51 private NotificationDAO notificationDao;
52 private ProjectService projectService;
53 private IssueActivityDAO issueActivityDao;
54 private IssueDAO issueDao;
55
56
57 private String issueServiceName;
58
59 private static final Logger logger = Logger
60 .getLogger(NotificationServiceImpl.class);
61 private IssueService issueService;
62 private ApplicationContext applicationContext;
63
64 public NotificationServiceImpl() {
65
66 this.emailService = null;
67 this.projectService = null;
68 this.notificationDao = null;
69 }
70
71 @Override
72 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
73 this.applicationContext = applicationContext;
74 }
75
76 public NotificationServiceImpl(EmailService emailService,
77 ProjectService projectService, NotificationDAO notificationDao, IssueActivityDAO issueActivityDao, IssueDAO issueDao, IssueService issueService) {
78 this();
79 this.setEmailService(emailService);
80 this.setProjectService(projectService);
81 this.setNotificationDao(notificationDao);
82 this.setIssueActivityDao(issueActivityDao);
83 this.setIssueDao(issueDao);
84 this.setIssueService(issueService);
85 }
86
87 public void sendNotification(Notification notification, Type type,
88 String url) {
89
90 if (logger.isDebugEnabled()) {
91 logger.debug("sendNotification: called with notification: "
92 + notification + ", type: " + url + ", url: " + url);
93 }
94 if (null == notification) {
95 throw new IllegalArgumentException("notification must not be null");
96 }
97 if (null == this.emailService || null == this.notificationDao) {
98 throw new IllegalStateException("service not initialized yet");
99 }
100 if (type == Type.SELF_REGISTER) {
101 this.handleSelfRegistrationNotification(notification.getUser()
102 .getLogin(), notification.getUser().getEmailAddress(), notification.getUser().getPreferences().getUserLocale(), url);
103 } else {
104 handleIssueNotification(notification.getIssue(), type, url);
105
106 }
107
108 }
109
110 public void sendNotification(Issue issue, Type type, String baseURL) {
111 if (logger.isDebugEnabled()) {
112 logger.debug("sendNotification: called with issue: " + issue
113 + ", type: " + type + ", baseURL: " + baseURL);
114 }
115 handleIssueNotification(issue, type, baseURL);
116
117 }
118
119 public void setEmailService(EmailService emailService) {
120
121 if (null == emailService)
122 throw new IllegalArgumentException("email service must not be null");
123
124 if (null != this.emailService) {
125 throw new IllegalStateException("email service allready set");
126 }
127 this.emailService = emailService;
128
129 }
130
131
132 private void handleSelfRegistrationNotification(String login,
133 InternetAddress toAddress, String locale, String url) {
134 if (logger.isDebugEnabled()) {
135 logger
136 .debug("handleSelfRegistrationNotification: called with login: "
137 + login
138 + ", toAddress"
139 + toAddress
140 + ", url: "
141 + url);
142 }
143 try {
144
145 if (toAddress != null && !"".equals(toAddress.getAddress())) {
146 String subject = ITrackerResources
147 .getString("itracker.email.selfreg.subject", locale);
148 String msgText = ITrackerResources.getString(
149 "itracker.email.selfreg.body", locale, new Object[]{login,
150 url + "/login.do"});
151 emailService.sendEmail(toAddress, subject, msgText);
152 } else {
153 throw new IllegalArgumentException(
154 "To-address must be set for self registration notification.");
155 }
156 } catch (RuntimeException e) {
157 logger.error("failed to handle self registration notification for "
158 + toAddress, e);
159 throw e;
160 }
161 }
162
163
164
165
166 private void handleIssueNotification(Issue issue, Type type, String url) {
167
168 if (logger.isDebugEnabled()) {
169 logger.debug("handleIssueNotification: called with issue: " + issue
170 + ", type: " + type + "url: " + url);
171 }
172 this.handleLocalizedIssueNotification(issue, type, url, null, null);
173 }
174
175
176
177
178
179 private void handleLocalizedIssueNotification(final Issue issue, final Type type, final String url,
180 final InternetAddress[] recipients, Integer lastModifiedDays) {
181 try {
182
183 if (logger.isDebugEnabled()) {
184 logger
185 .debug("handleLocalizedIssueNotification: running as thread, called with issue: "
186 + issue
187 + ", type: "
188 + type
189 + "url: "
190 + url
191 + ", recipients: "
192 + (null == recipients ? "<null>" : String
193 .valueOf(Arrays.asList(recipients)))
194 + ", lastModifiedDays: " + lastModifiedDays);
195 }
196
197 final Integer notModifiedSince;
198
199 if (lastModifiedDays == null || lastModifiedDays < 0) {
200 notModifiedSince = DEFAULT_ISSUE_AGE;
201 } else {
202 notModifiedSince = lastModifiedDays;
203 }
204
205 try {
206 if (logger.isDebugEnabled()) {
207 logger
208 .debug("handleLocalizedIssueNotification.run: running as thread, called with issue: "
209 + issue
210 + ", type: "
211 + type
212 + "url: "
213 + url
214 + ", recipients: "
215 + (null == recipients ? "<null>" : String
216 .valueOf(Arrays.asList(recipients)))
217 + ", notModifiedSince: " + notModifiedSince);
218 }
219 final List<Notification> notifications;
220 if (issue == null) {
221 logger
222 .warn("handleLocalizedIssueNotification: issue was null. Notification will not be handled");
223 return;
224 }
225 Map<InternetAddress, Locale> localeMapping;
226
227 if (recipients == null) {
228 notifications = this.getIssueNotifications(issue);
229
230 localeMapping = new Hashtable<>(notifications.size());
231 Iterator<Notification> it = notifications.iterator();
232 User currentUser;
233 while (it.hasNext()) {
234 currentUser = it.next().getUser();
235 if (null != currentUser
236 && null != currentUser.getEmailAddress()
237 && null != currentUser.getEmail()
238 && (!localeMapping.keySet()
239 .contains(currentUser.getEmailAddress()))) {
240
241 try {
242 localeMapping.put(currentUser.getEmailAddress(), ITrackerResources.getLocale(currentUser.getPreferences().getUserLocale()));
243 } catch (RuntimeException re) {
244 localeMapping.put(currentUser.getEmailAddress(), ITrackerResources.getLocale());
245 }
246 }
247 }
248 } else {
249 localeMapping = new Hashtable<>(1);
250 Locale locale = ITrackerResources.getLocale();
251 for (InternetAddress internetAddress : Arrays.asList(recipients)) {
252 localeMapping.put(internetAddress, locale);
253 }
254 }
255
256 this.handleNotification(issue, type, localeMapping, url, notModifiedSince);
257 } catch (Exception e) {
258 logger.error("run: failed to process notification", e);
259 }
260
261 } catch (Exception e) {
262 logger
263 .error(
264 "handleLocalizedIssueNotification: unexpected exception caught, throwing runtime exception",
265 e);
266 throw new RuntimeException(e);
267 }
268 }
269
270 @Override
271 public void sendReminder(Issue issue, User user, String baseURL, int issueAge) {
272 Map<InternetAddress, Locale> recipient = new HashMap<>(1);
273 recipient.put(user.getEmailAddress(), ITrackerResources.getLocale(user.getPreferences().getUserLocale()));
274 handleNotification(issue, Type.ISSUE_REMINDER, recipient, baseURL, issueAge);
275 }
276
277
278
279
280 private void handleNotification(Issue issue, Type type, Map<InternetAddress, Locale> recipientsLocales, final String url, Integer notModifiedSince) {
281 Set<InternetAddress> recipients;
282 Map<Locale, Set<InternetAddress>> localeRecipients = new Hashtable<>();
283
284 List<Component> components = issue.getComponents();
285
286 List<IssueActivity> activity = getIssueService().getIssueActivity(
287 issue.getId(), false);
288
289 IssueHistory history;
290 history = getIssueService().getLastIssueHistory(issue.getId());
291 StringBuilder recipientsString = new StringBuilder();
292
293 if (logger.isDebugEnabled() && null != history) {
294 logger.debug("handleIssueNotification: got most recent history: "
295 + history
296 + " ("
297 + history.getDescription()
298 + ")");
299 }
300
301 for (InternetAddress internetAddress : recipientsLocales.keySet()) {
302 recipientsString.append("\n ");
303 recipientsString.append(internetAddress.getPersonal());
304
305 if (localeRecipients.keySet().contains(recipientsLocales.get(internetAddress))) {
306 localeRecipients.get(recipientsLocales.get(internetAddress)).add(internetAddress);
307 } else {
308 Set<InternetAddress> addresses = new HashSet<>();
309 addresses.add(internetAddress);
310 localeRecipients.put(recipientsLocales.get(internetAddress), addresses);
311 }
312 }
313
314 Iterator<Locale> localesIt = localeRecipients.keySet().iterator();
315 try {
316 while (localesIt.hasNext()) {
317 Locale currentLocale = localesIt.next();
318 recipients = localeRecipients.get(currentLocale);
319
320
321 if (recipients.size() > 0) {
322 String subject;
323 if (type == Type.CREATED) {
324 subject = ITrackerResources.getString(
325 "itracker.email.issue.subject.created",
326 currentLocale,
327 new Object[]{issue.getId(),
328 issue.getProject().getName()});
329 } else if (type == Type.ASSIGNED) {
330 subject = ITrackerResources.getString(
331 "itracker.email.issue.subject.assigned",
332 currentLocale,
333 new Object[]{issue.getId(),
334 issue.getProject().getName()});
335 } else if (type == Type.CLOSED) {
336 subject = ITrackerResources.getString(
337 "itracker.email.issue.subject.closed",
338 currentLocale,
339 new Object[]{issue.getId(),
340 issue.getProject().getName()});
341 } else if (type == Type.ISSUE_REMINDER) {
342 subject = ITrackerResources.getString(
343 "itracker.email.issue.subject.reminder",
344 currentLocale,
345 new Object[]{issue.getId(),
346 issue.getProject().getName(),
347 notModifiedSince});
348 } else {
349 subject = ITrackerResources.getString(
350 "itracker.email.issue.subject.updated",
351 currentLocale,
352 new Object[]{issue.getId(),
353 issue.getProject().getName()});
354 }
355
356 String activityString;
357 String componentString = "";
358 StringBuilder sb = new StringBuilder();
359 if (activity.size() == 0) {
360 sb.append("-");
361 } else {
362 for (IssueActivity anActivity : activity) {
363 sb.append("\n ").append(
364 IssueUtilities.getActivityName(anActivity
365 .getActivityType(), currentLocale)).append(": ").append(
366 anActivity.getDescription());
367
368 }
369 }
370 sb.append("\n");
371 activityString = sb.toString();
372 for (int i = 0; i < components.size(); i++) {
373 componentString += (i != 0 ? ", " : "")
374 + components.get(i).getName();
375 }
376
377 final String owner = IssueUtilities.getOwnerName(issue.getOwner(), currentLocale);
378 final User hUser = null == history ? null : history.getUser();
379 final String historyUser = (null != hUser) ? hUser.getFullName()
380 : ITrackerResources.getString("itracker.web.generic.notapplicable", currentLocale);
381
382 final String historyText = (history == null ? "-"
383 : HTMLUtilities
384 .removeMarkup(history
385 .getDescription()));
386 final String status =
387 IssueUtilities.getStatusName(issue
388 .getStatus(), currentLocale);
389 final String msgText;
390 if (type == Type.ISSUE_REMINDER) {
391 msgText = ITrackerResources
392 .getString(
393 "itracker.email.issue.body.reminder",
394 currentLocale,
395 new Object[]{
396 IssueUtilities.getIssueURL(issue, url).toExternalForm(),
397 issue.getProject().getName(),
398 issue.getDescription(),
399 IssueUtilities.getStatusName(issue
400 .getStatus(), currentLocale),
401 IssueUtilities
402 .getSeverityName(issue
403 .getSeverity(), currentLocale),
404 owner,
405 componentString,
406 historyUser,
407 historyText,
408 notModifiedSince,
409 activityString});
410 } else {
411 String resolution = (issue.getResolution() == null ? ""
412 : issue.getResolution());
413 if (!resolution.equals("")
414 && ProjectUtilities
415 .hasOption(
416 ProjectUtilities.OPTION_PREDEFINED_RESOLUTIONS,
417 issue.getProject().getOptions())) {
418 resolution = IssueUtilities.getResolutionName(
419 resolution, currentLocale);
420 }
421 msgText = ITrackerResources
422 .getString(
423 "itracker.email.issue.body."
424 + (type == Type.CREATED ? "created" : "standard"),
425 currentLocale,
426 new Object[]{
427 url + "/module-projects/view_issue.do?id=" + issue.getId(),
428 issue.getProject().getName(),
429 issue.getDescription(),
430 status,
431 resolution,
432 IssueUtilities
433 .getSeverityName(issue
434 .getSeverity(), currentLocale),
435 owner,
436 componentString,
437 historyUser,
438 historyText,
439 activityString,
440 recipientsString});
441 }
442
443 if (logger.isInfoEnabled()) {
444 logger.info("handleNotification: sending notification for " + issue + " (" + type + ") to " + currentLocale + "-users (" + recipients + ")");
445
446 }
447 for (InternetAddress iadr : recipients) {
448 emailService.sendEmail(iadr, subject, msgText);
449 }
450
451 if (logger.isDebugEnabled()) {
452 logger.debug("handleNotification: sent notification for " + issue
453 + ": " + subject + "\n " + msgText);
454 }
455 }
456
457 updateIssueActivityNotification(issue, true);
458 if (logger.isDebugEnabled()) {
459 logger.debug("handleNotification: sent notification for locales " + localeRecipients.keySet() + " recipients: " + localeRecipients.values());
460 }
461 }
462 } catch (RuntimeException e) {
463 logger.error("handleNotification: failed to notify: " + issue + " (locales: " + localeRecipients.keySet() + ")", e);
464
465 } catch (MalformedURLException e) {
466 logger.error("handleNotification: URL was not well-formed", e);
467 }
468
469
470 }
471
472 private IssueService getIssueService() {
473 if (null == issueService) {
474 setIssueService((IssueService) applicationContext.getBean("issueService"));
475 }
476
477 return issueService;
478 }
479
480 public void setIssueService(IssueService issueService) {
481 this.issueService = issueService;
482 }
483
484 public void updateIssueActivityNotification(Issue issue,
485 Boolean notificationSent) {
486 if (logger.isDebugEnabled()) {
487 logger.debug("updateIssueActivityNotification: called with "
488 + issue + ", notificationSent: " + notificationSent);
489 }
490
491 Collection<IssueActivity> activities = getIssueActivityDao()
492 .findByIssueId(issue.getId());
493 for (IssueActivity activity : activities) {
494 activity.setNotificationSent(notificationSent);
495 }
496 }
497
498
499
500 public boolean addIssueNotification(Notification notification) {
501 if (logger.isDebugEnabled()) {
502 logger.debug("addIssueNotification: called with notification: "
503 + notification);
504 }
505 Issue issue = notification.getIssue();
506 if (!issue.getNotifications().contains(notification)) {
507 if (notification.getCreateDate() == null) {
508 notification.setCreateDate(new Date());
509 }
510 if (notification.getLastModifiedDate() == null) {
511 notification.setLastModifiedDate(new Date());
512 }
513
514 getNotificationDao().save(notification);
515
516 issue.getNotifications().add(notification);
517 getIssueDao().merge(issue);
518
519 return true;
520 }
521 if (logger.isDebugEnabled()) {
522 logger.debug("addIssueNotification: attempted to add duplicate notification " + notification + " for issue: " + issue);
523 }
524 return false;
525 }
526
527
528
529
530 public List<Notification> getIssueNotifications(Issue issue,
531 boolean primaryOnly, boolean activeOnly) {
532 if (logger.isDebugEnabled()) {
533 logger.debug("getIssueNotifications: called with issue: " + issue
534 + ", primaryOnly: " + primaryOnly + ", activeOnly: "
535 + activeOnly);
536 }
537 List<Notification> issueNotifications = new ArrayList<>();
538 if (issue == null) {
539 logger.warn("getIssueNotifications: no issue, throwing exception");
540 throw new IllegalArgumentException("issue must not be null");
541 }
542 if (!primaryOnly) {
543 List<Notification> notifications = getNotificationDao()
544 .findByIssueId(issue.getId());
545
546 for (Notification notification : notifications) {
547 User notificationUser = notification.getUser();
548 if (!activeOnly
549 || notificationUser.getStatus() == UserUtilities.STATUS_ACTIVE) {
550 issueNotifications.add(notification);
551 }
552 }
553 }
554
555
556
557
558 boolean hasOwner = false;
559 if (issue.getOwner() != null) {
560 User ownerModel = issue.getOwner();
561
562 if (ownerModel != null
563 && (!activeOnly || ownerModel.getStatus() == UserUtilities.STATUS_ACTIVE)) {
564 issueNotifications.add(new Notification(ownerModel, issue,
565 Role.OWNER));
566 hasOwner = true;
567 }
568 }
569
570 if (!primaryOnly || !hasOwner) {
571 User creatorModel = issue.getCreator();
572
573 if (creatorModel != null
574 && (!activeOnly || creatorModel.getStatus() == UserUtilities.STATUS_ACTIVE)) {
575 issueNotifications.add(new Notification(creatorModel,
576 issue, Role.CREATOR));
577 }
578 }
579
580 Project project = getProjectService().getProject(
581 issue.getProject().getId());
582
583 for (User projectOwner : project.getOwners()) {
584 if (projectOwner != null
585 && (!activeOnly || projectOwner.getStatus() == UserUtilities.STATUS_ACTIVE)) {
586 issueNotifications.add(new Notification(projectOwner,
587 issue, Role.PO));
588 }
589 }
590
591 if (logger.isDebugEnabled()) {
592 logger.debug("getIssueNotifications: returning "
593 + issueNotifications);
594 }
595 return issueNotifications;
596 }
597
598 public List<Notification> getIssueNotifications(Issue issue) {
599 if (logger.isDebugEnabled()) {
600 logger.debug("getIssueNotifications: called with: " + issue);
601 }
602 return this.getIssueNotifications(issue, false, true);
603 }
604
605 public List<Notification> getPrimaryIssueNotifications(Issue issue) {
606 if (logger.isDebugEnabled()) {
607 logger.debug("getPrimaryIssueNotifications: called with: " + issue);
608 }
609 return this.getIssueNotifications(issue, true, false);
610 }
611
612 public boolean hasIssueNotification(Issue issue, Integer userId) {
613 if (logger.isDebugEnabled()) {
614 logger.debug("hasIssueNotification: called with: " + issue
615 + ", userId: " + userId);
616 }
617 return hasIssueNotification(issue, userId, Role.ANY);
618 }
619
620 @Override
621 public boolean hasIssueNotification(Issue issue, String login) {
622
623 return hasIssueNotification(issue, login, Role.ANY);
624 }
625
626 @Override
627 public boolean hasIssueNotification(Issue issue, String login, Role role) {
628
629 if (issue != null && StringUtils.isNotBlank(login)) {
630
631 List<Notification> notifications = getIssueNotifications(issue,
632 false, false);
633
634 for (Notification notification : notifications) {
635
636 if (role == Role.ANY || notification.getRole() == role) {
637
638 if (StringUtils.equals(login, notification.getUser().getLogin())) {
639
640 return true;
641
642 }
643
644 }
645
646 }
647
648 }
649
650 return false;
651 }
652
653 public boolean hasIssueNotification(Issue issue, Integer userId, Role role) {
654
655 if (issue != null && userId != null) {
656
657 List<Notification> notifications = getIssueNotifications(issue,
658 false, false);
659
660 for (Notification notification : notifications) {
661
662 if (role == Role.ANY || notification.getRole() == role) {
663
664 if (notification.getUser().getId().equals(userId)) {
665
666 return true;
667
668 }
669
670 }
671
672 }
673
674 }
675
676 return false;
677
678 }
679
680 public boolean removeIssueNotification(Integer notificationId) {
681 Notification notification = this.getNotificationDao().findById(
682 notificationId);
683 getNotificationDao().delete(notification);
684 return true;
685 }
686
687 public void sendNotification(Issue issue, Type type, String baseURL,
688 InternetAddress[] receipients, Integer lastModifiedDays) {
689 this.handleLocalizedIssueNotification(issue, type, baseURL, receipients,
690 lastModifiedDays);
691
692 }
693
694
695
696
697
698 public EmailService getEmailService() {
699 return emailService;
700 }
701
702
703
704
705 private NotificationDAO getNotificationDao() {
706 return notificationDao;
707 }
708
709
710
711
712 public ProjectService getProjectService() {
713 return projectService;
714 }
715
716
717
718
719 public void setProjectService(ProjectService projectService) {
720 this.projectService = projectService;
721 }
722
723
724
725
726 public void setNotificationDao(NotificationDAO notificationDao) {
727 if (null == notificationDao) {
728 throw new IllegalArgumentException(
729 "notification dao must not be null");
730 }
731 if (null != this.notificationDao) {
732 throw new IllegalStateException("notification dao allready set");
733 }
734 this.notificationDao = notificationDao;
735 }
736
737
738
739
740
741 public IssueActivityDAO getIssueActivityDao() {
742 return issueActivityDao;
743 }
744
745
746
747
748 public void setIssueActivityDao(IssueActivityDAO issueActivityDao) {
749 this.issueActivityDao = issueActivityDao;
750 }
751
752
753
754
755 public IssueDAO getIssueDao() {
756 return issueDao;
757 }
758
759
760
761
762 public void setIssueDao(IssueDAO issueDao) {
763 this.issueDao = issueDao;
764 }
765
766 public String getIssueServiceName() {
767 return issueServiceName;
768 }
769
770 public void setIssueServiceName(String issueServiceName) {
771 this.issueServiceName = issueServiceName;
772 }
773
774 }