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 String from = _siteConfiguration.getValueAsString(siteName, "site-mail-from"); 578 if (StringUtils.isBlank(from)) 579 { 580 from = Config.getInstance().getValueAsString("smtp.mail.from"); 581 } 582 583 Survey survey = _resolver.resolveById(surveyId); 584 Set<UserIdentity> allowedUsers = _rightManager.getReadAccessAllowedUsers(survey).resolveAllowedUsers(false); 585 586 for (UserIdentity userIdentity : allowedUsers) 587 { 588 User user = _userHelper.getUser(userIdentity); 589 if (user != null && StringUtils.isNotEmpty(user.getEmail()) && !hasAlreadyAnswered(surveyId, userIdentity)) 590 { 591 try 592 { 593 String finalMessage = StringUtils.replace(body, "[name]", user.getFullName()); 594 SendMailHelper.sendMail(subject, null, finalMessage, user.getEmail(), from); 595 } 596 catch (MessagingException e) 597 { 598 new SLF4JLoggerAdapter(LoggerFactory.getLogger(this.getClass())).error("Unable to send mail to user " + user.getEmail(), e); 599 } 600 } 601 } 602 603 return new HashMap<>(); 604 } 605 606 /** 607 * Generates statistics on each question of a survey. 608 * @param id The survey id 609 * @return A map containing the statistics 610 */ 611 @Callable 612 public Map<String, Object> getStatistics(String id) 613 { 614 Map<String, Object> statistics = new HashMap<>(); 615 616 Survey survey = _resolver.resolveById(id); 617 618 int sessionCount = _surveyAnswerDao.getSessionCount(id); 619 List<SurveySession> sessions = _surveyAnswerDao.getSessionsWithAnswers(id); 620 621 statistics.put("id", id); 622 statistics.put("title", survey.getTitle()); 623 statistics.put("sessions", sessionCount); 624 625 Map<String, Map<String, Map<String, Object>>> statsMap = createStatsMap(survey); 626 627 dispatchStats(survey, sessions, statsMap); 628 629 List statsList = statsToArray(survey, statsMap); 630 631 statistics.put("questions", statsList); 632 633 return statistics; 634 } 635 636 /** 637 * Remove the existing services if exists 638 * @param siteName The site name 639 * @param lang The language 640 * @param surveyId The id of survey 641 * @return The list of modified pages ids 642 */ 643 protected List<String> removeExistingServices (String siteName, String lang, String surveyId) 644 { 645 List<String> modifiedPages = new ArrayList<>(); 646 647 String xpathQuery = "//element(" + siteName + ", ametys:site)/ametys-internal:sitemaps/" + lang 648 + "//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.survey.service.Display' and ametys:service_parameters/@ametys:surveyId = '" + surveyId + "']"; 649 650 AmetysObjectIterable<ModifiableZoneItem> zoneItems = _resolver.query(xpathQuery); 651 for (ModifiableZoneItem zoneItem : zoneItems) 652 { 653 ModifiablePage page = (ModifiablePage) zoneItem.getZone().getPage(); 654 655 String id = zoneItem.getId(); 656 ZoneType type = zoneItem.getType(); 657 658 zoneItem.remove(); 659 page.saveChanges(); 660 modifiedPages.add(page.getId()); 661 662 Map<String, Object> eventParams = new HashMap<>(); 663 eventParams.put(ObservationConstants.ARGS_PAGE, page); 664 eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, id); 665 eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, type); 666 _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_DELETED, _getCurrentUser(), eventParams)); 667 } 668 669 return modifiedPages; 670 } 671 672 /** 673 * Get the survey containing the given object. 674 * @param obj the object. 675 * @return the parent Survey. 676 */ 677 protected Survey getParentSurvey(JCRAmetysObject obj) 678 { 679 try 680 { 681 JCRAmetysObject currentAo = obj.getParent(); 682 683 while (!(currentAo instanceof Survey)) 684 { 685 currentAo = currentAo.getParent(); 686 } 687 688 if (currentAo instanceof Survey) 689 { 690 return (Survey) currentAo; 691 } 692 } 693 catch (AmetysRepositoryException e) 694 { 695 // Ignore, just return null. 696 } 697 698 return null; 699 } 700 701 /** 702 * Create the statistics Map for a survey. 703 * @param survey the survey. 704 * @return the statistics Map. It is of the following form: questionId -> optionId ->choiceId -> count. 705 */ 706 protected Map<String, Map<String, Map<String, Object>>> createStatsMap(Survey survey) 707 { 708 Map<String, Map<String, Map<String, Object>>> stats = new LinkedHashMap<>(); 709 710 for (SurveyQuestion question : survey.getQuestions()) 711 { 712 Map<String, Map<String, Object>> questionValues = new LinkedHashMap<>(); 713 stats.put(question.getName(), questionValues); 714 715 switch (question.getType()) 716 { 717 case FREE_TEXT: 718 case MULTILINE_FREE_TEXT: 719 Map<String, Object> values = new LinkedHashMap<>(); 720 questionValues.put("values", values); 721 values.put("answered", 0); 722 values.put("empty", 0); 723 break; 724 case SINGLE_CHOICE: 725 case MULTIPLE_CHOICE: 726 values = new LinkedHashMap<>(); 727 questionValues.put("values", values); 728 729 for (String option : question.getOptions().keySet()) 730 { 731 values.put(option, 0); 732 } 733 734 if (question.hasOtherOption()) 735 { 736 // Add other option 737 values.put(__OTHER_OPTION, 0); 738 } 739 break; 740 case SINGLE_MATRIX: 741 case MULTIPLE_MATRIX: 742 for (String option : question.getOptions().keySet()) 743 { 744 values = new LinkedHashMap<>(); 745 questionValues.put(option, values); 746 747 for (String column : question.getColumns().keySet()) 748 { 749 values.put(column, 0); 750 } 751 } 752 break; 753 default: 754 break; 755 } 756 } 757 758 return stats; 759 } 760 761 /** 762 * Dispatch the survey user sessions (input) in the statistics map. 763 * @param survey the survey. 764 * @param sessions the user sessions. 765 * @param stats the statistics Map to fill. 766 */ 767 protected void dispatchStats(Survey survey, Collection<SurveySession> sessions, Map<String, Map<String, Map<String, Object>>> stats) 768 { 769 for (SurveySession session : sessions) 770 { 771 for (SurveyAnswer answer : session.getAnswers()) 772 { 773 SurveyQuestion question = survey.getQuestion(answer.getQuestionId()); 774 if (question != null) 775 { 776 Map<String, Map<String, Object>> questionStats = stats.get(answer.getQuestionId()); 777 778 Map<String, Set<String>> valueMap = getValueMap(question, answer.getValue()); 779 780 switch (question.getType()) 781 { 782 case FREE_TEXT: 783 case MULTILINE_FREE_TEXT: 784 dispatchTextStats(session, questionStats, valueMap); 785 break; 786 case SINGLE_CHOICE: 787 case MULTIPLE_CHOICE: 788 dispatchChoiceStats(session, questionStats, valueMap); 789 break; 790 case SINGLE_MATRIX: 791 case MULTIPLE_MATRIX: 792 dispatchMatrixStats(session, questionStats, valueMap); 793 break; 794 default: 795 break; 796 } 797 } 798 } 799 } 800 } 801 802 /** 803 * Dispatch stats on a text question. 804 * @param session the survey session. 805 * @param questionStats the Map to fill with the stats. 806 * @param valueMap the value map. 807 */ 808 protected void dispatchTextStats(SurveySession session, Map<String, Map<String, Object>> questionStats, Map<String, Set<String>> valueMap) 809 { 810 Map<String, Object> optionStats = questionStats.get("values"); 811 812 if (valueMap.containsKey("values")) 813 { 814 String singleValue = valueMap.get("values").iterator().next(); 815 boolean isBlank = StringUtils.isBlank(singleValue); 816 String stat = isBlank ? "empty" : "answered"; 817 818 int iValue = (Integer) optionStats.get(stat); 819 optionStats.put(stat, iValue + 1); 820 821 if (!isBlank) 822 { 823 optionStats.put(Integer.toString(session.getId()), singleValue); 824 } 825 } 826 } 827 828 /** 829 * Dispatch stats on a choice 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 dispatchChoiceStats(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 for (String value : valueMap.get("values")) 841 { 842 if (optionStats.containsKey(value)) 843 { 844 int iValue = (Integer) optionStats.get(value); 845 optionStats.put(value, iValue + 1); 846 } 847 else 848 { 849 int iValue = (Integer) optionStats.get(__OTHER_OPTION); 850 optionStats.put(__OTHER_OPTION, iValue + 1); 851 } 852 } 853 } 854 } 855 856 /** 857 * Dispatch stats on a matrix question. 858 * @param session the survey session. 859 * @param questionStats the Map to fill with the stats. 860 * @param valueMap the value map. 861 */ 862 protected void dispatchMatrixStats(SurveySession session, Map<String, Map<String, Object>> questionStats, Map<String, Set<String>> valueMap) 863 { 864 for (String option : valueMap.keySet()) 865 { 866 Map<String, Object> optionStats = questionStats.get(option); 867 if (optionStats != null) 868 { 869 for (String value : valueMap.get(option)) 870 { 871 if (optionStats.containsKey(value)) 872 { 873 int iValue = (Integer) optionStats.get(value); 874 optionStats.put(value, iValue + 1); 875 } 876 } 877 } 878 879 } 880 } 881 882 /** 883 * Transforms the statistics map into an array with some info. 884 * @param survey The survey 885 * @param stats The filled statistics Map. 886 * @return A list of statistics. 887 */ 888 protected List<Map<String, Object>> statsToArray (Survey survey, Map<String, Map<String, Map<String, Object>>> stats) 889 { 890 List<Map<String, Object>> result = new ArrayList<>(); 891 892 for (String questionId : stats.keySet()) 893 { 894 Map<String, Object> questionMap = new HashMap<>(); 895 896 SurveyQuestion question = survey.getQuestion(questionId); 897 Map<String, Map<String, Object>> questionStats = stats.get(questionId); 898 899 questionMap.put("id", questionId); 900 questionMap.put("title", question.getTitle()); 901 questionMap.put("type", question.getType()); 902 questionMap.put("mandatory", question.isMandatory()); 903 904 List<Object> options = new ArrayList<>(); 905 for (String optionId : questionStats.keySet()) 906 { 907 Map<String, Object> option = new HashMap<>(); 908 909 option.put("id", optionId); 910 option.put("label", getOptionLabel(question, optionId)); 911 912 questionStats.get(optionId).entrySet(); 913 List<Object> choices = new ArrayList<>(); 914 for (Entry<String, Object> choice : questionStats.get(optionId).entrySet()) 915 { 916 Map<String, Object> choiceMap = new HashMap<>(); 917 918 String choiceId = choice.getKey(); 919 choiceMap.put("value", choiceId); 920 choiceMap.put("label", getChoiceLabel(question, choiceId)); 921 choiceMap.put("count", choice.getValue()); 922 923 choices.add(choiceMap); 924 } 925 option.put("choices", choices); 926 927 options.add(option); 928 } 929 questionMap.put("options", options); 930 931 result.add(questionMap); 932 } 933 934 return result; 935 } 936 937 /** 938 * Get an option label, depending on the question type. 939 * @param question the question. 940 * @param optionId the option ID. 941 * @return the question label, can be the empty string. 942 */ 943 protected String getOptionLabel(SurveyQuestion question, String optionId) 944 { 945 String label = ""; 946 947 switch (question.getType()) 948 { 949 case FREE_TEXT: 950 case MULTILINE_FREE_TEXT: 951 case SINGLE_CHOICE: 952 case MULTIPLE_CHOICE: 953 break; 954 case SINGLE_MATRIX: 955 case MULTIPLE_MATRIX: 956 label = question.getOptions().get(optionId); 957 break; 958 default: 959 break; 960 } 961 962 return label; 963 } 964 965 /** 966 * Get an option label, depending on the question type. 967 * @param question the question. 968 * @param choiceId the choice id. 969 * @return the option label, can be the empty string. 970 */ 971 protected String getChoiceLabel(SurveyQuestion question, String choiceId) 972 { 973 String label = ""; 974 975 switch (question.getType()) 976 { 977 case FREE_TEXT: 978 case MULTILINE_FREE_TEXT: 979 break; 980 case SINGLE_CHOICE: 981 case MULTIPLE_CHOICE: 982 if (question.getOptions().containsKey(choiceId)) 983 { 984 label = question.getOptions().get(choiceId); 985 } 986 else if (question.hasOtherOption()) 987 { 988 label = _i18nUtils.translate(new I18nizableText("plugin.survey", "PLUGINS_SURVEY_STATISTICS_OTHER_OPTION")); 989 } 990 break; 991 case SINGLE_MATRIX: 992 case MULTIPLE_MATRIX: 993 label = question.getColumns().get(choiceId); 994 break; 995 default: 996 break; 997 } 998 999 return label; 1000 } 1001 1002 /** 1003 * Get the user-input value as a Map from the database value, which is a single serialized string. 1004 * @param question the question. 1005 * @param value the value from the database. 1006 * @return the value as a Map. 1007 */ 1008 protected Map<String, Set<String>> getValueMap(SurveyQuestion question, String value) 1009 { 1010 Map<String, Set<String>> values = new HashMap<>(); 1011 1012 if (value != null) 1013 { 1014 switch (question.getType()) 1015 { 1016 case SINGLE_MATRIX: 1017 case MULTIPLE_MATRIX: 1018 String[] entries = StringUtils.split(value, ';'); 1019 for (String entry : entries) 1020 { 1021 String[] keyValue = StringUtils.split(entry, ':'); 1022 if (keyValue.length == 2 && StringUtils.isNotEmpty(keyValue[0])) 1023 { 1024 Set<String> valueSet = new HashSet<>(Arrays.asList(StringUtils.split(keyValue[1], ','))); 1025 1026 values.put(keyValue[0], valueSet); 1027 } 1028 } 1029 break; 1030 case SINGLE_CHOICE: 1031 case MULTIPLE_CHOICE: 1032 Set<String> valueSet = new HashSet<>(Arrays.asList(StringUtils.split(value, ','))); 1033 values.put("values", valueSet); 1034 break; 1035 case FREE_TEXT: 1036 case MULTILINE_FREE_TEXT: 1037 default: 1038 values.put("values", Collections.singleton(value)); 1039 break; 1040 } 1041 } 1042 1043 return values; 1044 } 1045 1046 /** 1047 * Determines if the user has already answered to the survey 1048 * @param surveyId The survey id 1049 * @param user the user 1050 * @return <code>true</code> if the user has already answered 1051 */ 1052 protected boolean hasAlreadyAnswered (String surveyId, UserIdentity user) 1053 { 1054 if (user != null && StringUtils.isNotBlank(user.getLogin()) && StringUtils.isNotBlank(user.getPopulationId())) 1055 { 1056 SurveySession userSession = _surveyAnswerDao.getSession(surveyId, user); 1057 1058 if (userSession != null) 1059 { 1060 return true; 1061 } 1062 } 1063 return false; 1064 } 1065 1066 /** 1067 * Get the email subject 1068 * @return The subject 1069 */ 1070 protected String getMailSubject () 1071 { 1072 return _i18nUtils.translate(new I18nizableText("plugin.survey", "PLUGINS_SURVEY_SEND_MAIL_SUBJECT")); 1073 } 1074 1075 /** 1076 * Get the email body 1077 * @param surveyId The survey id 1078 * @param message The message 1079 * @param siteName The site name 1080 * @return The text body 1081 */ 1082 protected String getMailBody (String surveyId, String message, String siteName) 1083 { 1084 Site site = _siteManager.getSite(siteName); 1085 String surveyURI = getSurveyUri(surveyId, siteName); 1086 1087 String replacedMessage = StringUtils.replace(message, "[link]", surveyURI); 1088 replacedMessage = StringUtils.replace(replacedMessage, "[site]", site.getTitle()); 1089 1090 return replacedMessage; 1091 } 1092 1093 /** 1094 * Get the survey page uri 1095 * @param surveyId The survey id 1096 * @param siteName The site name 1097 * @return The survey absolute uri 1098 */ 1099 protected String getSurveyUri (String surveyId, String siteName) 1100 { 1101 Site site = _siteManager.getSite(siteName); 1102 Survey survey = _resolver.resolveById(surveyId); 1103 1104 Page page = null; 1105 String xpathQuery = "//element(" + siteName + ", ametys:site)/ametys-internal:sitemaps/" + survey.getLanguage() 1106 + "//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.survey.service.Display' and ametys:service_parameters/@ametys:surveyId = '" + surveyId + "']"; 1107 1108 AmetysObjectIterable<ZoneItem> zoneItems = _resolver.query(xpathQuery); 1109 Iterator<ZoneItem> it = zoneItems.iterator(); 1110 if (it.hasNext()) 1111 { 1112 page = it.next().getZone().getPage(); 1113 } 1114 1115 if (page != null) 1116 { 1117 return site.getUrl() + "/" + page.getSitemap().getName() + "/" + page.getPathInSitemap() + ".html"; 1118 } 1119 1120 return ""; 1121 } 1122}