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