001/*
002 *  Copyright 2019 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.odf.course;
017
018import java.time.LocalDate;
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import javax.mail.MessagingException;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.commons.lang3.StringUtils;
033
034import org.ametys.cms.ObservationConstants;
035import org.ametys.cms.content.ContentHelper;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.repository.ModifiableContent;
038import org.ametys.core.observation.Event;
039import org.ametys.core.observation.ObservationManager;
040import org.ametys.core.right.RightManager;
041import org.ametys.core.right.RightManager.RightResult;
042import org.ametys.core.user.CurrentUserProvider;
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.repository.data.holder.group.impl.ModifiableModelAwareComposite;
049import org.ametys.runtime.config.Config;
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.runtime.model.ModelItem;
052import org.ametys.runtime.plugin.component.AbstractLogEnabled;
053
054/**
055 * Helper for shareable course status
056 */
057public class ShareableCourseStatusHelper extends AbstractLogEnabled implements Component, Serviceable
058{
059    /** The component role. */
060    public static final String ROLE = ShareableCourseStatusHelper.class.getName();
061    
062    /** The metadata name for the shareable course composite */
063    public static final String SHAREABLE_COURSE_COMPOSITE_METADATA = "shareable-workflow";
064    
065    /** The metadata name for the shareable course status */
066    public static final String SHAREABLE_COURSE_STATUS_METADATA = "status";
067    
068    /** The metadata name for the date of the "proposed" state */
069    private static final String __PROPOSED_DATE_METADATA = "proposed_date";
070
071    /** The metadata name for the author of the "proposed" state */
072    private static final String __PROPOSED_AUTHOR_METADATA = "proposed_author";
073
074    /** The metadata name for the date of the "validated" state */
075    private static final String __VALIDATED_DATE_METADATA = "validated_date";
076
077    /** The metadata name for the author of the "validated" state */
078    private static final String __VALIDATED_AUTHOR_METADATA = "validated_author";
079
080    /** The notification right id */
081    private static final String __NOTIFICATION_RIGHT_ID = "ODF_Rights_Shareable_Course_Receive_Notification";
082    
083    /** The propose right id */
084    private static final String __PROPOSE_RIGHT_ID = "ODF_Rights_Shareable_Course_Propose";
085    
086    /** The validate right id */
087    private static final String __VALIDATE_RIGHT_ID = "ODF_Rights_Shareable_Course_Validate";
088    
089    /** The refusal right id */
090    private static final String __REFUSE_RIGHT_ID = "ODF_Rights_Shareable_Course_Refuse";
091    
092    /** The current user provider */
093    protected CurrentUserProvider _currentUserProvider;
094    
095    /** The observation manager */
096    protected ObservationManager _observationManager;
097    
098    /** The right manager */
099    protected RightManager _rightManager;
100    
101    /** The user manager */
102    protected UserManager _userManager;
103    
104    /** The i18n utils */
105    protected I18nUtils _i18nUtils;
106    
107    /** The content helper */
108    protected ContentHelper _contentHelper;
109    
110    /**
111     * Enumeration for the shareable course status
112     */
113    public enum ShareableStatus
114    {
115        /** Aucun */
116        NONE,
117        /** Proposé */
118        PROPOSED,
119        /** Validé */
120        VALIDATED,
121    }
122    
123    @Override
124    public void service(ServiceManager manager) throws ServiceException
125    {
126        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
127        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
128        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
129        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
130        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
131        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
132    }
133    
134    /**
135     * Get the shareable status of the course
136     * @param content the content
137     * @return the shareable course status
138     */
139    public ShareableStatus getShareableStatus(Content content)
140    {
141        String metadataPath = SHAREABLE_COURSE_COMPOSITE_METADATA + ModelItem.ITEM_PATH_SEPARATOR + SHAREABLE_COURSE_STATUS_METADATA;
142        String status = content.hasValue(metadataPath) ? content.getValue(metadataPath, false, ShareableStatus.NONE.name()) : ShareableStatus.NONE.name();
143        return ShareableStatus.valueOf(status.toUpperCase());
144    }
145    
146    /**
147     * Set the workflow state attribute (date, login) to the content 
148     * @param content the content
149     * @param validationDate the validation date
150     * @param user the user
151     * @param status the shareable course status
152     * @param ignoreRights true to ignore user rights
153     */
154    public void setWorkflowStateAttribute(ModifiableContent content, LocalDate validationDate, UserIdentity user, ShareableStatus status, boolean ignoreRights)
155    {
156        boolean hasChanges = false;
157        switch (status)
158        {
159            case NONE :
160                if (ignoreRights || _rightManager.hasRight(user, __REFUSE_RIGHT_ID, content).equals(RightResult.RIGHT_ALLOW))
161                {
162                    UserIdentity proposedStateAuthor = getProposedStateAuthor(content);
163                    hasChanges = removeShareableWorkflow(content);
164                    
165                    // Send mail to users who propose the course as shareable
166                    _sendNotificationMail(content, Collections.singleton(proposedStateAuthor), "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_SUBJECT_ACTION_REFUSE", "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_BODY_ACTION_REFUSE");
167                }
168                break;
169            case PROPOSED :
170                if (ignoreRights || _rightManager.hasRight(user, __PROPOSE_RIGHT_ID, content).equals(RightResult.RIGHT_ALLOW))
171                {
172                    hasChanges = setProposedStateAttribute(content, validationDate, user);
173                    
174                    // Send mail to users who have the right "ODF_Rights_Shareable_Course_Receive_Notification"
175                    Set<UserIdentity> users = _rightManager.getAllowedUsers(__NOTIFICATION_RIGHT_ID, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"));
176                    _sendNotificationMail(content, users, "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_SUBJECT_ACTION_PROPOSE", "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_BODY_ACTION_PROPOSE");
177                }
178                break;
179            case VALIDATED :
180                if (ignoreRights || _rightManager.hasRight(user, __VALIDATE_RIGHT_ID, content).equals(RightResult.RIGHT_ALLOW))
181                {
182                    hasChanges = setValidatedStateAttribute(content, validationDate, user);
183                    
184                    // Send mail to users who propose the course as shareable
185                    UserIdentity proposedStateAuthor = getProposedStateAuthor(content);
186                    if (proposedStateAuthor != null)
187                    {
188                        _sendNotificationMail(content, Collections.singleton(proposedStateAuthor), "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_SUBJECT_ACTION_VALIDATE", "PLUGINS_ODF_SHAREABLE_COURSE_WORKFLOW_MAIL_BODY_ACTION_VALIDATE");
189                    }
190                }
191                break;
192            default :
193                getLogger().error("{} is an unknown shareable course status", status);
194        }
195        
196        if (hasChanges)
197        {
198            _notifyShareableCourseWorkflowModification(content);
199        }
200    }
201    
202    /**
203     * Remove the shareable course workflow metadata.
204     * @param content The content to clean
205     * @return <code>true</code> if the content has changed
206     */
207    public boolean removeShareableWorkflow(ModifiableContent content)
208    {
209        if (content.hasValue(SHAREABLE_COURSE_COMPOSITE_METADATA))
210        {
211            content.removeValue(SHAREABLE_COURSE_COMPOSITE_METADATA);
212    
213            if (content.needsSave())
214            {
215                content.saveChanges();
216                return true;
217            }
218        }
219        
220        return false;
221    }
222    
223    /**
224     * Set the attribute for 'proposed' state
225     * @param content the content
226     * @param proposeDate the proposed date
227     * @param user the user
228     * @return <code>true</code> if the content has changed.
229     */
230    public boolean setProposedStateAttribute(ModifiableContent content, LocalDate proposeDate, UserIdentity user)
231    {
232        ModifiableModelAwareComposite composite = content.getComposite(SHAREABLE_COURSE_COMPOSITE_METADATA, true);
233        
234        composite.setValue(__PROPOSED_DATE_METADATA, proposeDate);
235        composite.setValue(__PROPOSED_AUTHOR_METADATA, user);
236        
237        composite.setValue(SHAREABLE_COURSE_STATUS_METADATA, ShareableStatus.PROPOSED.name());
238
239        if (content.needsSave())
240        {
241            content.saveChanges();
242            return true;
243        }
244        
245        return false;
246    }
247    
248    /**
249     * Set the attribute for 'validated' state
250     * @param content the content
251     * @param validationDate the validation date
252     * @param user the login
253     * @return <code>true</code> if the content has changed.
254     */
255    public boolean setValidatedStateAttribute(ModifiableContent content, LocalDate validationDate, UserIdentity user)
256    {
257        ModifiableModelAwareComposite composite = content.getComposite(SHAREABLE_COURSE_COMPOSITE_METADATA, true);
258        
259        composite.setValue(__VALIDATED_DATE_METADATA, validationDate);
260        composite.setValue(__VALIDATED_AUTHOR_METADATA, user);
261        
262        composite.setValue(SHAREABLE_COURSE_STATUS_METADATA, ShareableStatus.VALIDATED.name());
263
264        if (content.needsSave())
265        {
266            content.saveChanges();
267            return true;
268        }
269        
270        return false;
271    }
272    
273    /**
274     * Get 'proposed' state author
275     * @param content the content
276     * @return the 'proposed' state author
277     */
278    public UserIdentity getProposedStateAuthor(Content content)
279    {
280        return content.getValue(SHAREABLE_COURSE_COMPOSITE_METADATA + ModelItem.ITEM_PATH_SEPARATOR + __PROPOSED_AUTHOR_METADATA, false, null);
281    }
282    
283    /**
284     * Send a notification with the content modified event.
285     * @param content The content to notify on
286     */
287    protected void _notifyShareableCourseWorkflowModification(Content content)
288    {
289        Map<String, Object> eventParams = new HashMap<>();
290        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
291        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
292        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
293    }
294
295    /**
296     * Send mail to users
297     * @param content the course modified
298     * @param usersNotified the list of user to notify
299     * @param mailSubjectKey the mail subject i18n key
300     * @param mailBodyKey the mail body i18n key
301     */
302    protected void _sendNotificationMail(Content content, Set<UserIdentity> usersNotified, String mailSubjectKey, String mailBodyKey)
303    {
304        UserIdentity currentUser = _currentUserProvider.getUser();
305
306        String mailSubject = _getMailSubject(mailSubjectKey, content);
307        String mailBody = _getMailBody(mailBodyKey, _userManager.getUser(currentUser), content);
308        String from = Config.getInstance().getValue("smtp.mail.from");
309        
310        for (UserIdentity userIdentity : usersNotified)
311        {
312            User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
313            if (user != null && StringUtils.isNotBlank(user.getEmail()))
314            {
315                String mail = user.getEmail();
316                try
317                {
318                    SendMailHelper.sendMail(mailSubject, null, mailBody, mail, from, true);
319                }
320                catch (MessagingException e)
321                {
322                    getLogger().warn("Could not send a notification mail to " + mail, e);
323                }
324            }
325        }
326    }
327    
328    /**
329     * Get the subject of mail
330     * @param subjectI18nKey the i18n key to use for subject
331     * @param content the content
332     * @return the subject
333     */
334    protected String _getMailSubject (String subjectI18nKey, Content content)
335    {
336        I18nizableText subjectKey = new I18nizableText("plugin.odf", subjectI18nKey, _getSubjectI18nParams(content));
337        return _i18nUtils.translate(subjectKey, content.getLanguage());
338    }
339    
340    /**
341     * Get the i18n parameters of mail subject
342     * @param content the content
343     * @return the i18n parameters
344     */
345    protected List<String> _getSubjectI18nParams (Content content)
346    {
347        List<String> params = new ArrayList<>();
348        params.add(_contentHelper.getTitle(content));
349        return params;
350    }
351    
352    /**
353     * Get the text body of mail
354     * @param bodyI18nKey the i18n key to use for body
355     * @param user the caller
356     * @param content the content
357     * @return the text body
358     */
359    protected String _getMailBody (String bodyI18nKey, User user, Content content)
360    {
361        I18nizableText bodyKey = new I18nizableText("plugin.odf", bodyI18nKey, _getBodyI18nParams(user, content));
362        return _i18nUtils.translate(bodyKey, content.getLanguage());
363    }
364    
365    /**
366     * Get the i18n parameters of mail body text
367     * @param user the caller
368     * @param content the content
369     * @return the i18n parameters
370     */
371    protected List<String> _getBodyI18nParams (User user, Content content)
372    {
373        List<String> params = new ArrayList<>();
374        
375        params.add(user.getFullName()); // {0}
376        params.add(content.getTitle()); // {1}
377        params.add(_getContentUri(content)); // {2}
378        
379        return params;
380    }
381    
382    /**
383     * Get the content uri
384     * @param content the content
385     * @return the content uri
386     */
387    protected String _getContentUri(Content content)
388    {
389        String requestUri = _getRequestUri();
390        return requestUri + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27";
391    }
392    
393    /**
394     * Get the request URI.
395     * @return the full request URI.
396     */
397    protected String _getRequestUri()
398    {
399        return StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/");
400    }
401    
402}