001/* 002 * Copyright 2021 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 */ 016 017package org.ametys.plugins.forms.dao; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026import java.util.Set; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.context.Context; 030import org.apache.avalon.framework.context.ContextException; 031import org.apache.avalon.framework.context.Contextualizable; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.cocoon.Constants; 036import org.apache.cocoon.ProcessingException; 037import org.apache.commons.collections.ListUtils; 038import org.apache.commons.lang3.ArrayUtils; 039import org.apache.commons.lang3.StringUtils; 040 041import org.ametys.core.observation.Event; 042import org.ametys.core.observation.ObservationManager; 043import org.ametys.core.right.RightManager; 044import org.ametys.core.ui.Callable; 045import org.ametys.core.upload.UploadManager; 046import org.ametys.core.user.CurrentUserProvider; 047import org.ametys.core.user.UserIdentity; 048import org.ametys.core.util.I18nUtils; 049import org.ametys.core.util.JSONUtils; 050import org.ametys.plugins.forms.ObservationConstants; 051import org.ametys.plugins.forms.helper.FormAdminDashboardHelper; 052import org.ametys.plugins.forms.question.FormQuestionType; 053import org.ametys.plugins.forms.question.FormQuestionTypeExtensionPoint; 054import org.ametys.plugins.forms.question.sources.AbstractSourceType; 055import org.ametys.plugins.forms.question.sources.ChoiceOption; 056import org.ametys.plugins.forms.question.sources.ChoiceSourceType; 057import org.ametys.plugins.forms.question.sources.ChoiceSourceTypeExtensionPoint; 058import org.ametys.plugins.forms.question.types.impl.ChoicesListQuestionType; 059import org.ametys.plugins.forms.question.types.impl.ComputedQuestionType; 060import org.ametys.plugins.forms.question.types.impl.RichTextQuestionType; 061import org.ametys.plugins.forms.repository.CopyFormUpdater; 062import org.ametys.plugins.forms.repository.CopyFormUpdaterExtensionPoint; 063import org.ametys.plugins.forms.repository.Form; 064import org.ametys.plugins.forms.repository.FormEntry; 065import org.ametys.plugins.forms.repository.FormPage; 066import org.ametys.plugins.forms.repository.FormPageRule; 067import org.ametys.plugins.forms.repository.FormPageRule.PageRuleType; 068import org.ametys.plugins.forms.repository.FormQuestion; 069import org.ametys.plugins.forms.repository.type.Rule; 070import org.ametys.plugins.forms.repository.type.Rule.QuestionRuleType; 071import org.ametys.plugins.forms.rights.FormsDirectoryRightAssignmentContext; 072import org.ametys.plugins.repository.AmetysObjectResolver; 073import org.ametys.plugins.repository.UnknownAmetysObjectException; 074import org.ametys.plugins.repository.jcr.NameHelper; 075import org.ametys.plugins.repository.jcr.NameHelper.NameComputationMode; 076import org.ametys.runtime.i18n.I18nizableText; 077import org.ametys.runtime.model.DefinitionContext; 078import org.ametys.runtime.model.ElementDefinition; 079import org.ametys.runtime.model.Model; 080import org.ametys.runtime.model.ModelItem; 081import org.ametys.runtime.model.View; 082import org.ametys.runtime.plugin.component.AbstractLogEnabled; 083import org.ametys.web.parameters.ParametersManager; 084 085/** DAO for manipulating form questions */ 086public class FormQuestionDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 087{ 088 /** The Avalon role */ 089 public static final String ROLE = FormQuestionDAO.class.getName(); 090 091 /** Name for rules root jcr node */ 092 public static final String RULES_ROOT = "ametys-internal:form-page-rules"; 093 094 /** Ametys object resolver. */ 095 protected AmetysObjectResolver _resolver; 096 /** Observer manager. */ 097 protected ObservationManager _observationManager; 098 /** The current user provider. */ 099 protected CurrentUserProvider _currentUserProvider; 100 /** Manager for retrieving uploaded files */ 101 protected UploadManager _uploadManager; 102 /** JSON helper */ 103 protected JSONUtils _jsonUtils; 104 /** I18n Utils */ 105 protected I18nUtils _i18nUtils; 106 /** The form question type extension point */ 107 protected FormQuestionTypeExtensionPoint _formQuestionTypeExtensionPoint; 108 /** The parameters manager */ 109 protected ParametersManager _parametersManager; 110 /** The Avalon context */ 111 protected Context _context; 112 /** The cocoon context */ 113 protected org.apache.cocoon.environment.Context _cocoonContext; 114 /** The choice source type extension point */ 115 protected ChoiceSourceTypeExtensionPoint _choiceSourceTypeExtensionPoint; 116 /**The form DAO */ 117 protected FormDAO _formDAO; 118 /** The right manager */ 119 protected RightManager _rightManager; 120 /** The copy form updater extension point */ 121 protected CopyFormUpdaterExtensionPoint _copyFormEP; 122 /** The form admin dashboard helper */ 123 protected FormAdminDashboardHelper _formAdminDashboardHelper; 124 125 @Override 126 public void service(ServiceManager serviceManager) throws ServiceException 127 { 128 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 129 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 130 _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE); 131 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 132 _uploadManager = (UploadManager) serviceManager.lookup(UploadManager.ROLE); 133 _jsonUtils = (JSONUtils) serviceManager.lookup(JSONUtils.ROLE); 134 _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE); 135 _formQuestionTypeExtensionPoint = (FormQuestionTypeExtensionPoint) serviceManager.lookup(FormQuestionTypeExtensionPoint.ROLE); 136 _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE); 137 _formDAO = (FormDAO) serviceManager.lookup(FormDAO.ROLE); 138 _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE); 139 _copyFormEP = (CopyFormUpdaterExtensionPoint) serviceManager.lookup(CopyFormUpdaterExtensionPoint.ROLE); 140 _formAdminDashboardHelper = (FormAdminDashboardHelper) serviceManager.lookup(FormAdminDashboardHelper.ROLE); 141 } 142 143 @Override 144 public void contextualize(Context context) throws ContextException 145 { 146 _context = context; 147 _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 148 } 149 150 /** 151 * Provides the current user. 152 * @return the user which cannot be <code>null</code>. 153 */ 154 protected UserIdentity _getCurrentUser() 155 { 156 return _currentUserProvider.getUser(); 157 } 158 159 /** 160 * Gets properties of a form question 161 * @param id The id of the form question 162 * @return The properties 163 */ 164 @Callable (rights = Callable.NO_CHECK_REQUIRED) 165 public Map<String, Object> getQuestionProperties (String id) 166 { 167 // Assume that no read access is checked (required for bus message target) 168 try 169 { 170 FormQuestion question = _resolver.resolveById(id); 171 return getQuestionProperties(question, true); 172 } 173 catch (UnknownAmetysObjectException e) 174 { 175 getLogger().warn("Can't find question with id: {}. It probably has just been deleted", id, e); 176 Map<String, Object> infos = new HashMap<>(); 177 infos.put("id", id); 178 return infos; 179 } 180 } 181 182 /** 183 * Gets properties of a form question 184 * @param question The form question 185 * @param withRight <code>true</code> to have the rights in the properties 186 * @return The properties 187 */ 188 public Map<String, Object> getQuestionProperties (FormQuestion question, boolean withRight) 189 { 190 Map<String, Object> properties = new HashMap<>(); 191 192 boolean hasTerminalRule = _hasTerminalRule(question); 193 List<String> questionTitlesWithRule = _getQuestionTitlesWithRule(question); 194 List<String> pageTitlesWithRule = _getPageTitlesWithRule(question); 195 196 properties.put("type", "question"); 197 properties.put("hasTerminalRule", hasTerminalRule); 198 properties.put("pageTitlesWithRule", pageTitlesWithRule); 199 properties.put("questionTitlesWithRule", questionTitlesWithRule); 200 properties.put("isReadRestricted", question.isReadRestricted()); 201 properties.put("isModifiable", question.isModifiable()); 202 properties.put("hasChildren", false); 203 204 /** Use in the bus message */ 205 properties.put("id", question.getId()); 206 properties.put("title", question.getTitle()); 207 properties.put("questionType", question.getType().getId()); 208 properties.put("pageId", question.getFormPage().getId()); 209 properties.put("formId", question.getForm().getId()); 210 properties.put("iconGlyph", question.getType().getIconGlyph()); 211 properties.put("typeLabel", question.getType().getLabel()); 212 properties.put("hasEntries", !question.getForm().getEntries().isEmpty()); 213 properties.put("hasRule", hasTerminalRule || !pageTitlesWithRule.isEmpty() || !questionTitlesWithRule.isEmpty()); 214 properties.put("isConfigured", question.getType().isQuestionConfigured(question)); 215 216 if (withRight) 217 { 218 properties.put("rights", _getUserRights(question)); 219 } 220 else 221 { 222 properties.put("canWrite", _formDAO.hasWriteRightOnForm(_currentUserProvider.getUser(), question)); 223 } 224 225 return properties; 226 } 227 228 /** 229 * Get options from the choice list question 230 * @param questionId the choice list question id 231 * @return the map of option 232 */ 233 @Callable (rights = FormDAO.HANDLE_FORMS_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 0) 234 public Map<String, I18nizableText> getChoiceListQuestionOptions (String questionId) 235 { 236 FormQuestion question = _resolver.resolveById(questionId); 237 if (question.getType() instanceof ChoicesListQuestionType type) 238 { 239 return type.getOptions(question); 240 } 241 242 return new HashMap<>(); 243 } 244 245 /** 246 * Get user rights for the given form question 247 * @param question the form question 248 * @return the set of rights 249 */ 250 protected Set<String> _getUserRights (FormQuestion question) 251 { 252 UserIdentity user = _currentUserProvider.getUser(); 253 return _rightManager.getUserRights(user, question); 254 } 255 256 private boolean _hasTerminalRule(FormQuestion question) 257 { 258 return question.getPageRules() 259 .stream() 260 .map(FormPageRule::getType) 261 .filter(t -> t == PageRuleType.FINISH) 262 .findAny() 263 .isPresent(); 264 } 265 266 /** 267 * Get the question titles having rule concerning the given question 268 * @param question the question 269 * @return the list of question titles 270 */ 271 protected List<String> _getQuestionTitlesWithRule(FormQuestion question) 272 { 273 return question.getForm() 274 .getQuestionsRule(question.getId()) 275 .keySet() 276 .stream() 277 .map(FormQuestion::getTitle) 278 .toList(); 279 } 280 281 /** 282 * Get the page titles having rule concerning the given question 283 * @param question the question 284 * @return the list of page titles 285 */ 286 protected List<String> _getPageTitlesWithRule(FormQuestion question) 287 { 288 return question.getPageRules() 289 .stream() 290 .filter(r -> r.getType() != PageRuleType.FINISH) 291 .map(FormPageRule::getPageId) 292 .distinct() 293 .map(this::_getFormPage) 294 .map(FormPage::getTitle) 295 .toList(); 296 } 297 298 private FormPage _getFormPage(String pageId) 299 { 300 return _resolver.resolveById(pageId); 301 } 302 303 /** 304 * Get view for question type 305 * @param typeID id of the question type 306 * @param formId id of the form 307 * @return the view parsed in json for configurableFormPanel 308 * @throws ProcessingException error while parsing view to json 309 */ 310 @Callable (rights = FormDAO.HANDLE_FORMS_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 1) 311 public Map<String, Object> getQuestionParametersDefinitions(String typeID, String formId) throws ProcessingException 312 { 313 Map<String, Object> response = new HashMap<>(); 314 Form form = _resolver.resolveById(formId); 315 FormQuestionType questionType = _formQuestionTypeExtensionPoint.getExtension(typeID); 316 View view = questionType.getView(form); 317 response.put("parameters", view.toJSON(DefinitionContext.newInstance().withEdition(true))); 318 response.put("questionNames", form.getQuestionsNames()); 319 return response; 320 } 321 322 /** 323 * Get questions parameters values 324 * @param questionID id of current question 325 * @return map of question parameters value 326 */ 327 @Callable (rights = FormDAO.HANDLE_FORMS_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 0) 328 public Map<String, Object> getQuestionParametersValues(String questionID) 329 { 330 Map<String, Object> results = new HashMap<>(); 331 332 FormQuestion question = _resolver.resolveById(questionID); 333 FormQuestionType type = question.getType(); 334 Collection< ? extends ModelItem> questionModelItems = type.getModel().getModelItems(); 335 Map<String, Object> parametersValues = _parametersManager.getParametersValues(questionModelItems, question, StringUtils.EMPTY); 336 results.put("values", parametersValues); 337 338 // Repeater values aren't handled by getParametersValues() 339 @SuppressWarnings("unchecked") 340 List<Map<String, Object>> repeaters = _parametersManager.getRepeatersValues((Collection<ModelItem>) questionModelItems, question, StringUtils.EMPTY); 341 results.put("repeaters", repeaters); 342 results.put("fieldToDisable", _getFieldNameToDisable(question)); 343 344 return results; 345 } 346 347 private List<String> _getFieldNameToDisable(FormQuestion question) 348 { 349 Form form = question.getForm(); 350 if (form.getEntries().isEmpty()) 351 { 352 return List.of(); 353 } 354 355 return question.getType().getFieldToDisableIfFormPublished(question); 356 } 357 358 /** 359 * Creates a {@link FormQuestion}. 360 * @param pageId id of current page 361 * @param typeId id of FormQuestionType 362 * @return The id of the created form question, the id of the page and the id of the form 363 */ 364 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 365 public Map<String, Object> createQuestion(String pageId, String typeId) 366 { 367 Map<String, Object> result = new HashMap<>(); 368 FormPage page = _resolver.resolveById(pageId); 369 370 _formDAO.checkHandleFormRight(page); 371 372 FormQuestionType type = _formQuestionTypeExtensionPoint.getExtension(typeId); 373 Form form = page.getForm(); 374 375 String defaultTitle = _i18nUtils.translate(type.getDefaultTitle()); 376 String nameForForm = form.findUniqueQuestionName(defaultTitle); 377 378 FormQuestion question = page.createChild(NameHelper.getUniqueAmetysObjectName(page, nameForForm, NameComputationMode.GENERATED_KEY, false), "ametys:form-question"); 379 question.setNameForForm(nameForForm); 380 question.setTypeId(typeId); 381 382 Model model = question.getType().getModel(); 383 for (ModelItem modelItem : model.getModelItems()) 384 { 385 if (modelItem instanceof ElementDefinition) 386 { 387 Object defaultValue = ((ElementDefinition) modelItem).getDefaultValue(); 388 if (defaultValue != null) 389 { 390 question.setValue(modelItem.getPath(), defaultValue); 391 } 392 } 393 } 394 395 question.setTitle(form.findUniqueQuestionTitle(defaultTitle)); 396 397 page.saveChanges(); 398 399 Map<String, Object> eventParams = new HashMap<>(); 400 eventParams.put("form", page.getForm()); 401 _observationManager.notify(new Event(ObservationConstants.FORM_MODIFIED, _getCurrentUser(), eventParams)); 402 403 result.put("id", question.getId()); 404 result.put("pageId", page.getId()); 405 result.put("formId", question.getForm().getId()); 406 result.put("type", typeId); 407 return result; 408 } 409 410 /** 411 * Rename a {@link FormQuestion} 412 * @param id The id of the question 413 * @param newName The new name of the question 414 * @return A result map 415 */ 416 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 417 public Map<String, String> renameQuestion (String id, String newName) 418 { 419 Map<String, String> results = new HashMap<>(); 420 421 FormQuestion question = _resolver.resolveById(id); 422 _formDAO.checkHandleFormRight(question); 423 424 question.setTitle(newName); 425 question.saveChanges(); 426 427 Map<String, Object> eventParams = new HashMap<>(); 428 eventParams.put("form", question.getForm()); 429 _observationManager.notify(new Event(ObservationConstants.FORM_MODIFIED, _getCurrentUser(), eventParams)); 430 431 results.put("id", id); 432 results.put("newName", newName); 433 results.put("formId", question.getForm().getId()); 434 435 return results; 436 } 437 438 /** 439 * Edits a {@link FormQuestion}. 440 * @param questionId id of current question 441 * @param values The question's values 442 * @return The id of the edited form question, the id of the page and the id of the form 443 */ 444 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 445 public Map<String, Object> editQuestion (String questionId, Map<String, Object> values) 446 { 447 Map<String, Object> result = new HashMap<>(); 448 Map<String, I18nizableText> errors = new HashMap<>(); 449 450 FormQuestion question = _resolver.resolveById(questionId); 451 _formDAO.checkHandleFormRight(question); 452 453 Form parentForm = question.getForm(); 454 String questionName = StringUtils.defaultString((String) values.get("name-for-form")); 455 456 // if question can not be answered by user, id can't be changed and is unique by default 457 String oldQuestionName = question.getNameForForm(); 458 if (!question.getType().canBeAnsweredByUser(question) || questionName.equals(oldQuestionName) || parentForm.isQuestionNameUnique(questionName)) 459 { 460 FormQuestionType type = question.getType(); 461 type.validateQuestionValues(values, errors); 462 463 if (!errors.isEmpty()) 464 { 465 result.put("errors", errors); 466 return result; 467 } 468 469 _parametersManager.setParameterValues(question, type.getModel().getModelItems(), values); 470 type.doAdditionalOperations(question, values); 471 472 question.saveChanges(); 473 474 Map<String, Object> eventParams = new HashMap<>(); 475 eventParams.put("form", parentForm); 476 _observationManager.notify(new Event(ObservationConstants.FORM_MODIFIED, _getCurrentUser(), eventParams)); 477 478 result.put("id", question.getId()); 479 result.put("pageId", question.getParent().getId()); 480 result.put("formId", parentForm.getId()); 481 result.put("type", question.getType().toString()); 482 483 if (!questionName.equals(oldQuestionName)) 484 { 485 _formAdminDashboardHelper.updateDashboardConfigOnQuestionRename(parentForm, oldQuestionName, questionName); 486 } 487 } 488 else 489 { 490 errors.put("duplicate_name", new I18nizableText("plugin.forms", "PLUGINS_FORMS_QUESTIONS_SET_ID_ERROR")); 491 result.put("errors", errors); 492 getLogger().error("An error occurred creating the question. The identifier value '" + questionName + "' is already used."); 493 } 494 495 return result; 496 } 497 498 /** 499 * Deletes a {@link FormQuestion}. 500 * @param id The id of the form question to delete 501 * @return The id of the form question, the id of the page and the id of the form 502 */ 503 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 504 public Map<String, String> deleteQuestion (String id) 505 { 506 FormQuestion question = _resolver.resolveById(id); 507 String questionName = question.getNameForForm(); 508 Form parentForm = question.getForm(); 509 _formDAO.checkHandleFormRight(question); 510 511 parentForm.deleteQuestionsRule(question.getId()); 512 513 FormPage page = question.getParent(); 514 question.remove(); 515 516 page.saveChanges(); 517 518 Map<String, Object> eventParams = new HashMap<>(); 519 eventParams.put("form", page.getForm()); 520 _observationManager.notify(new Event(ObservationConstants.FORM_MODIFIED, _getCurrentUser(), eventParams)); 521 522 _formAdminDashboardHelper.updateDashboardConfigOnQuestionDelete(parentForm, questionName); 523 524 return Map.of("id", id); 525 } 526 527 /** 528 * Copies and pastes a form question. 529 * @param pageId The id of the page, target of the copy 530 * @param questionId The id of the question to copy 531 * @return The id of the created question, the id of the page and the id of the form 532 */ 533 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 534 public Map<String, String> copyQuestion(String pageId, String questionId) 535 { 536 Map<String, String> result = new HashMap<>(); 537 538 FormQuestion originalQuestion = _resolver.resolveById(questionId); 539 _formDAO.checkHandleFormRight(originalQuestion); 540 541 FormPage parentPage = _resolver.resolveById(pageId); 542 543 Form parentForm = parentPage.getForm(); 544 545 String uniqueName = parentForm.findUniqueQuestionName(originalQuestion.getNameForForm()); 546 FormQuestion questionCopy = parentPage.createChild(uniqueName, "ametys:form-question"); 547 originalQuestion.copyTo(questionCopy); 548 549 String copyTitle = _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGIN_FORMS_TREE_COPY_NAME_PREFIX")) + originalQuestion.getTitle(); 550 questionCopy.setTitle(parentForm.findUniqueQuestionTitle(copyTitle)); 551 questionCopy.setTypeId(originalQuestion.getType().getId()); 552 questionCopy.setNameForForm(uniqueName); 553 554 for (String epId : _copyFormEP.getExtensionsIds()) 555 { 556 CopyFormUpdater copyFormUpdater = _copyFormEP.getExtension(epId); 557 copyFormUpdater.updateFormQuestion(originalQuestion, questionCopy); 558 } 559 560 parentPage.saveChanges(); 561 562 Map<String, Object> eventParams = new HashMap<>(); 563 eventParams.put("form", parentForm); 564 _observationManager.notify(new Event(ObservationConstants.FORM_MODIFIED, _getCurrentUser(), eventParams)); 565 566 result.put("id", questionCopy.getId()); 567 result.put("pageId", parentPage.getId()); 568 result.put("formId", parentForm.getId()); 569 result.put("type", questionCopy.getType().getId()); 570 571 return result; 572 } 573 574 /** 575 * Gets the page rules for a form question. 576 * @param id The id of the form question. 577 * @param number The question number 578 * @return The rules 579 * @throws Exception error while getting choice options 580 */ 581 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 582 public Map<String, Object> getRules (String id, int number) throws Exception 583 { 584 Map<String, Object> result = new HashMap<>(); 585 586 FormQuestion question = _resolver.resolveById(id); 587 _formDAO.checkHandleFormRight(question); 588 589 FormQuestionType type = question.getType(); 590 if (type instanceof ChoicesListQuestionType cLType) 591 { 592 ChoiceSourceType sourceType = cLType.getSourceType(question); 593 594 result.put("id", question.getId()); 595 result.put("number", String.valueOf(number)); 596 result.put("title", question.getTitle()); 597 598 List<Object> rules = new ArrayList<>(); 599 for (FormPageRule rule : question.getPageRules()) 600 { 601 String option = rule.getOption(); 602 Map<String, Object> enumParam = new HashMap<>(); 603 enumParam.put(AbstractSourceType.QUESTION_PARAM_KEY, question); 604 I18nizableText label = sourceType.getEntry(new ChoiceOption(option), enumParam); 605 606 Map<String, Object> resultRule = new HashMap<>(); 607 resultRule.put("option", option); 608 resultRule.put("optionLabel", label); 609 resultRule.put("type", rule.getType()); 610 String pageId = rule.getPageId(); 611 if (pageId != null) 612 { 613 try 614 { 615 FormPage page = _resolver.resolveById(pageId); 616 resultRule.put("page", pageId); 617 resultRule.put("pageName", page.getTitle()); 618 } 619 catch (UnknownAmetysObjectException e) 620 { 621 // Page does not exist anymore 622 } 623 } 624 625 rules.add(resultRule); 626 } 627 628 result.put("rules", rules); 629 } 630 631 return result; 632 } 633 634 /** 635 * Adds a new rule to a question. 636 * @param id The question id 637 * @param option The option 638 * @param rule The rule type 639 * @param page The page to jump or skip 640 * @return An empty map, or an error 641 */ 642 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 643 public Map<String, Object> addPageRule (String id, String option, String rule, String page) 644 { 645 Map<String, Object> result = new HashMap<>(); 646 647 FormQuestion question = _resolver.resolveById(id); 648 _formDAO.checkHandleFormRight(question); 649 650 // Check if exists 651 if (question.hasPageRule(option)) 652 { 653 result.put("error", "already-exists"); 654 return result; 655 } 656 657 question.addPageRules(option, PageRuleType.valueOf(rule), page); 658 question.saveChanges(); 659 660 Map<String, Object> eventParams = new HashMap<>(); 661 eventParams.put("form", question.getForm()); 662 _observationManager.notify(new Event(ObservationConstants.FORM_MODIFIED, _getCurrentUser(), eventParams)); 663 664 result.put("id", question.getId()); 665 result.put("pageId", question.getFormPage().getId()); 666 result.put("formId", question.getForm().getId()); 667 result.put("type", question.getType().getId()); 668 return result; 669 } 670 671 /** 672 * Deletes a rule to a question. 673 * @param id The question id 674 * @param option The option to delete 675 * @return An empty map 676 */ 677 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 678 public Map<String, Object> deletePageRule (String id, String option) 679 { 680 FormQuestion question = _resolver.resolveById(id); 681 _formDAO.checkHandleFormRight(question); 682 683 question.deletePageRule(option); 684 question.saveChanges(); 685 686 Map<String, Object> eventParams = new HashMap<>(); 687 eventParams.put("form", question.getForm()); 688 _observationManager.notify(new Event(ObservationConstants.FORM_MODIFIED, _getCurrentUser(), eventParams)); 689 690 return new HashMap<>(); 691 } 692 693 /** 694 * Record for entry values coming from input or from the entry 695 * @param inputValues the inputValues. Can be null if the entry is not null 696 * @param entry the form entry. Can be null if the input values is not null 697 */ 698 public record FormEntryValues(Map<String, Object> inputValues, FormEntry entry) { 699 Object getValue(String attributeName) 700 { 701 if (inputValues != null) 702 { 703 return inputValues.get(attributeName); 704 } 705 else 706 { 707 return entry.getValue(attributeName); 708 } 709 } 710 } 711 712 /** 713 * Get the list of active question depending of the form rules 714 * @param form the form 715 * @param entryValues the entry values to compute rules 716 * @param currentStepId the current step id. Can be empty if the form has no workflow 717 * @param onlyWritableQuestion <code>true</code> to have only writable question 718 * @param onlyReadableQuestion <code>true</code> to have only readable question 719 * @return the list of active question depending of the form rules 720 */ 721 public List<FormQuestion> getRuleFilteredQuestions(Form form, FormEntryValues entryValues, Optional<Long> currentStepId, boolean onlyWritableQuestion, boolean onlyReadableQuestion) 722 { 723 List<FormQuestion> filteredQuestions = new ArrayList<>(); 724 for (FormQuestion activeQuestion : _getActiveQuestions(form, entryValues, currentStepId, onlyWritableQuestion, onlyReadableQuestion)) 725 { 726 if (!activeQuestion.getType().onlyForDisplay(activeQuestion)) 727 { 728 Optional<Rule> firstQuestionRule = activeQuestion.getFirstQuestionRule(); 729 if (firstQuestionRule.isPresent()) 730 { 731 Rule rule = firstQuestionRule.get(); 732 FormQuestion sourceQuestion = _resolver.resolveById(rule.getSourceId()); 733 List<String> ruleValues = _getRuleValues(entryValues, sourceQuestion.getNameForForm()); 734 boolean equalsRuleOption = ruleValues.contains(rule.getOption()); 735 QuestionRuleType ruleAction = rule.getAction(); 736 737 if (!equalsRuleOption && ruleAction.equals(QuestionRuleType.HIDE) 738 || equalsRuleOption && ruleAction.equals(QuestionRuleType.SHOW)) 739 { 740 filteredQuestions.add(activeQuestion); 741 } 742 } 743 else 744 { 745 filteredQuestions.add(activeQuestion); 746 } 747 } 748 } 749 750 return filteredQuestions; 751 } 752 753 /** 754 * Get a list of the form questions not being hidden by a rule 755 * @param form the current form 756 * @param entryValues the entry values 757 * @param currentStepId current step of the entry. Can be empty if the form has no workflow 758 * @param onlyWritableQuestion <code>true</code> to have only writable question 759 * @param onlyReadableQuestion <code>true</code> to have only readable question 760 * @return a list of visible questions 761 */ 762 protected List<FormQuestion> _getActiveQuestions(Form form, FormEntryValues entryValues, Optional<Long> currentStepId, boolean onlyWritableQuestion, boolean onlyReadableQuestion) 763 { 764 String nextActivePage = null; 765 List<FormQuestion> activeQuestions = new ArrayList<>(); 766 for (FormPage page : form.getPages()) 767 { 768 if (nextActivePage == null || page.getId().equals(nextActivePage)) 769 { 770 nextActivePage = null; 771 for (FormQuestion question : page.getQuestions()) 772 { 773 if (_isQuestionAuthorized(question, currentStepId, onlyWritableQuestion, onlyReadableQuestion)) 774 { 775 activeQuestions.add(question); 776 } 777 778 if (question.getType() instanceof ChoicesListQuestionType type && !type.getSourceType(question).remoteData()) 779 { 780 List<String> ruleValues = _getRuleValues(entryValues, question.getNameForForm()); 781 for (FormPageRule rule : question.getPageRules()) 782 { 783 if (ruleValues.contains(rule.getOption())) 784 { 785 nextActivePage = _getNextActivePage(rule); 786 } 787 } 788 } 789 } 790 } 791 792 FormPageRule rule = page.getRule(); 793 if (rule != null && nextActivePage == null) 794 { 795 nextActivePage = _getNextActivePage(rule); 796 } 797 } 798 return activeQuestions; 799 } 800 801 private boolean _isQuestionAuthorized(FormQuestion question, Optional<Long> currentStepId, boolean onlyWritableQuestion, boolean onlyReadableQuestion) 802 { 803 return currentStepId.isEmpty() // no current step id, ignore rights access 804 || (!onlyReadableQuestion || question.canRead(currentStepId.get())) 805 && 806 (!onlyWritableQuestion || question.canWrite(currentStepId.get())); 807 } 808 809 private String _getNextActivePage(FormPageRule rule) 810 { 811 return rule.getType() == PageRuleType.FINISH 812 ? "finish" 813 : rule.getPageId(); 814 } 815 816 private List<String> _getRuleValues(FormEntryValues entryValues, String nameForForm) 817 { 818 Object ruleValue = entryValues.getValue(nameForForm); 819 if (ruleValue == null) 820 { 821 return ListUtils.EMPTY_LIST; 822 } 823 824 if (ruleValue.getClass().isArray()) 825 { 826 String[] stringArray = ArrayUtils.toStringArray((Object[]) ruleValue); 827 return Arrays.asList(stringArray); 828 } 829 else 830 { 831 return List.of(ruleValue.toString()); 832 } 833 } 834 835 /** 836 * <code>true</code> if the submitter can edit submission for the given question 837 * @param question the question 838 * @return <code>true</code> if the submitter can edit submission for the given question 839 */ 840 public boolean canSubmitterEditSubmission(FormQuestion question) 841 { 842 FormQuestionType type = question.getType(); 843 return !(type instanceof ComputedQuestionType || type instanceof RichTextQuestionType); 844 } 845}