001/*
002 *  Copyright 2015 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.survey.dao;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.LinkedHashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Set;
031
032import javax.jcr.Node;
033import javax.jcr.RepositoryException;
034import javax.mail.MessagingException;
035
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
039import org.apache.commons.lang.StringUtils;
040import org.slf4j.LoggerFactory;
041
042import org.ametys.cms.FilterNameHelper;
043import org.ametys.core.observation.Event;
044import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint;
045import org.ametys.core.right.RightManager;
046import org.ametys.core.ui.Callable;
047import org.ametys.core.user.User;
048import org.ametys.core.user.UserIdentity;
049import org.ametys.core.util.I18nUtils;
050import org.ametys.core.util.mail.SendMailHelper;
051import org.ametys.plugins.core.user.UserHelper;
052import org.ametys.plugins.repository.AmetysObjectIterable;
053import org.ametys.plugins.repository.AmetysRepositoryException;
054import org.ametys.plugins.repository.ModifiableAmetysObject;
055import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
056import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
057import org.ametys.plugins.repository.jcr.JCRAmetysObject;
058import org.ametys.plugins.survey.SurveyEvents;
059import org.ametys.plugins.survey.data.SurveyAnswer;
060import org.ametys.plugins.survey.data.SurveyAnswerDao;
061import org.ametys.plugins.survey.data.SurveySession;
062import org.ametys.plugins.survey.repository.Survey;
063import org.ametys.plugins.survey.repository.SurveyPage;
064import org.ametys.plugins.survey.repository.SurveyQuestion;
065import org.ametys.runtime.config.Config;
066import org.ametys.runtime.i18n.I18nizableText;
067import org.ametys.web.ObservationConstants;
068import org.ametys.web.repository.page.ModifiablePage;
069import org.ametys.web.repository.page.ModifiableZoneItem;
070import org.ametys.web.repository.page.Page;
071import org.ametys.web.repository.page.ZoneItem;
072import org.ametys.web.repository.page.ZoneItem.ZoneType;
073import org.ametys.web.repository.site.Site;
074import org.ametys.web.site.SiteConfigurationExtensionPoint;
075
076/**
077 * DAO for manipulating surveys.
078 *
079 */
080public class SurveyDAO extends AbstractDAO 
081{
082    /** The Avalon role */
083    public static final String ROLE = SurveyDAO.class.getName();
084    
085    private static final String __OTHER_OPTION = "__opt_other";
086    
087    /** The survey answer dao. */
088    protected SurveyAnswerDao _surveyAnswerDao;
089    
090    /** The page DAO */
091    protected PageDAO _pageDAO;
092    
093    /** The site configuration. */
094    protected SiteConfigurationExtensionPoint _siteConfiguration;
095    
096    private I18nUtils _i18nUtils;
097    private RightManager _rightManager;
098    private UserHelper _userHelper;
099    private ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageEP;
100
101    @Override
102    public void service(ServiceManager serviceManager) throws ServiceException
103    {
104        super.service(serviceManager);
105        _surveyAnswerDao = (SurveyAnswerDao) serviceManager.lookup(SurveyAnswerDao.ROLE);
106        _pageDAO = (PageDAO) serviceManager.lookup(PageDAO.ROLE);
107        _siteConfiguration = (SiteConfigurationExtensionPoint) serviceManager.lookup(SiteConfigurationExtensionPoint.ROLE);
108        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
109        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
110        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
111        _profileAssignmentStorageEP = (ProfileAssignmentStorageExtensionPoint) serviceManager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE);
112    }
113    
114    /**
115     * Gets properties of a survey
116     * @param id The id of the survey
117     * @return The properties
118     */
119    @Callable
120    public Map<String, Object> getSurvey (String id)
121    {
122        Survey survey = _resolver.resolveById(id);
123        
124        return getSurvey(survey);
125    }
126    
127    /**
128     * Gets properties of a survey
129     * @param survey The survey
130     * @return The properties
131     */
132    public Map<String, Object> getSurvey (Survey survey)
133    {
134        Map<String, Object> properties = new HashMap<>();
135        
136        properties.put("id", survey.getId());
137        properties.put("label", survey.getLabel());
138        properties.put("title", survey.getTitle());
139        properties.put("description", survey.getDescription());
140        properties.put("endingMessage", survey.getEndingMessage());
141        properties.put("private", isPrivate(survey));
142        
143        if (survey.getRedirection() == null)
144        {
145            properties.put("redirection", "");
146        }
147        else
148        {
149            properties.put("redirection", survey.getRedirection());
150        }
151        
152        properties.putAll(getPictureInfo(survey));
153        
154        return properties;
155    }
156    
157    /**
158     * Determines if the survey is private
159     * @param survey The survey
160     * @return true if the survey is reading restricted
161     */
162    public boolean isPrivate (Survey survey)
163    {
164        return !_rightManager.hasAnonymousReadAccess(survey);
165    }
166    
167    /**
168     * Gets the online status of a survey
169     * @param id The id of the survey
170     * @return A map indicating if the survey is valid and if it is online
171     */
172    @Callable
173    public Map<String, String> isOnline (String id)
174    {
175        Map<String, String> result = new HashMap<>();
176        
177        Survey survey = _resolver.resolveById(id);
178        
179        String xpathQuery = "//element(" + survey.getSiteName() + ", ametys:site)/ametys-internal:sitemaps/" + survey.getLanguage()
180                + "//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.survey.service.Display' and ametys:service_parameters/@ametys:surveyId = '" + id + "']";
181
182        AmetysObjectIterable<ZoneItem> zoneItems = _resolver.query(xpathQuery);
183        
184        result.put("isValid", String.valueOf(survey.isValidated()));
185        result.put("isOnline", String.valueOf(zoneItems.iterator().hasNext()));
186        
187        return result;
188    }
189    
190    /**
191     * Gets the children pages of a survey
192     * @param id The id of the survey
193     * @return A map of pages properties
194     */
195    @Callable
196    public List<Object> getChildren (String id)
197    {
198        List<Object> result = new ArrayList<>();
199        
200        Survey survey = _resolver.resolveById(id);
201        AmetysObjectIterable<SurveyPage> pages = survey.getChildren();
202        for (SurveyPage page : pages)
203        {
204            result.add(_pageDAO.getPage(page));
205        }
206        
207        return result;
208    }
209    
210    /**
211     * Creates a survey.
212     * @param values The survey values
213     * @param siteName The site name
214     * @param language The language
215     * @return The id of the created survey
216     * @throws Exception if an error occurs during the survey creation process
217     */
218    @Callable
219    public Map<String, String> createSurvey (Map<String, Object> values, String siteName, String language) throws Exception
220    {
221        Map<String, String> result = new HashMap<>();
222        
223        ModifiableTraversableAmetysObject rootNode = getSurveyRootNode(siteName, language);
224        
225        String label = StringUtils.defaultString((String) values.get("label"));
226        
227        // Find unique name
228        String originalName = FilterNameHelper.filterName(label);
229        String name = originalName;
230        int index = 2;
231        while (rootNode.hasChild(name))
232        {
233            name = originalName + "-" + (index++);
234        }
235        
236        Survey survey = rootNode.createChild(name, "ametys:survey");
237        _setValues(survey, values);
238        
239        rootNode.saveChanges();
240
241        Map<String, Object> eventParams = new HashMap<>();
242        eventParams.put("survey", survey);
243        _observationManager.notify(new Event(SurveyEvents.SURVEY_CREATED, _getCurrentUser(), eventParams));
244        
245        // Set public access
246        _setPublicAccess(survey);
247        
248        result.put("id", survey.getId());
249        
250        return result;
251    }
252    
253    /**
254     * Edits a survey.
255     * @param values The survey values
256     * @param siteName The site name
257     * @param language The language
258     * @return The id of the edited survey
259     */
260    @Callable
261    public Map<String, String> editSurvey (Map<String, Object> values, String siteName, String language)
262    {
263        Map<String, String> result = new HashMap<>();
264        
265        String id = StringUtils.defaultString((String) values.get("id"));
266        Survey survey = _resolver.resolveById(id);
267        
268        _setValues(survey, values);
269        
270        survey.saveChanges();
271        
272        Map<String, Object> eventParams = new HashMap<>();
273        eventParams.put("survey", survey);
274        _observationManager.notify(new Event(SurveyEvents.SURVEY_MODIFIED, _getCurrentUser(), eventParams));
275        
276        result.put("id", survey.getId());
277        
278        return result;
279    }
280    
281    private void _setPublicAccess (Survey survey)
282    {
283        _profileAssignmentStorageEP.allowProfileToAnonymous(RightManager.READER_PROFILE_ID, survey);
284        
285        Map<String, Object> eventParams = new HashMap<>();
286        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, survey);
287        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, Collections.singleton(RightManager.READER_PROFILE_ID));
288        
289        _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
290    }
291    
292    private void _setValues (Survey survey, Map<String, Object> values)
293    {
294        survey.setTitle(StringUtils.defaultString((String) values.get("title")));
295        survey.setLabel(StringUtils.defaultString((String) values.get("label")));
296        survey.setDescription(StringUtils.defaultString((String) values.get("description")));
297        survey.setEndingMessage(StringUtils.defaultString((String) values.get("endingMessage")));
298        
299        survey.setPictureAlternative(StringUtils.defaultString((String) values.get("picture-alternative")));
300        setPicture(survey, StringUtils.defaultString((String) values.get("picture")));
301    }
302    
303    /**
304     * Copies and pastes a survey.
305     * @param surveyId The id of the survey to copy
306     * @param label The label 
307     * @param title The title
308     * @return The id of the created survey
309     * @throws Exception if an error occurs during the survey copying process
310     */
311    @Callable
312    public Map<String, String> copySurvey(String surveyId, String label, String title) throws Exception
313    {
314        Map<String, String> result = new HashMap<>();
315        
316        String originalName = FilterNameHelper.filterName(label);
317        
318        Survey surveyToCopy = _resolver.resolveById(surveyId);
319        
320        ModifiableTraversableAmetysObject rootNode = getSurveyRootNode(surveyToCopy.getSiteName(), surveyToCopy.getLanguage());
321        
322        // Find unique name
323        String name = originalName;
324        int index = 2;
325        while (rootNode.hasChild(name))
326        {
327            name = originalName + "-" + (index++);
328        }
329        
330        Survey survey = surveyToCopy.copyTo(rootNode, name);
331        survey.setLabel(label);
332        survey.setTitle(title);
333        
334        // Update rules references after copy
335        updateReferencesAfterCopy (surveyToCopy, survey);
336        
337        rootNode.saveChanges();
338        
339        Map<String, Object> eventParams = new HashMap<>();
340        eventParams.put("survey", survey);
341        _observationManager.notify(new Event(SurveyEvents.SURVEY_MODIFIED, _getCurrentUser(), eventParams));
342        
343        _setPublicAccess(survey);
344        
345        result.put("id", survey.getId());
346        
347        return result;
348    }
349    
350    /**
351     * Deletes a survey.
352     * @param id The id of the survey to delete
353     * @return The id of the deleted survey
354     */
355    @Callable
356    public Map<String, String> deleteSurvey (String id)
357    {
358        Map<String, String> result = new HashMap<>();
359        
360        Survey survey = _resolver.resolveById(id);
361        ModifiableAmetysObject parent = survey.getParent();
362        
363        String siteName = survey.getSiteName();
364        
365        survey.remove();
366        
367        _surveyAnswerDao.deleteSessions(id);
368        
369        parent.saveChanges();
370        
371        Map<String, Object> eventParams = new HashMap<>();
372        eventParams.put("siteName", siteName);
373        _observationManager.notify(new Event(SurveyEvents.SURVEY_DELETED, _getCurrentUser(), eventParams));
374        
375        result.put("id", id);
376        
377        return result;
378    }
379    
380    /**
381     * Validates a survey.
382     * @param id The id of the survey to validate
383     * @return The id of the validated survey
384     */
385    @Callable
386    public Map<String, String> validateSurvey (String id)
387    {
388        Map<String, String> result = new HashMap<>();
389        
390        Survey survey = _resolver.resolveById(id);
391        survey.setValidated(true);
392        survey.setValidationDate(new Date());
393        survey.saveChanges();
394        
395        result.put("id", survey.getId());
396        
397        return result;
398    }
399    
400    /**
401     * Reinitializes a survey.
402     * @param id The id of the survey to validate
403     * @param invalidate True to invalidate the survey
404     * @return The id of the reinitialized survey
405     */
406    @Callable
407    public Map<String, Object> reinitSurvey (String id, boolean invalidate)
408    {
409        Map<String, Object> result = new HashMap<>();
410        
411        Survey survey = _resolver.resolveById(id);
412        
413        if (invalidate)
414        {
415            // Invalidate survey
416            survey.setValidated(false);
417            survey.setValidationDate(null);
418            survey.saveChanges();
419            
420            result.put("modifiedPages", removeExistingServices (survey.getSiteName(), survey.getLanguage(), id));
421        }
422        
423        // Delete all answers
424        _surveyAnswerDao.deleteSessions(id);
425        
426        result.put("id", survey.getId());
427        
428        return result;
429    }
430    
431    /**
432     * Sets a new redirection page to the survey.
433     * @param surveyId The id of the survey to edit.
434     * @param pageId The id of the redirection page.
435     * @return The id of the edited survey
436     */
437    @Callable
438    public Map<String, String> setRedirection (String surveyId, String pageId)
439    {
440        Map<String, String> result = new HashMap<>();
441        
442        Survey survey = _resolver.resolveById(surveyId);
443        if (StringUtils.isNotEmpty(pageId))
444        {
445            survey.setRedirection(pageId);
446        }
447        else
448        {
449            // Remove redirection
450            survey.setRedirection(null);
451        }
452        survey.saveChanges();
453        
454        Map<String, Object> eventParams = new HashMap<>();
455        eventParams.put("survey", survey);
456        _observationManager.notify(new Event(SurveyEvents.SURVEY_MODIFIED, _getCurrentUser(), eventParams));
457        
458        result.put("id", survey.getId());
459        
460        return result;
461    }
462    
463    /**
464     * Moves an element of the survey.
465     * @param id The id of the element to move.
466     * @param oldParent The id of the element's parent.
467     * @param newParent The id of the new element's parent.
468     * @param index The index where to move. null to place the element at the end.
469     * @return A map with the ids of the element, the old parent and the new parent
470     * @throws Exception if an error occurs when moving an element of the survey
471     */
472    @Callable
473    public Map<String, String> moveObject (String id, String oldParent, String newParent, int index) throws Exception
474    {
475        Map<String, String> result = new HashMap<>();
476        
477        JCRAmetysObject aoMoved = _resolver.resolveById(id);
478        DefaultTraversableAmetysObject newParentAO = _resolver.resolveById(newParent);
479        JCRAmetysObject  brother = null;
480        if (index != -1)
481        {
482            brother = newParentAO.getChildAt(index);
483        }
484        Survey oldSurvey = getParentSurvey(aoMoved);
485        if (oldSurvey != null)
486        {
487            result.put("oldSurveyId", oldSurvey.getId());
488        }
489        
490        if (oldParent.equals(newParent) && brother != null)
491        {
492            Node node = aoMoved.getNode();
493            String name = "";
494            try
495            {
496                name = brother.getName();
497                node.getParent().orderBefore(node.getName(), name);
498            }
499            catch (RepositoryException e)
500            {
501                throw new AmetysRepositoryException(String.format("Unable to order AmetysOject '%s' before sibling '%s'", this, name), e);
502            }
503        }
504        else
505        {
506            Node node = aoMoved.getNode();
507            
508            String name = node.getName();
509            // Find unused name on new parent node
510            int localIndex = 2;
511            while (newParentAO.hasChild(name))
512            {
513                name = node.getName() + "-" + localIndex++;
514            }
515            
516            node.getSession().move(node.getPath(), newParentAO.getNode().getPath() + "/" + name);
517            
518            if (brother != null)
519            {
520                node.getParent().orderBefore(node.getName(), brother.getName());
521            }
522        }
523        
524        if (newParentAO.needsSave())
525        {
526            newParentAO.saveChanges();
527        }
528        
529        Survey survey = getParentSurvey(aoMoved);
530        if (survey != null)
531        {
532            result.put("newSurveyId", survey.getId());
533            
534            Map<String, Object> eventParams = new HashMap<>();
535            eventParams.put("survey", survey);
536            _observationManager.notify(new Event(SurveyEvents.SURVEY_MODIFIED, _getCurrentUser(), eventParams));
537        }
538        
539        result.put("id", id);
540        
541        if (aoMoved instanceof SurveyPage)
542        {
543            result.put("type", "page");
544        }
545        else if (aoMoved instanceof SurveyQuestion)
546        {
547            result.put("type", "question");
548            result.put("questionType", ((SurveyQuestion) aoMoved).getType().name());
549        }
550        
551        result.put("newParentId", newParentAO.getId());
552        result.put("oldParentId", oldParent);
553        
554        return result;
555    }
556    
557    
558    
559    /**
560     * Sends invitations emails.
561     * @param surveyId The id of the survey.
562     * @param message The message content.
563     * @param siteName The site name.
564     * @return An empty map
565     */
566    @Callable
567    public Map<String, Object> sendInvitations (String surveyId, String message, String siteName)
568    {
569        String subject = getMailSubject();
570        String body = getMailBody(surveyId, message, siteName);
571        
572        String from = _siteConfiguration.getValueAsString(siteName, "site-mail-from");
573        if (StringUtils.isBlank(from))
574        {
575            from = Config.getInstance().getValueAsString("smtp.mail.from");
576        }
577       
578        Survey survey = _resolver.resolveById(surveyId);
579        Set<UserIdentity> allowedUsers = _rightManager.getReadAccessAllowedUsers(survey).resolveAllowedUsers(false);
580        
581        for (UserIdentity userIdentity : allowedUsers)
582        {
583            User user = _userHelper.getUser(userIdentity);
584            if (user != null && StringUtils.isNotEmpty(user.getEmail()) && !hasAlreadyAnswered(surveyId, userIdentity))
585            {
586                try
587                {
588                    String finalMessage = StringUtils.replace(body, "[name]", user.getFullName());
589                    SendMailHelper.sendMail(subject, null, finalMessage, user.getEmail(), from);
590                }
591                catch (MessagingException e) 
592                {
593                    new SLF4JLoggerAdapter(LoggerFactory.getLogger(this.getClass())).error("Unable to send mail to user " + user.getEmail(), e);
594                }
595            }
596        }
597        
598        return new HashMap<>();
599    }
600    
601    /**
602     * Generates statistics on each question of a survey.
603     * @param id The survey id
604     * @return A map containing the statistics
605     */
606    @Callable
607    public Map<String, Object> getStatistics(String id)
608    {
609        Map<String, Object> statistics = new HashMap<>();
610        
611        Survey survey = _resolver.resolveById(id);
612        
613        int sessionCount = _surveyAnswerDao.getSessionCount(id);
614        List<SurveySession> sessions = _surveyAnswerDao.getSessionsWithAnswers(id);
615        
616        statistics.put("id", id);
617        statistics.put("title", survey.getTitle());
618        statistics.put("sessions", sessionCount);
619        
620        Map<String, Map<String, Map<String, Object>>> statsMap = createStatsMap(survey);
621        
622        dispatchStats(survey, sessions, statsMap);
623        
624        List statsList = statsToArray(survey, statsMap);
625        
626        statistics.put("questions", statsList);
627        
628        return statistics;
629    }
630    
631    /**
632     * Remove the existing services if exists
633     * @param siteName The site name
634     * @param lang The language
635     * @param surveyId The id of survey
636     * @return The list of modified pages ids
637     */
638    protected List<String> removeExistingServices (String siteName, String lang, String surveyId)
639    {
640        List<String> modifiedPages = new ArrayList<>();
641        
642        String xpathQuery = "//element(" + siteName + ", ametys:site)/ametys-internal:sitemaps/" + lang
643                        + "//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.survey.service.Display' and ametys:service_parameters/@ametys:surveyId = '" + surveyId + "']";
644        
645        AmetysObjectIterable<ModifiableZoneItem> zoneItems = _resolver.query(xpathQuery);
646        for (ModifiableZoneItem zoneItem : zoneItems)
647        {
648            ModifiablePage page = (ModifiablePage) zoneItem.getZone().getPage();
649            
650            String id = zoneItem.getId();
651            ZoneType type = zoneItem.getType();
652            
653            zoneItem.remove();
654            page.saveChanges();
655            modifiedPages.add(page.getId());
656            
657            Map<String, Object> eventParams = new HashMap<>();
658            eventParams.put(ObservationConstants.ARGS_PAGE, page);
659            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, id);
660            eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, type);
661            _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_DELETED, _getCurrentUser(), eventParams));
662        }
663        
664        return modifiedPages;
665    }
666    
667    /**
668     * Get the survey containing the given object.
669     * @param obj the object.
670     * @return the parent Survey.
671     */
672    protected Survey getParentSurvey(JCRAmetysObject obj)
673    {
674        try
675        {
676            JCRAmetysObject currentAo = obj.getParent();
677            
678            while (!(currentAo instanceof Survey))
679            {
680                currentAo = currentAo.getParent();
681            }
682            
683            if (currentAo instanceof Survey)
684            {
685                return (Survey) currentAo;
686            }
687        }
688        catch (AmetysRepositoryException e)
689        {
690            // Ignore, just return null.
691        }
692        
693        return null;
694    }
695    
696    /**
697     * Create the statistics Map for a survey.
698     * @param survey the survey.
699     * @return the statistics Map. It is of the following form: questionId -&gt; optionId -&gt;choiceId -&gt; count.
700     */
701    protected Map<String, Map<String, Map<String, Object>>> createStatsMap(Survey survey)
702    {
703        Map<String, Map<String, Map<String, Object>>> stats = new LinkedHashMap<>();
704        
705        for (SurveyQuestion question : survey.getQuestions())
706        {
707            Map<String, Map<String, Object>> questionValues = new LinkedHashMap<>();
708            stats.put(question.getName(), questionValues);
709            
710            switch (question.getType())
711            {
712                case FREE_TEXT:
713                case MULTILINE_FREE_TEXT:
714                    Map<String, Object> values = new LinkedHashMap<>();
715                    questionValues.put("values", values);
716                    values.put("answered", 0);
717                    values.put("empty", 0);
718                    break;
719                case SINGLE_CHOICE:
720                case MULTIPLE_CHOICE:
721                    values = new LinkedHashMap<>();
722                    questionValues.put("values", values);
723                    
724                    for (String option : question.getOptions().keySet())
725                    {
726                        values.put(option, 0);
727                    }
728                    
729                    if (question.hasOtherOption())
730                    {
731                        // Add other option
732                        values.put(__OTHER_OPTION, 0);
733                    }
734                    break;
735                case SINGLE_MATRIX:
736                case MULTIPLE_MATRIX:
737                    for (String option : question.getOptions().keySet())
738                    {
739                        values = new LinkedHashMap<>();
740                        questionValues.put(option, values);
741                        
742                        for (String column : question.getColumns().keySet())
743                        {
744                            values.put(column, 0);
745                        }
746                    }
747                    break;
748                default:
749                    break;
750            }
751        }
752        
753        return stats;
754    }
755    
756    /**
757     * Dispatch the survey user sessions (input) in the statistics map.
758     * @param survey the survey.
759     * @param sessions the user sessions.
760     * @param stats the statistics Map to fill.
761     */
762    protected void dispatchStats(Survey survey, Collection<SurveySession> sessions, Map<String, Map<String, Map<String, Object>>> stats)
763    {
764        for (SurveySession session : sessions)
765        {
766            for (SurveyAnswer answer : session.getAnswers())
767            {
768                SurveyQuestion question = survey.getQuestion(answer.getQuestionId());
769                if (question != null)
770                {
771                    Map<String, Map<String, Object>> questionStats = stats.get(answer.getQuestionId());
772                    
773                    Map<String, Set<String>> valueMap = getValueMap(question, answer.getValue());
774                    
775                    switch (question.getType())
776                    {
777                        case FREE_TEXT:
778                        case MULTILINE_FREE_TEXT:
779                            dispatchTextStats(session, questionStats, valueMap);
780                            break;
781                        case SINGLE_CHOICE:
782                        case MULTIPLE_CHOICE:
783                            dispatchChoiceStats(session, questionStats, valueMap);
784                            break;
785                        case SINGLE_MATRIX:
786                        case MULTIPLE_MATRIX:
787                            dispatchMatrixStats(session, questionStats, valueMap);
788                            break;
789                        default:
790                            break;
791                    }
792                }
793            }
794        }
795    }
796
797    /**
798     * Dispatch stats on a text question.
799     * @param session the survey session.
800     * @param questionStats the Map to fill with the stats.
801     * @param valueMap the value map.
802     */
803    protected void dispatchTextStats(SurveySession session, Map<String, Map<String, Object>> questionStats, Map<String, Set<String>> valueMap)
804    {
805        Map<String, Object> optionStats = questionStats.get("values");
806        
807        if (valueMap.containsKey("values"))
808        {
809            String singleValue = valueMap.get("values").iterator().next();
810            boolean isBlank = StringUtils.isBlank(singleValue);
811            String stat = isBlank ? "empty" : "answered";
812            
813            int iValue = (Integer) optionStats.get(stat);
814            optionStats.put(stat, iValue + 1);
815            
816            if (!isBlank)
817            {
818                optionStats.put(Integer.toString(session.getId()), singleValue);
819            }
820        }
821    }
822    
823    /**
824     * Dispatch stats on a choice question.
825     * @param session the survey session.
826     * @param questionStats the Map to fill with the stats.
827     * @param valueMap the value map.
828     */
829    protected void dispatchChoiceStats(SurveySession session, Map<String, Map<String, Object>> questionStats, Map<String, Set<String>> valueMap)
830    {
831        Map<String, Object> optionStats = questionStats.get("values");
832        
833        if (valueMap.containsKey("values"))
834        {
835            for (String value : valueMap.get("values"))
836            {
837                if (optionStats.containsKey(value))
838                {
839                    int iValue = (Integer) optionStats.get(value);
840                    optionStats.put(value, iValue + 1);
841                }
842                else
843                {
844                    int iValue = (Integer) optionStats.get(__OTHER_OPTION);
845                    optionStats.put(__OTHER_OPTION, iValue + 1);
846                }
847            }
848        }
849    }
850    
851    /**
852     * Dispatch stats on a matrix question.
853     * @param session the survey session.
854     * @param questionStats the Map to fill with the stats.
855     * @param valueMap the value map.
856     */
857    protected void dispatchMatrixStats(SurveySession session, Map<String, Map<String, Object>> questionStats, Map<String, Set<String>> valueMap)
858    {
859        for (String option : valueMap.keySet())
860        {
861            Map<String, Object> optionStats = questionStats.get(option);
862            if (optionStats != null)
863            {
864                for (String value : valueMap.get(option))
865                {
866                    if (optionStats.containsKey(value))
867                    {
868                        int iValue = (Integer) optionStats.get(value);
869                        optionStats.put(value, iValue + 1);
870                    }
871                }
872            }
873            
874        }
875    }
876    
877    /**
878     * Transforms the statistics map into an array with some info.
879     * @param survey The survey
880     * @param stats The filled statistics Map.
881     * @return A list of statistics.
882     */
883    protected List<Map<String, Object>> statsToArray (Survey survey, Map<String, Map<String, Map<String, Object>>> stats)
884    {
885        List<Map<String, Object>> result = new ArrayList<>();
886        
887        for (String questionId : stats.keySet())
888        {
889            Map<String, Object> questionMap = new HashMap<>();
890            
891            SurveyQuestion question = survey.getQuestion(questionId);
892            Map<String, Map<String, Object>> questionStats = stats.get(questionId);
893            
894            questionMap.put("id", questionId);
895            questionMap.put("title", question.getTitle());
896            questionMap.put("type", question.getType());
897            questionMap.put("mandatory", question.isMandatory());
898            
899            List<Object> options = new ArrayList<>();
900            for (String optionId : questionStats.keySet())
901            {
902                Map<String, Object> option = new HashMap<>();
903                
904                option.put("id", optionId);
905                option.put("label", getOptionLabel(question, optionId));
906                
907                questionStats.get(optionId).entrySet();
908                List<Object> choices = new ArrayList<>();
909                for (Entry<String, Object> choice : questionStats.get(optionId).entrySet())
910                {
911                    Map<String, Object> choiceMap = new HashMap<>();
912                    
913                    String choiceId = choice.getKey();
914                    choiceMap.put("value", choiceId);
915                    choiceMap.put("label", getChoiceLabel(question, choiceId));
916                    choiceMap.put("count", choice.getValue());
917                    
918                    choices.add(choiceMap);
919                }
920                option.put("choices", choices);
921                
922                options.add(option);
923            }
924            questionMap.put("options", options);
925            
926            result.add(questionMap);
927        }
928        
929        return result;
930    }
931    
932    /**
933     * Get an option label, depending on the question type.
934     * @param question the question.
935     * @param optionId the option ID.
936     * @return the question label, can be the empty string.
937     */
938    protected String getOptionLabel(SurveyQuestion question, String optionId)
939    {
940        String label = "";
941        
942        switch (question.getType())
943        {
944            case FREE_TEXT:
945            case MULTILINE_FREE_TEXT:
946            case SINGLE_CHOICE:
947            case MULTIPLE_CHOICE:
948                break;
949            case SINGLE_MATRIX:
950            case MULTIPLE_MATRIX:
951                label = question.getOptions().get(optionId);
952                break;
953            default:
954                break;
955        }
956        
957        return label;
958    }
959    
960    /**
961     * Get an option label, depending on the question type.
962     * @param question the question.
963     * @param choiceId the choice id.
964     * @return the option label, can be the empty string.
965     */
966    protected String getChoiceLabel(SurveyQuestion question, String choiceId)
967    {
968        String label = "";
969        
970        switch (question.getType())
971        {
972            case FREE_TEXT:
973            case MULTILINE_FREE_TEXT:
974                break;
975            case SINGLE_CHOICE:
976            case MULTIPLE_CHOICE:
977                if (question.getOptions().containsKey(choiceId))
978                {
979                    label = question.getOptions().get(choiceId);
980                }
981                else if (question.hasOtherOption())
982                {
983                    label = _i18nUtils.translate(new I18nizableText("plugin.survey", "PLUGINS_SURVEY_STATISTICS_OTHER_OPTION"));
984                }
985                break;
986            case SINGLE_MATRIX:
987            case MULTIPLE_MATRIX:
988                label = question.getColumns().get(choiceId);
989                break;
990            default:
991                break;
992        }
993        
994        return label;
995    }
996    
997    /**
998     * Get the user-input value as a Map from the database value, which is a single serialized string.
999     * @param question the question.
1000     * @param value the value from the database.
1001     * @return the value as a Map.
1002     */
1003    protected Map<String, Set<String>> getValueMap(SurveyQuestion question, String value)
1004    {
1005        Map<String, Set<String>> values = new HashMap<>();
1006        
1007        if (value != null)
1008        {
1009            switch (question.getType())
1010            {
1011                case SINGLE_MATRIX:
1012                case MULTIPLE_MATRIX:
1013                    String[] entries = StringUtils.split(value, ';');
1014                    for (String entry : entries)
1015                    {
1016                        String[] keyValue = StringUtils.split(entry, ':');
1017                        if (keyValue.length == 2 && StringUtils.isNotEmpty(keyValue[0]))
1018                        {
1019                            Set<String> valueSet = new HashSet<>(Arrays.asList(StringUtils.split(keyValue[1], ',')));
1020                            
1021                            values.put(keyValue[0], valueSet);
1022                        }
1023                    }
1024                    break;
1025                case SINGLE_CHOICE:
1026                case MULTIPLE_CHOICE:
1027                    Set<String> valueSet = new HashSet<>(Arrays.asList(StringUtils.split(value, ',')));
1028                    values.put("values", valueSet);
1029                    break;
1030                case FREE_TEXT:
1031                case MULTILINE_FREE_TEXT:
1032                default:
1033                    values.put("values", Collections.singleton(value));
1034                    break;
1035            }
1036        }
1037        
1038        return values;
1039    }
1040    
1041    /**
1042     * Determines if the user has already answered to the survey
1043     * @param surveyId The survey id
1044     * @param user the user
1045     * @return <code>true</code> if the user has already answered
1046     */
1047    protected boolean hasAlreadyAnswered (String surveyId, UserIdentity user)
1048    {
1049        if (user != null && StringUtils.isNotBlank(user.getLogin()) && StringUtils.isNotBlank(user.getPopulationId()))
1050        {
1051            SurveySession userSession = _surveyAnswerDao.getSession(surveyId, user);
1052            
1053            if (userSession != null)
1054            {
1055                return true;
1056            }
1057        }
1058        return false;
1059    }
1060    
1061    /**
1062     * Get the email subject
1063     * @return The subject
1064     */
1065    protected String getMailSubject ()
1066    {
1067        return _i18nUtils.translate(new I18nizableText("plugin.survey", "PLUGINS_SURVEY_SEND_MAIL_SUBJECT"));
1068    }
1069    
1070    /**
1071     * Get the email body
1072     * @param surveyId The survey id
1073     * @param message The message
1074     * @param siteName The site name
1075     * @return The text body
1076     */
1077    protected String getMailBody (String surveyId, String message, String siteName)
1078    {
1079        Site site = _siteManager.getSite(siteName);
1080        String surveyURI = getSurveyUri(surveyId, siteName);
1081        
1082        String replacedMessage = StringUtils.replace(message, "[link]", surveyURI);
1083        replacedMessage = StringUtils.replace(replacedMessage, "[site]", site.getTitle());
1084        
1085        return replacedMessage;
1086    }
1087    
1088    /**
1089     * Get the survey page uri
1090     * @param surveyId The survey id
1091     * @param siteName The site name
1092     * @return The survey absolute uri
1093     */
1094    protected String getSurveyUri (String surveyId, String siteName)
1095    {
1096        Site site = _siteManager.getSite(siteName);
1097        Survey survey = _resolver.resolveById(surveyId);
1098        
1099        Page page = null;
1100        String xpathQuery = "//element(" + siteName + ", ametys:site)/ametys-internal:sitemaps/" + survey.getLanguage()
1101                        + "//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.survey.service.Display' and ametys:service_parameters/@ametys:surveyId = '" + surveyId + "']";
1102        
1103        AmetysObjectIterable<ZoneItem> zoneItems = _resolver.query(xpathQuery);
1104        Iterator<ZoneItem> it = zoneItems.iterator();
1105        if (it.hasNext())
1106        {
1107            page = it.next().getZone().getPage();
1108        }
1109        
1110        if (page != null)
1111        {
1112            return site.getUrl() + "/" + page.getSitemap().getName() + "/" + page.getPathInSitemap() + ".html";
1113        }
1114        
1115        return "";
1116    }
1117}