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