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            
419            result.put("modifiedPages", removeExistingServices (survey.getSiteName(), survey.getLanguage(), id));
420        }
421        
422        // Re-initialize the survey
423        survey.reinit();
424        survey.saveChanges();
425        
426        // Send observer to clear survey service page cache
427        Map<String, Object> eventParams = new HashMap<>();
428        eventParams.put("survey", survey);
429        _observationManager.notify(new Event(SurveyEvents.SURVEY_REINITIALIZED, _getCurrentUser(), eventParams));
430
431        // Delete all answers
432        _surveyAnswerDao.deleteSessions(id);
433        
434        
435        result.put("id", survey.getId());
436        
437        return result;
438    }
439    
440    /**
441     * Sets a new redirection page to the survey.
442     * @param surveyId The id of the survey to edit.
443     * @param pageId The id of the redirection page.
444     * @return The id of the edited survey
445     */
446    @Callable
447    public Map<String, String> setRedirection (String surveyId, String pageId)
448    {
449        Map<String, String> result = new HashMap<>();
450        
451        Survey survey = _resolver.resolveById(surveyId);
452        if (StringUtils.isNotEmpty(pageId))
453        {
454            survey.setRedirection(pageId);
455        }
456        else
457        {
458            // Remove redirection
459            survey.setRedirection(null);
460        }
461        survey.saveChanges();
462        
463        Map<String, Object> eventParams = new HashMap<>();
464        eventParams.put("survey", survey);
465        _observationManager.notify(new Event(SurveyEvents.SURVEY_MODIFIED, _getCurrentUser(), eventParams));
466        
467        result.put("id", survey.getId());
468        
469        return result;
470    }
471    
472    /**
473     * Moves an element of the survey.
474     * @param id The id of the element to move.
475     * @param oldParent The id of the element's parent.
476     * @param newParent The id of the new element's parent.
477     * @param index The index where to move. null to place the element at the end.
478     * @return A map with the ids of the element, the old parent and the new parent
479     * @throws Exception if an error occurs when moving an element of the survey
480     */
481    @Callable
482    public Map<String, String> moveObject (String id, String oldParent, String newParent, long index) throws Exception
483    {
484        Map<String, String> result = new HashMap<>();
485        
486        JCRAmetysObject aoMoved = _resolver.resolveById(id);
487        DefaultTraversableAmetysObject newParentAO = _resolver.resolveById(newParent);
488        JCRAmetysObject  brother = null;
489        long size = newParentAO.getChildren().getSize();
490        if (index != -1 && index < size)
491        {
492            brother = newParentAO.getChildAt(index);
493        }
494        else if (index >= size)
495        {
496            brother = newParentAO.getChildAt(Math.toIntExact(size) - 1);
497        }
498        Survey oldSurvey = getParentSurvey(aoMoved);
499        if (oldSurvey != null)
500        {
501            result.put("oldSurveyId", oldSurvey.getId());
502        }
503        
504        if (oldParent.equals(newParent) && brother != null)
505        {
506            Node node = aoMoved.getNode();
507            String name = "";
508            try
509            {
510                name = brother.getName();
511                node.getParent().orderBefore(node.getName(), name);
512            }
513            catch (RepositoryException e)
514            {
515                throw new AmetysRepositoryException(String.format("Unable to order AmetysOject '%s' before sibling '%s'", this, name), e);
516            }
517        }
518        else
519        {
520            Node node = aoMoved.getNode();
521            
522            String name = node.getName();
523            // Find unused name on new parent node
524            int localIndex = 2;
525            while (newParentAO.hasChild(name))
526            {
527                name = node.getName() + "-" + localIndex++;
528            }
529            
530            node.getSession().move(node.getPath(), newParentAO.getNode().getPath() + "/" + name);
531            
532            if (brother != null)
533            {
534                node.getParent().orderBefore(node.getName(), brother.getName());
535            }
536        }
537        
538        if (newParentAO.needsSave())
539        {
540            newParentAO.saveChanges();
541        }
542        
543        Survey survey = getParentSurvey(aoMoved);
544        if (survey != null)
545        {
546            result.put("newSurveyId", survey.getId());
547            
548            Map<String, Object> eventParams = new HashMap<>();
549            eventParams.put("survey", survey);
550            _observationManager.notify(new Event(SurveyEvents.SURVEY_MODIFIED, _getCurrentUser(), eventParams));
551        }
552        
553        result.put("id", id);
554        
555        if (aoMoved instanceof SurveyPage)
556        {
557            result.put("type", "page");
558        }
559        else if (aoMoved instanceof SurveyQuestion)
560        {
561            result.put("type", "question");
562            result.put("questionType", ((SurveyQuestion) aoMoved).getType().name());
563        }
564        
565        result.put("newParentId", newParentAO.getId());
566        result.put("oldParentId", oldParent);
567        
568        return result;
569    }
570    
571    
572    
573    /**
574     * Sends invitations emails.
575     * @param surveyId The id of the survey.
576     * @param message The message content.
577     * @param siteName The site name.
578     * @return An empty map
579     */
580    @Callable
581    public Map<String, Object> sendInvitations (String surveyId, String message, String siteName)
582    {
583        String subject = getMailSubject();
584        String body = getMailBody(surveyId, message, siteName);
585        
586        Site site = _siteManager.getSite(siteName);
587        String defaultFromValue = Config.getInstance().getValue("smtp.mail.from");
588        String from = site.getValue("site-mail-from", false, defaultFromValue);
589       
590        Survey survey = _resolver.resolveById(surveyId);
591        Set<UserIdentity> allowedUsers = _rightManager.getReadAccessAllowedUsers(survey).resolveAllowedUsers(false);
592        
593        for (UserIdentity userIdentity : allowedUsers)
594        {
595            User user = _userHelper.getUser(userIdentity);
596            if (user != null && StringUtils.isNotEmpty(user.getEmail()) && !hasAlreadyAnswered(surveyId, userIdentity))
597            {
598                try
599                {
600                    String finalMessage = StringUtils.replace(body, "[name]", user.getFullName());
601                    SendMailHelper.sendMail(subject, null, finalMessage, user.getEmail(), from);
602                }
603                catch (MessagingException e) 
604                {
605                    new SLF4JLoggerAdapter(LoggerFactory.getLogger(this.getClass())).error("Unable to send mail to user " + user.getEmail(), e);
606                }
607            }
608        }
609        
610        return new HashMap<>();
611    }
612    
613    /**
614     * Generates statistics on each question of a survey.
615     * @param id The survey id
616     * @return A map containing the statistics
617     */
618    @Callable
619    public Map<String, Object> getStatistics(String id)
620    {
621        Map<String, Object> statistics = new HashMap<>();
622        
623        Survey survey = _resolver.resolveById(id);
624        
625        int sessionCount = _surveyAnswerDao.getSessionCount(id);
626        List<SurveySession> sessions = _surveyAnswerDao.getSessionsWithAnswers(id);
627        
628        statistics.put("id", id);
629        statistics.put("title", survey.getTitle());
630        statistics.put("sessions", sessionCount);
631        
632        Map<String, Map<String, Map<String, Object>>> statsMap = createStatsMap(survey);
633        
634        dispatchStats(survey, sessions, statsMap);
635        
636        List statsList = statsToArray(survey, statsMap);
637        
638        statistics.put("questions", statsList);
639        
640        return statistics;
641    }
642    
643    /**
644     * Remove the existing services if exists
645     * @param siteName The site name
646     * @param lang The language
647     * @param surveyId The id of survey
648     * @return The list of modified pages ids
649     */
650    protected List<String> removeExistingServices (String siteName, String lang, String surveyId)
651    {
652        List<String> modifiedPages = new ArrayList<>();
653        for (ModifiableZoneItem zoneItem : getSurveyZoneItems(siteName, lang, surveyId))
654        {
655            ModifiablePage page = (ModifiablePage) zoneItem.getZone().getPage();
656            
657            String id = zoneItem.getId();
658            ZoneType type = zoneItem.getType();
659            
660            zoneItem.remove();
661            page.saveChanges();
662            modifiedPages.add(page.getId());
663            
664            Map<String, Object> eventParams = new HashMap<>();
665            eventParams.put(ObservationConstants.ARGS_PAGE, page);
666            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, id);
667            eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, type);
668            _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_DELETED, _getCurrentUser(), eventParams));
669        }
670        
671        return modifiedPages;
672    }
673    
674    /**
675     * Get all zone items which contains the survey
676     * @param siteName the site name
677     * @param lang the language
678     * @param surveyId the survey id
679     * @return the zone items
680     */
681    public AmetysObjectIterable<ModifiableZoneItem> getSurveyZoneItems(String siteName, String lang, String surveyId)
682    {
683        String xpathQuery = "//element(" + siteName + ", ametys:site)/ametys-internal:sitemaps/" + lang
684                + "//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.survey.service.Display' and ametys:service_parameters/@ametys:surveyId = '" + surveyId + "']";
685
686        return _resolver.query(xpathQuery);
687    }
688    
689    /**
690     * Get the survey containing the given object.
691     * @param obj the object.
692     * @return the parent Survey.
693     */
694    protected Survey getParentSurvey(JCRAmetysObject obj)
695    {
696        try
697        {
698            JCRAmetysObject currentAo = obj.getParent();
699            
700            while (!(currentAo instanceof Survey))
701            {
702                currentAo = currentAo.getParent();
703            }
704            
705            if (currentAo instanceof Survey)
706            {
707                return (Survey) currentAo;
708            }
709        }
710        catch (AmetysRepositoryException e)
711        {
712            // Ignore, just return null.
713        }
714        
715        return null;
716    }
717    
718    /**
719     * Create the statistics Map for a survey.
720     * @param survey the survey.
721     * @return the statistics Map. It is of the following form: questionId -&gt; optionId -&gt;choiceId -&gt; count.
722     */
723    protected Map<String, Map<String, Map<String, Object>>> createStatsMap(Survey survey)
724    {
725        Map<String, Map<String, Map<String, Object>>> stats = new LinkedHashMap<>();
726        
727        for (SurveyQuestion question : survey.getQuestions())
728        {
729            Map<String, Map<String, Object>> questionValues = new LinkedHashMap<>();
730            stats.put(question.getName(), questionValues);
731            
732            switch (question.getType())
733            {
734                case FREE_TEXT:
735                case MULTILINE_FREE_TEXT:
736                    Map<String, Object> values = new LinkedHashMap<>();
737                    questionValues.put("values", values);
738                    values.put("answered", 0);
739                    values.put("empty", 0);
740                    break;
741                case SINGLE_CHOICE:
742                case MULTIPLE_CHOICE:
743                    values = new LinkedHashMap<>();
744                    questionValues.put("values", values);
745                    
746                    for (String option : question.getOptions().keySet())
747                    {
748                        values.put(option, 0);
749                    }
750                    
751                    if (question.hasOtherOption())
752                    {
753                        // Add other option
754                        values.put(__OTHER_OPTION, 0);
755                    }
756                    break;
757                case SINGLE_MATRIX:
758                case MULTIPLE_MATRIX:
759                    for (String option : question.getOptions().keySet())
760                    {
761                        values = new LinkedHashMap<>();
762                        questionValues.put(option, values);
763                        
764                        for (String column : question.getColumns().keySet())
765                        {
766                            values.put(column, 0);
767                        }
768                    }
769                    break;
770                default:
771                    break;
772            }
773        }
774        
775        return stats;
776    }
777    
778    /**
779     * Dispatch the survey user sessions (input) in the statistics map.
780     * @param survey the survey.
781     * @param sessions the user sessions.
782     * @param stats the statistics Map to fill.
783     */
784    protected void dispatchStats(Survey survey, Collection<SurveySession> sessions, Map<String, Map<String, Map<String, Object>>> stats)
785    {
786        for (SurveySession session : sessions)
787        {
788            for (SurveyAnswer answer : session.getAnswers())
789            {
790                SurveyQuestion question = survey.getQuestion(answer.getQuestionId());
791                if (question != null)
792                {
793                    Map<String, Map<String, Object>> questionStats = stats.get(answer.getQuestionId());
794                    
795                    Map<String, Set<String>> valueMap = getValueMap(question, answer.getValue());
796                    
797                    switch (question.getType())
798                    {
799                        case FREE_TEXT:
800                        case MULTILINE_FREE_TEXT:
801                            dispatchTextStats(session, questionStats, valueMap);
802                            break;
803                        case SINGLE_CHOICE:
804                        case MULTIPLE_CHOICE:
805                            dispatchChoiceStats(session, questionStats, valueMap);
806                            break;
807                        case SINGLE_MATRIX:
808                        case MULTIPLE_MATRIX:
809                            dispatchMatrixStats(session, questionStats, valueMap);
810                            break;
811                        default:
812                            break;
813                    }
814                }
815            }
816        }
817    }
818
819    /**
820     * Dispatch stats on a text question.
821     * @param session the survey session.
822     * @param questionStats the Map to fill with the stats.
823     * @param valueMap the value map.
824     */
825    protected void dispatchTextStats(SurveySession session, Map<String, Map<String, Object>> questionStats, Map<String, Set<String>> valueMap)
826    {
827        Map<String, Object> optionStats = questionStats.get("values");
828        
829        if (valueMap.containsKey("values"))
830        {
831            String singleValue = valueMap.get("values").iterator().next();
832            boolean isBlank = StringUtils.isBlank(singleValue);
833            String stat = isBlank ? "empty" : "answered";
834            
835            int iValue = (Integer) optionStats.get(stat);
836            optionStats.put(stat, iValue + 1);
837            
838            if (!isBlank)
839            {
840                optionStats.put(Integer.toString(session.getId()), singleValue);
841            }
842        }
843    }
844    
845    /**
846     * Dispatch stats on a choice question.
847     * @param session the survey session.
848     * @param questionStats the Map to fill with the stats.
849     * @param valueMap the value map.
850     */
851    protected void dispatchChoiceStats(SurveySession session, Map<String, Map<String, Object>> questionStats, Map<String, Set<String>> valueMap)
852    {
853        Map<String, Object> optionStats = questionStats.get("values");
854        
855        if (valueMap.containsKey("values"))
856        {
857            for (String value : valueMap.get("values"))
858            {
859                if (optionStats.containsKey(value))
860                {
861                    int iValue = (Integer) optionStats.get(value);
862                    optionStats.put(value, iValue + 1);
863                }
864                else
865                {
866                    int iValue = (Integer) optionStats.get(__OTHER_OPTION);
867                    optionStats.put(__OTHER_OPTION, iValue + 1);
868                }
869            }
870        }
871    }
872    
873    /**
874     * Dispatch stats on a matrix question.
875     * @param session the survey session.
876     * @param questionStats the Map to fill with the stats.
877     * @param valueMap the value map.
878     */
879    protected void dispatchMatrixStats(SurveySession session, Map<String, Map<String, Object>> questionStats, Map<String, Set<String>> valueMap)
880    {
881        for (String option : valueMap.keySet())
882        {
883            Map<String, Object> optionStats = questionStats.get(option);
884            if (optionStats != null)
885            {
886                for (String value : valueMap.get(option))
887                {
888                    if (optionStats.containsKey(value))
889                    {
890                        int iValue = (Integer) optionStats.get(value);
891                        optionStats.put(value, iValue + 1);
892                    }
893                }
894            }
895            
896        }
897    }
898    
899    /**
900     * Transforms the statistics map into an array with some info.
901     * @param survey The survey
902     * @param stats The filled statistics Map.
903     * @return A list of statistics.
904     */
905    protected List<Map<String, Object>> statsToArray (Survey survey, Map<String, Map<String, Map<String, Object>>> stats)
906    {
907        List<Map<String, Object>> result = new ArrayList<>();
908        
909        for (String questionId : stats.keySet())
910        {
911            Map<String, Object> questionMap = new HashMap<>();
912            
913            SurveyQuestion question = survey.getQuestion(questionId);
914            Map<String, Map<String, Object>> questionStats = stats.get(questionId);
915            
916            questionMap.put("id", questionId);
917            questionMap.put("title", question.getTitle());
918            questionMap.put("type", question.getType());
919            questionMap.put("mandatory", question.isMandatory());
920            
921            List<Object> options = new ArrayList<>();
922            for (String optionId : questionStats.keySet())
923            {
924                Map<String, Object> option = new HashMap<>();
925                
926                option.put("id", optionId);
927                option.put("label", getOptionLabel(question, optionId));
928                
929                questionStats.get(optionId).entrySet();
930                List<Object> choices = new ArrayList<>();
931                for (Entry<String, Object> choice : questionStats.get(optionId).entrySet())
932                {
933                    Map<String, Object> choiceMap = new HashMap<>();
934                    
935                    String choiceId = choice.getKey();
936                    choiceMap.put("value", choiceId);
937                    choiceMap.put("label", getChoiceLabel(question, choiceId));
938                    choiceMap.put("count", choice.getValue());
939                    
940                    choices.add(choiceMap);
941                }
942                option.put("choices", choices);
943                
944                options.add(option);
945            }
946            questionMap.put("options", options);
947            
948            result.add(questionMap);
949        }
950        
951        return result;
952    }
953    
954    /**
955     * Get an option label, depending on the question type.
956     * @param question the question.
957     * @param optionId the option ID.
958     * @return the question label, can be the empty string.
959     */
960    protected String getOptionLabel(SurveyQuestion question, String optionId)
961    {
962        String label = "";
963        
964        switch (question.getType())
965        {
966            case FREE_TEXT:
967            case MULTILINE_FREE_TEXT:
968            case SINGLE_CHOICE:
969            case MULTIPLE_CHOICE:
970                break;
971            case SINGLE_MATRIX:
972            case MULTIPLE_MATRIX:
973                label = question.getOptions().get(optionId);
974                break;
975            default:
976                break;
977        }
978        
979        return label;
980    }
981    
982    /**
983     * Get an option label, depending on the question type.
984     * @param question the question.
985     * @param choiceId the choice id.
986     * @return the option label, can be the empty string.
987     */
988    protected String getChoiceLabel(SurveyQuestion question, String choiceId)
989    {
990        String label = "";
991        
992        switch (question.getType())
993        {
994            case FREE_TEXT:
995            case MULTILINE_FREE_TEXT:
996                break;
997            case SINGLE_CHOICE:
998            case MULTIPLE_CHOICE:
999                if (question.getOptions().containsKey(choiceId))
1000                {
1001                    label = question.getOptions().get(choiceId);
1002                }
1003                else if (question.hasOtherOption())
1004                {
1005                    label = _i18nUtils.translate(new I18nizableText("plugin.survey", "PLUGINS_SURVEY_STATISTICS_OTHER_OPTION"));
1006                }
1007                break;
1008            case SINGLE_MATRIX:
1009            case MULTIPLE_MATRIX:
1010                label = question.getColumns().get(choiceId);
1011                break;
1012            default:
1013                break;
1014        }
1015        
1016        return label;
1017    }
1018    
1019    /**
1020     * Get the user-input value as a Map from the database value, which is a single serialized string.
1021     * @param question the question.
1022     * @param value the value from the database.
1023     * @return the value as a Map.
1024     */
1025    protected Map<String, Set<String>> getValueMap(SurveyQuestion question, String value)
1026    {
1027        Map<String, Set<String>> values = new HashMap<>();
1028        
1029        if (value != null)
1030        {
1031            switch (question.getType())
1032            {
1033                case SINGLE_MATRIX:
1034                case MULTIPLE_MATRIX:
1035                    String[] entries = StringUtils.split(value, ';');
1036                    for (String entry : entries)
1037                    {
1038                        String[] keyValue = StringUtils.split(entry, ':');
1039                        if (keyValue.length == 2 && StringUtils.isNotEmpty(keyValue[0]))
1040                        {
1041                            Set<String> valueSet = new HashSet<>(Arrays.asList(StringUtils.split(keyValue[1], ',')));
1042                            
1043                            values.put(keyValue[0], valueSet);
1044                        }
1045                    }
1046                    break;
1047                case SINGLE_CHOICE:
1048                case MULTIPLE_CHOICE:
1049                    Set<String> valueSet = new HashSet<>(Arrays.asList(StringUtils.split(value, ',')));
1050                    values.put("values", valueSet);
1051                    break;
1052                case FREE_TEXT:
1053                case MULTILINE_FREE_TEXT:
1054                default:
1055                    values.put("values", Collections.singleton(value));
1056                    break;
1057            }
1058        }
1059        
1060        return values;
1061    }
1062    
1063    /**
1064     * Determines if the user has already answered to the survey
1065     * @param surveyId The survey id
1066     * @param user the user
1067     * @return <code>true</code> if the user has already answered
1068     */
1069    protected boolean hasAlreadyAnswered (String surveyId, UserIdentity user)
1070    {
1071        if (user != null && StringUtils.isNotBlank(user.getLogin()) && StringUtils.isNotBlank(user.getPopulationId()))
1072        {
1073            SurveySession userSession = _surveyAnswerDao.getSession(surveyId, user);
1074            
1075            if (userSession != null)
1076            {
1077                return true;
1078            }
1079        }
1080        return false;
1081    }
1082    
1083    /**
1084     * Get the email subject
1085     * @return The subject
1086     */
1087    protected String getMailSubject ()
1088    {
1089        return _i18nUtils.translate(new I18nizableText("plugin.survey", "PLUGINS_SURVEY_SEND_MAIL_SUBJECT"));
1090    }
1091    
1092    /**
1093     * Get the email body
1094     * @param surveyId The survey id
1095     * @param message The message
1096     * @param siteName The site name
1097     * @return The text body
1098     */
1099    protected String getMailBody (String surveyId, String message, String siteName)
1100    {
1101        Site site = _siteManager.getSite(siteName);
1102        String surveyURI = getSurveyUri(surveyId, siteName);
1103        
1104        String replacedMessage = StringUtils.replace(message, "[link]", surveyURI);
1105        replacedMessage = StringUtils.replace(replacedMessage, "[site]", site.getTitle());
1106        
1107        return replacedMessage;
1108    }
1109    
1110    /**
1111     * Get the survey page uri
1112     * @param surveyId The survey id
1113     * @param siteName The site name
1114     * @return The survey absolute uri
1115     */
1116    protected String getSurveyUri (String surveyId, String siteName)
1117    {
1118        Site site = _siteManager.getSite(siteName);
1119        Survey survey = _resolver.resolveById(surveyId);
1120        
1121        Page page = null;
1122        String xpathQuery = "//element(" + siteName + ", ametys:site)/ametys-internal:sitemaps/" + survey.getLanguage()
1123                        + "//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.survey.service.Display' and ametys:service_parameters/@ametys:surveyId = '" + surveyId + "']";
1124        
1125        AmetysObjectIterable<ZoneItem> zoneItems = _resolver.query(xpathQuery);
1126        Iterator<ZoneItem> it = zoneItems.iterator();
1127        if (it.hasNext())
1128        {
1129            page = it.next().getZone().getPage();
1130        }
1131        
1132        if (page != null)
1133        {
1134            return site.getUrl() + "/" + page.getSitemap().getName() + "/" + page.getPathInSitemap() + ".html";
1135        }
1136        
1137        return "";
1138    }
1139}