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