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