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