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