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