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