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;
027import java.util.Set;
028
029import javax.mail.MessagingException;
030
031import org.apache.avalon.framework.activity.Initializable;
032import org.apache.avalon.framework.context.Context;
033import org.apache.avalon.framework.context.ContextException;
034import org.apache.avalon.framework.context.Contextualizable;
035import org.apache.cocoon.components.ContextHelper;
036import org.apache.cocoon.environment.Request;
037import org.apache.commons.collections.CollectionUtils;
038import org.apache.commons.lang.StringUtils;
039import org.apache.commons.lang3.BooleanUtils;
040
041import org.ametys.cms.transformation.xslt.ResolveURIComponent;
042import org.ametys.core.right.RightManager;
043import org.ametys.core.user.User;
044import org.ametys.core.user.UserIdentity;
045import org.ametys.core.user.UserManager;
046import org.ametys.core.util.I18nUtils;
047import org.ametys.core.util.mail.SendMailHelper;
048import org.ametys.plugins.explorer.calendars.Calendar;
049import org.ametys.plugins.explorer.calendars.CalendarEvent;
050import org.ametys.plugins.explorer.calendars.EventRecurrenceTypeEnum;
051import org.ametys.plugins.explorer.workflow.AbstractExplorerNodeWorkflowComponent;
052import org.ametys.plugins.repository.AmetysObject;
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        Set<Page> pages = _projectManager.getModulePages(project, CalendarWorkspaceModule.CALENDAR_MODULE_ID);
522        
523        Page firstPage = null;
524        
525        if (!pages.isEmpty())
526        {
527            for (Page page : pages)
528            {
529                firstPage = page;
530                if (page.getSitemapName().equals(defaultLanguage))
531                {
532                    return page;
533                }
534            }
535        }
536        
537        return firstPage;
538    }
539}
540