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