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