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