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