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