001/* 002 * Copyright 2014 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.workspaces.project.notification; 017 018import java.text.DateFormat; 019import java.text.SimpleDateFormat; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Date; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027 028import javax.mail.MessagingException; 029 030import org.apache.avalon.framework.activity.Initializable; 031import org.apache.avalon.framework.context.Context; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.context.Contextualizable; 034import org.apache.cocoon.components.ContextHelper; 035import org.apache.cocoon.environment.Request; 036import org.apache.commons.collections.CollectionUtils; 037import org.apache.commons.lang.StringUtils; 038import org.apache.commons.lang3.BooleanUtils; 039 040import org.ametys.cms.transformation.xslt.ResolveURIComponent; 041import org.ametys.core.right.RightManager; 042import org.ametys.core.user.User; 043import org.ametys.core.user.UserIdentity; 044import org.ametys.core.user.UserManager; 045import org.ametys.core.util.I18nUtils; 046import org.ametys.core.util.mail.SendMailHelper; 047import org.ametys.plugins.explorer.calendars.Calendar; 048import org.ametys.plugins.explorer.calendars.CalendarEvent; 049import org.ametys.plugins.explorer.calendars.EventRecurrenceTypeEnum; 050import org.ametys.plugins.explorer.workflow.AbstractExplorerNodeWorkflowComponent; 051import org.ametys.plugins.repository.AmetysObject; 052import org.ametys.plugins.repository.AmetysObjectIterator; 053import org.ametys.plugins.repository.AmetysObjectResolver; 054import org.ametys.plugins.workflow.store.AmetysStep; 055import org.ametys.plugins.workflow.support.WorkflowProvider; 056import org.ametys.plugins.workspaces.calendars.CalendarWorkspaceModule; 057import org.ametys.plugins.workspaces.project.ProjectManager; 058import org.ametys.plugins.workspaces.project.objects.Project; 059import org.ametys.runtime.config.Config; 060import org.ametys.runtime.i18n.I18nizableText; 061import org.ametys.runtime.plugin.component.PluginAware; 062import org.ametys.web.renderingcontext.RenderingContext; 063import org.ametys.web.renderingcontext.RenderingContextHandler; 064import org.ametys.web.repository.page.Page; 065 066import com.google.common.collect.Iterables; 067import com.opensymphony.module.propertyset.PropertySet; 068import com.opensymphony.workflow.FunctionProvider; 069import com.opensymphony.workflow.Workflow; 070import com.opensymphony.workflow.WorkflowException; 071import com.opensymphony.workflow.spi.Step; 072 073/** 074 * OS workflow function to send mail after an action is triggered. 075 */ 076public class SendCalendarNotificationFunction extends AbstractExplorerNodeWorkflowComponent implements FunctionProvider, Initializable, PluginAware, Contextualizable 077{ 078 /** 079 * Provide "false" to prevent the function sending the mail. 080 * Useful when making large automatic workflow operations (for instance, when bulk importing and proposing in one action). 081 */ 082 public static final String SEND_MAIL = "send-mail"; 083 084 /** The mail subject key. */ 085 protected static final String SUBJECT_KEY = "subjectKey"; 086 087 /** The mail subject key. */ 088 protected static final String RIGHTS = "rights"; 089 090 /** The mail body key. */ 091 protected static final String BODY_KEY = "bodyKey"; 092 093 /** The right manager. */ 094 protected RightManager _rightManager; 095 096 /** The users manager. */ 097 protected UserManager _userManager; 098 099 /** The workflow provider */ 100 protected WorkflowProvider _workflowProvider; 101 102 /** The plugin name. */ 103 protected String _pluginName; 104 105 /** I18nUtils */ 106 protected I18nUtils _i18nUtils; 107 108 /** The ametys resolver */ 109 protected AmetysObjectResolver _resolver; 110 111 /** The project resolver */ 112 protected ProjectManager _projectManager; 113 114 /** Context available to subclasses. */ 115 protected Context _context; 116 117 /** The rendering context handler */ 118 protected RenderingContextHandler _renderingContextHandler; 119 120 @Override 121 public void initialize() throws Exception 122 { 123 _rightManager = (RightManager) _manager.lookup(RightManager.ROLE); 124 _userManager = (UserManager) _manager.lookup(UserManager.ROLE); 125 _workflowProvider = (WorkflowProvider) _manager.lookup(WorkflowProvider.ROLE); 126 _i18nUtils = (I18nUtils) _manager.lookup(I18nUtils.ROLE); 127 _resolver = (AmetysObjectResolver) _manager.lookup(AmetysObjectResolver.ROLE); 128 _projectManager = (ProjectManager) _manager.lookup(ProjectManager.ROLE); 129 _renderingContextHandler = (RenderingContextHandler) _manager.lookup(RenderingContextHandler.ROLE); 130 } 131 132 @Override 133 public void setPluginInfo(String pluginName, String featureName, String id) 134 { 135 _pluginName = pluginName; 136 } 137 138 public void contextualize(Context context) throws ContextException 139 { 140 _context = context; 141 } 142 143 @Override 144 public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException 145 { 146 String subjectI18nKey = StringUtils.defaultString((String) args.get(SUBJECT_KEY)); 147 String bodyI18nKey = StringUtils.defaultString((String) args.get(BODY_KEY)); 148 String rights = StringUtils.defaultString((String) args.get(RIGHTS)); 149 150 Request request = ContextHelper.getRequest(_context); 151 request.setAttribute("pluginName", _pluginName); 152 153 try 154 { 155 Boolean sendMail = (Boolean) transientVars.get("sendMail"); 156 157 if (BooleanUtils.isNotFalse(sendMail)) 158 { 159 String eventId = (String) transientVars.get("eventId"); 160 CalendarEvent event = _resolver.resolveById(eventId); 161 162 UserIdentity userIdentity = getUser(transientVars); 163 User issuer = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin()); 164 165 String projectName = (String) request.getAttribute("projectName"); 166 Project project = _projectManager.getProject(projectName); 167 168 sendMail(project, event, issuer, subjectI18nKey, bodyI18nKey, rights.split(",")); 169 } 170 } 171 catch (Exception e) 172 { 173 _logger.error("An error occurred: unable to send mail to notify workflow change.", e); 174 } 175 } 176 177 /** 178 * Sent an email 179 * @param project The project 180 * @param event The calendar event 181 * @param issuer The issuer 182 * @param mailSubjecti18nKey The i18n key for subject 183 * @param mailBodyi18nKey The i18n key for body 184 * @param rightIds The rights to check 185 */ 186 protected void sendMail(Project project, CalendarEvent event, User issuer, String mailSubjecti18nKey, String mailBodyi18nKey, String[] rightIds) 187 { 188 // Subject 189 List<String> mailSubjectParams = getSubjectI18nParams(project, issuer, event); 190 I18nizableText i18nSubject = new I18nizableText(null, mailSubjecti18nKey, mailSubjectParams); 191 String subject = _i18nUtils.translate(i18nSubject); 192 193 // Body 194 List<String> mailBodyParams = getBodyI18nParams(project, issuer, event); 195 I18nizableText i18nBody = new I18nizableText(null, mailBodyi18nKey, mailBodyParams); 196 String body = _i18nUtils.translate(i18nBody); 197 198 List<UserIdentity> users = getUsersToNotify(event.getParent(), rightIds); 199 200 String sender = Config.getInstance().getValue("smtp.mail.from"); 201 202 for (UserIdentity userIdentity : users) 203 { 204 User recipient = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin()); 205 if (recipient != null) 206 { 207 String email = recipient.getEmail(); 208 if (StringUtils.isNotEmpty(email)) 209 { 210 try 211 { 212 SendMailHelper.sendMail(subject, null, body, email, sender, true); 213 } 214 catch (MessagingException e) 215 { 216 _logger.warn("Could not send an notification e-mail to " + email, e); 217 } 218 } 219 } 220 } 221 } 222 223 /** 224 * Get the users allowed to be notified 225 * @param object The object responsible of the notification 226 * @param rightsId The id of rights to check 227 * @return The allowed users 228 */ 229 protected List<UserIdentity> getUsersToNotify(AmetysObject object, String[] rightsId) 230 { 231 boolean returnAll = Config.getInstance().getValue("runtime.mail.massive.sending"); 232 Collection<UserIdentity> allowedUsers = _rightManager.getReadAccessAllowedUsers(object).resolveAllowedUsers(returnAll); 233 234 for (String rightId : rightsId) 235 { 236 allowedUsers = CollectionUtils.retainAll(allowedUsers, _rightManager.getAllowedUsers(rightId, object).resolveAllowedUsers(returnAll)); 237 238 } 239 return (List<UserIdentity>) allowedUsers; 240 } 241 242 /** 243 * Get the i18n parameters of mail subject 244 * @param project the the project 245 * @param issuer the issuer 246 * @param event the event 247 * @return the i18n parameters 248 */ 249 protected List<String> getSubjectI18nParams (Project project, User issuer, CalendarEvent event) 250 { 251 List<String> params = new ArrayList<>(); 252 params.add(project.getTitle()); // {0} 253 254 params.add(event.getTitle()); // {1} 255 256 Calendar calendar = event.getParent(); 257 params.add(calendar.getName()); // {2} 258 return params; 259 } 260 261 /** 262 * Get the i18n parameters of mail body text 263 * @param project The project 264 * @param issuer the issuer 265 * @param event the event 266 * @return the i18n parameters 267 */ 268 protected List<String> getBodyI18nParams (Project project, User issuer, CalendarEvent event) 269 { 270 List<String> mailBodyParams = new ArrayList<>(); 271 // {0} project title 272 mailBodyParams.add(project.getTitle()); 273 // {1} event title 274 mailBodyParams.add(event.getTitle()); 275 // {2} calender title 276 Calendar calendar = event.getParent(); 277 mailBodyParams.add(calendar.getName()); // {2} 278 // {3} issuer full name 279 String login = issuer.getIdentity().getLogin(); 280 String populationId = issuer.getIdentity().getPopulationId(); 281 mailBodyParams.add(_userManager.getUser(populationId, login).getFullName()); 282 // {4} issuer email 283 mailBodyParams.add(issuer.getEmail()); 284 285 Date startDate = event.getStartDate(); 286 Date endDate = event.getEndDate(); 287 288 DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 289 String startDateAsStr = df.format(startDate); 290 String endDateAsStr = df.format(endDate); 291 292 // {5} event url 293 mailBodyParams.add(getEventUrl(project, calendar.getId(), startDate)); 294 // {6} project url 295 mailBodyParams.add(getProjectUrl(project)); 296 297 I18nizableText dayFormatI18nText = new I18nizableText("plugin." + _pluginName, "PROJECT_MAIL_NOTIFICATION_EVENT_DATE_FORMAT"); 298 DateFormat dayFormat = new SimpleDateFormat(_i18nUtils.translate(dayFormatI18nText)); 299 300 I18nizableText hourFormatI18nText = new I18nizableText("plugin." + _pluginName, "PROJECT_MAIL_NOTIFICATION_EVENT_HOUR_FORMAT"); 301 DateFormat hourFormat = new SimpleDateFormat(_i18nUtils.translate(hourFormatI18nText)); 302 303 String eventDates = ""; 304 305 if (event.getFullDay()) 306 { 307 java.util.Calendar gcStartDate = java.util.Calendar.getInstance(); 308 gcStartDate.setTime(startDate); 309 gcStartDate.set(java.util.Calendar.HOUR_OF_DAY, 0); 310 gcStartDate.set(java.util.Calendar.MINUTE, 0); 311 gcStartDate.set(java.util.Calendar.SECOND, 0); 312 gcStartDate.set(java.util.Calendar.MILLISECOND, 0); 313 314 java.util.Calendar gcEndDate = java.util.Calendar.getInstance(); 315 gcEndDate.setTime(endDate); 316 gcEndDate.set(java.util.Calendar.HOUR_OF_DAY, 0); 317 gcEndDate.set(java.util.Calendar.MINUTE, 0); 318 gcEndDate.set(java.util.Calendar.SECOND, 0); 319 gcEndDate.set(java.util.Calendar.MILLISECOND, 0); 320 321 if (gcStartDate.equals(gcEndDate)) 322 { 323 // Full day event on same day 324 List<String> paramsDate = new ArrayList<>(); 325 paramsDate.add(dayFormat.format(startDate)); 326 327 I18nizableText dateI18n = new I18nizableText("plugin." + _pluginName, "PROJECT_MAIL_NOTIFICATION_EVENT_DATE_FULLDAY_FORMAT", paramsDate); 328 eventDates = _i18nUtils.translate(dateI18n); 329 } 330 else 331 { 332 // Full day event on several days 333 List<String> paramsDate = new ArrayList<>(); 334 paramsDate.add(dayFormat.format(startDate)); 335 paramsDate.add(dayFormat.format(endDate)); 336 337 I18nizableText dateI18n = new I18nizableText("plugin." + _pluginName, "PROJECT_MAIL_NOTIFICATION_EVENT_DATE_FULLDAYS_FORMAT", paramsDate); 338 eventDates = _i18nUtils.translate(dateI18n); 339 } 340 } 341 else 342 { 343 if (startDateAsStr.equals(endDateAsStr)) 344 { 345 // Time slot on same day 346 List<String> paramsDate = new ArrayList<>(); 347 paramsDate.add(dayFormat.format(startDate)); 348 paramsDate.add(hourFormat.format(startDate)); 349 paramsDate.add(hourFormat.format(endDate)); 350 351 I18nizableText dateI18n = new I18nizableText("plugin." + _pluginName, "PROJECT_MAIL_NOTIFICATION_EVENT_DATE_SAME_DAY_TIME_SLOT_FORMAT", paramsDate); 352 eventDates = _i18nUtils.translate(dateI18n); 353 } 354 else 355 { 356 // Time slot on several day 357 List<String> paramsDate = new ArrayList<>(); 358 paramsDate.add(dayFormat.format(startDate)); 359 paramsDate.add(hourFormat.format(startDate)); 360 paramsDate.add(dayFormat.format(endDate)); 361 paramsDate.add(hourFormat.format(endDate)); 362 363 I18nizableText dateI18n = new I18nizableText("plugin." + _pluginName, "PROJECT_MAIL_NOTIFICATION_EVENT_DATE_TIME_SLOT_FORMAT", paramsDate); 364 eventDates = _i18nUtils.translate(dateI18n); 365 } 366 } 367 368 eventDates += _getRecurrentDateInfo(event, dayFormat); // concat with recurrent info 369 // {7} event dates; 370 mailBodyParams.add(eventDates); // {7} 371 372 // Get the workflow comment 373 long workflowId = event.getWorkflowId(); 374 Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event); 375 Iterator<Step> steps = workflow.getHistorySteps(workflowId).iterator(); 376 377 // Browse the step list to find the newest proposition action. 378 if (steps.hasNext()) 379 { 380 Step step = steps.next(); 381 if (step instanceof AmetysStep) 382 { 383 String comment = (String) ((AmetysStep) step).getProperty("comment"); 384 if (StringUtils.isNotEmpty(comment)) 385 { 386 List<String> params = new ArrayList<>(); 387 params.add(comment); 388 389 I18nizableText commentKey = new I18nizableText("plugin.cms", "WORKFLOW_MAIL_BODY_USER_COMMENT", params); 390 String commentTxt = _i18nUtils.translate(commentKey); 391 392 // {8} comments; 393 mailBodyParams.add(commentTxt); 394 } 395 } 396 } 397 398 return mailBodyParams; 399 } 400 401 /** 402 * Get the recurrent information on a event 403 * @param event the event 404 * @param dayFormat the date format for day 405 * @return the recurrent information or empty if the event is not recurrent 406 */ 407 protected String _getRecurrentDateInfo(CalendarEvent event, DateFormat dayFormat) 408 { 409 EventRecurrenceTypeEnum recurrenceTypeEnum = event.getRecurrenceType(); 410 411 Date untilDate = event.getRepeatUntil(); 412 String i18nKey = ""; 413 414 switch (recurrenceTypeEnum) 415 { 416 case ALL_DAY: 417 i18nKey = "PROJECT_MAIL_NOTIFICATION_EVENT_RECURRENT_ALL_DAY" + (untilDate != null ? "_UNTIL" : ""); 418 break; 419 case ALL_WORKING_DAY: 420 i18nKey = "PROJECT_MAIL_NOTIFICATION_EVENT_RECURRENT_ALL_WORKING_DAY" + (untilDate != null ? "_UNTIL" : ""); 421 break; 422 case WEEKLY: 423 i18nKey = "PROJECT_MAIL_NOTIFICATION_EVENT_RECURRENT_WEEKLY" + (untilDate != null ? "_UNTIL" : ""); 424 break; 425 case BIWEEKLY: 426 i18nKey = "PROJECT_MAIL_NOTIFICATION_EVENT_RECURRENT_BIWEEKLY" + (untilDate != null ? "_UNTIL" : ""); 427 break; 428 case MONTHLY: 429 i18nKey = "PROJECT_MAIL_NOTIFICATION_EVENT_RECURRENT_MONTHLY" + (untilDate != null ? "_UNTIL" : ""); 430 break; 431 default: 432 // The event is not recurrent 433 return ""; 434 } 435 436 List<String> paramsDate = new ArrayList<>(); 437 if (untilDate != null) 438 { 439 paramsDate.add(dayFormat.format(untilDate)); 440 } 441 442 I18nizableText dateI18n = new I18nizableText("plugin." + _pluginName, i18nKey, paramsDate); 443 return _i18nUtils.translate(dateI18n); 444 } 445 446 /** 447 * Get the absolute full url of the event 448 * @param project The project 449 * @param calendarId The id of parent calendar 450 * @param eventStartDate The start date of the event 451 * @return The full uri 452 */ 453 protected String getEventUrl(Project project, String calendarId, Date eventStartDate) 454 { 455 Page modulePage = getCalendarModulePage(project); 456 if (modulePage != null) 457 { 458 RenderingContext currentContext = _renderingContextHandler.getRenderingContext(); 459 460 try 461 { 462 StringBuilder sb = new StringBuilder(); 463 464 _renderingContextHandler.setRenderingContext(RenderingContext.FRONT); 465 466 sb.append(ResolveURIComponent.resolve("page", modulePage.getId(), false, true)); 467 468 if (eventStartDate != null) 469 { 470 DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 471 sb.append("?date=").append(df.format(eventStartDate)); 472 } 473 474 if (calendarId != null) 475 { 476 sb.append("#").append(calendarId); 477 } 478 479 return sb.toString(); 480 } 481 finally 482 { 483 _renderingContextHandler.setRenderingContext(currentContext); 484 } 485 } 486 else 487 { 488 return getProjectUrl(project); 489 } 490 } 491 492 /** 493 * Get the absolute url of project 494 * @param project The project 495 * @return the project's url 496 */ 497 protected String getProjectUrl(Project project) 498 { 499 return Iterables.getFirst(_projectManager.getProjectUrls(project), StringUtils.EMPTY); 500 } 501 502 /** 503 * Get the default language to resolve module's page 504 * @return The default language 505 */ 506 protected String getDefaultLanguage() 507 { 508 Map objectModel = ContextHelper.getObjectModel(_context); 509 Locale locale = org.apache.cocoon.i18n.I18nUtils.findLocale(objectModel, "locale", null, Locale.getDefault(), true); 510 return locale.getLanguage(); 511 } 512 513 /** 514 * Get the module's page 515 * @param project The project 516 * @return The page or <code>null</code> if not found 517 */ 518 protected Page getCalendarModulePage(Project project) 519 { 520 String defaultLanguage = getDefaultLanguage(); 521 AmetysObjectIterator<Page> pages = _projectManager.getModulePages(project, CalendarWorkspaceModule.CALENDAR_MODULE_ID, null).iterator(); 522 523 Page firstPage = null; 524 525 if (pages.getSize() > 0) 526 { 527 while (pages.hasNext()) 528 { 529 Page page = pages.next(); 530 firstPage = page; 531 if (page.getSitemapName().equals(defaultLanguage)) 532 { 533 return page; 534 } 535 } 536 } 537 538 return firstPage; 539 } 540} 541