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 -> optionId ->choiceId -> 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}