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.repository.AmetysObjectResolver; 069import org.ametys.plugins.repository.UnknownAmetysObjectException; 070import org.ametys.plugins.repository.jcr.NameHelper; 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 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 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 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 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 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(NameHelper.filterName(defaultTitle)); 368 369 String id = nameForForm; 370 int i = 1; 371 while (page.hasChild(id)) 372 { 373 id = nameForForm + "-" + i; 374 i++; 375 } 376 FormQuestion question = page.createChild(id, "ametys:form-question"); 377 question.setNameForForm(nameForForm); 378 question.setTypeId(typeId); 379 380 Model model = question.getType().getModel(); 381 for (ModelItem modelItem : model.getModelItems()) 382 { 383 if (modelItem instanceof ElementDefinition) 384 { 385 Object defaultValue = ((ElementDefinition) modelItem).getDefaultValue(); 386 if (defaultValue != null) 387 { 388 question.setValue(modelItem.getPath(), defaultValue); 389 } 390 } 391 } 392 393 question.setTitle(form.findUniqueQuestionTitle(defaultTitle)); 394 395 page.saveChanges(); 396 397 Map<String, Object> eventParams = new HashMap<>(); 398 eventParams.put("form", page.getForm()); 399 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 400 401 result.put("id", question.getId()); 402 result.put("pageId", page.getId()); 403 result.put("formId", question.getForm().getId()); 404 result.put("type", typeId); 405 return result; 406 } 407 408 /** 409 * Rename a {@link FormQuestion} 410 * @param id The id of the question 411 * @param newName The new name of the question 412 * @return A result map 413 */ 414 @Callable 415 public Map<String, String> renameQuestion (String id, String newName) 416 { 417 Map<String, String> results = new HashMap<>(); 418 419 FormQuestion question = _resolver.resolveById(id); 420 _formDAO.checkHandleFormRight(question); 421 422 question.setTitle(newName); 423 question.saveChanges(); 424 425 Map<String, Object> eventParams = new HashMap<>(); 426 eventParams.put("form", question.getForm()); 427 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 428 429 results.put("id", id); 430 results.put("newName", newName); 431 results.put("formId", question.getForm().getId()); 432 433 return results; 434 } 435 436 /** 437 * Edits a {@link FormQuestion}. 438 * @param questionId id of current question 439 * @param values The question's values 440 * @return The id of the edited form question, the id of the page and the id of the form 441 */ 442 @Callable 443 public Map<String, Object> editQuestion (String questionId, Map<String, Object> values) 444 { 445 Map<String, Object> result = new HashMap<>(); 446 Map<String, I18nizableText> errors = new HashMap<>(); 447 448 FormQuestion question = _resolver.resolveById(questionId); 449 _formDAO.checkHandleFormRight(question); 450 451 Form parentForm = question.getForm(); 452 String questionName = StringUtils.defaultString((String) values.get("name-for-form")); 453 454 // if question can not be answered by user, id can't be changed and is unique by default 455 if (!question.getType().canBeAnsweredByUser(question) || questionName.equals(question.getNameForForm()) || parentForm.isQuestionNameUnique(questionName)) 456 { 457 FormQuestionType type = question.getType(); 458 type.validateQuestionValues(values, errors); 459 460 if (!errors.isEmpty()) 461 { 462 result.put("errors", errors); 463 return result; 464 } 465 466 _parametersManager.setParameterValues(question.getDataHolder(), type.getModel().getModelItems(), values); 467 type.doAdditionalOperations(question, values); 468 469 question.saveChanges(); 470 471 Map<String, Object> eventParams = new HashMap<>(); 472 eventParams.put("form", parentForm); 473 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 474 475 result.put("id", question.getId()); 476 result.put("pageId", question.getParent().getId()); 477 result.put("formId", parentForm.getId()); 478 result.put("type", question.getType().toString()); 479 } 480 else 481 { 482 errors.put("duplicate_name", new I18nizableText("plugin.forms", "PLUGINS_FORMS_QUESTIONS_SET_ID_ERROR")); 483 result.put("errors", errors); 484 getLogger().error("An error occurred creating the question. The identifier value '" + questionName + "' is already used."); 485 } 486 487 return result; 488 } 489 490 /** 491 * Deletes a {@link FormQuestion}. 492 * @param id The id of the form question to delete 493 * @return The id of the form question, the id of the page and the id of the form 494 */ 495 @Callable 496 public Map<String, String> deleteQuestion (String id) 497 { 498 FormQuestion question = _resolver.resolveById(id); 499 _formDAO.checkHandleFormRight(question); 500 501 question.getForm().deleteQuestionsRule(question.getId()); 502 503 FormPage page = question.getParent(); 504 question.remove(); 505 506 page.saveChanges(); 507 508 Map<String, Object> eventParams = new HashMap<>(); 509 eventParams.put("form", page.getForm()); 510 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 511 512 return Map.of("id", id); 513 } 514 515 /** 516 * Copies and pastes a form question. 517 * @param pageId The id of the page, target of the copy 518 * @param questionId The id of the question to copy 519 * @return The id of the created question, the id of the page and the id of the form 520 */ 521 @Callable 522 public Map<String, String> copyQuestion(String pageId, String questionId) 523 { 524 Map<String, String> result = new HashMap<>(); 525 526 FormQuestion originalQuestion = _resolver.resolveById(questionId); 527 _formDAO.checkHandleFormRight(originalQuestion); 528 529 FormPage parentPage = _resolver.resolveById(pageId); 530 531 Form parentForm = parentPage.getForm(); 532 533 String questionName = parentForm.findUniqueQuestionName(originalQuestion.getNameForForm()); 534 FormQuestion questionCopy = parentPage.createChild(questionName, "ametys:form-question"); 535 originalQuestion.copyTo(questionCopy); 536 537 String copyTitle = _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGIN_FORMS_TREE_COPY_NAME_PREFIX")) + originalQuestion.getTitle(); 538 questionCopy.setTitle(parentForm.findUniqueQuestionTitle(copyTitle)); 539 questionCopy.setTypeId(originalQuestion.getType().getId()); 540 questionCopy.setNameForForm(questionName); 541 542 for (String epId : _copyFormEP.getExtensionsIds()) 543 { 544 CopyFormUpdater copyFormUpdater = _copyFormEP.getExtension(epId); 545 copyFormUpdater.updateFormQuestion(originalQuestion, questionCopy); 546 } 547 548 parentPage.saveChanges(); 549 550 Map<String, Object> eventParams = new HashMap<>(); 551 eventParams.put("form", parentForm); 552 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 553 554 result.put("id", questionCopy.getId()); 555 result.put("pageId", parentPage.getId()); 556 result.put("formId", parentForm.getId()); 557 result.put("type", questionCopy.getType().getId()); 558 559 return result; 560 } 561 562 /** 563 * Gets the page rules for a form question. 564 * @param id The id of the form question. 565 * @param number The question number 566 * @return The rules 567 * @throws Exception error while getting choice options 568 */ 569 @Callable 570 public Map<String, Object> getRules (String id, int number) throws Exception 571 { 572 Map<String, Object> result = new HashMap<>(); 573 574 FormQuestion question = _resolver.resolveById(id); 575 _formDAO.checkHandleFormRight(question); 576 577 FormQuestionType type = question.getType(); 578 if (type instanceof ChoicesListQuestionType cLType) 579 { 580 ChoiceSourceType sourceType = cLType.getSourceType(question); 581 582 result.put("id", question.getId()); 583 result.put("number", String.valueOf(number)); 584 result.put("title", question.getTitle()); 585 586 List<Object> rules = new ArrayList<>(); 587 for (FormPageRule rule : question.getPageRules()) 588 { 589 String option = rule.getOption(); 590 Map<String, Object> enumParam = new HashMap<>(); 591 enumParam.put(AbstractSourceType.QUESTION_PARAM_KEY, question); 592 I18nizableText label = sourceType.getEntry(new ChoiceOption(option), enumParam); 593 594 Map<String, Object> resultRule = new HashMap<>(); 595 resultRule.put("option", option); 596 resultRule.put("optionLabel", label); 597 resultRule.put("type", rule.getType()); 598 String pageId = rule.getPageId(); 599 if (pageId != null) 600 { 601 try 602 { 603 FormPage page = _resolver.resolveById(pageId); 604 resultRule.put("page", pageId); 605 resultRule.put("pageName", page.getTitle()); 606 } 607 catch (UnknownAmetysObjectException e) 608 { 609 // Page does not exist anymore 610 } 611 } 612 613 rules.add(resultRule); 614 } 615 616 result.put("rules", rules); 617 } 618 619 return result; 620 } 621 622 /** 623 * Adds a new rule to a question. 624 * @param id The question id 625 * @param option The option 626 * @param rule The rule type 627 * @param page The page to jump or skip 628 * @return An empty map, or an error 629 */ 630 @Callable 631 public Map<String, Object> addPageRule (String id, String option, String rule, String page) 632 { 633 Map<String, Object> result = new HashMap<>(); 634 635 FormQuestion question = _resolver.resolveById(id); 636 _formDAO.checkHandleFormRight(question); 637 638 // Check if exists 639 if (question.hasPageRule(option)) 640 { 641 result.put("error", "already-exists"); 642 return result; 643 } 644 645 question.addPageRules(option, PageRuleType.valueOf(rule), page); 646 question.saveChanges(); 647 648 Map<String, Object> eventParams = new HashMap<>(); 649 eventParams.put("form", question.getForm()); 650 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 651 652 result.put("id", question.getId()); 653 result.put("pageId", question.getFormPage().getId()); 654 result.put("formId", question.getForm().getId()); 655 result.put("type", question.getType().getId()); 656 return result; 657 } 658 659 /** 660 * Deletes a rule to a question. 661 * @param id The question id 662 * @param option The option to delete 663 * @return An empty map 664 */ 665 @Callable 666 public Map<String, Object> deletePageRule (String id, String option) 667 { 668 FormQuestion question = _resolver.resolveById(id); 669 _formDAO.checkHandleFormRight(question); 670 671 question.deletePageRule(option); 672 question.saveChanges(); 673 674 Map<String, Object> eventParams = new HashMap<>(); 675 eventParams.put("form", question.getForm()); 676 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 677 678 return new HashMap<>(); 679 } 680 681 /** 682 * Record for entry values coming from input or from the entry 683 * @param inputValues the inputValues. Can be null if the entry is not null 684 * @param entry the form entry. Can be null if the input values is not null 685 */ 686 public record FormEntryValues(Map<String, Object> inputValues, FormEntry entry) 687 { 688 Object getValue(String attributeName) 689 { 690 if (inputValues != null) 691 { 692 return inputValues.get(attributeName); 693 } 694 else 695 { 696 return entry.getValue(attributeName); 697 } 698 } 699 } 700 701 /** 702 * Get the list of active question depending of the form rules 703 * @param form the form 704 * @param entryValues the entry values to compute rules 705 * @param currentStepId the current step id. Can be empty if the form has no workflow 706 * @param onlyWritableQuestion <code>true</code> to have only writable question 707 * @param onlyReadableQuestion <code>true</code> to have only readable question 708 * @return the list of active question depending of the form rules 709 */ 710 public List<FormQuestion> getRuleFilteredQuestions(Form form, FormEntryValues entryValues, Optional<Long> currentStepId, boolean onlyWritableQuestion, boolean onlyReadableQuestion) 711 { 712 List<FormQuestion> filteredQuestions = new ArrayList<>(); 713 for (FormQuestion activeQuestion : _getActiveQuestions(form, entryValues, currentStepId, onlyWritableQuestion, onlyReadableQuestion)) 714 { 715 if (!activeQuestion.getType().onlyForDisplay(activeQuestion)) 716 { 717 Optional<Rule> firstQuestionRule = activeQuestion.getFirstQuestionRule(); 718 if (firstQuestionRule.isPresent()) 719 { 720 Rule rule = firstQuestionRule.get(); 721 FormQuestion sourceQuestion = _resolver.resolveById(rule.getSourceId()); 722 List<String> ruleValues = _getRuleValues(entryValues, sourceQuestion.getNameForForm()); 723 boolean equalsRuleOption = ruleValues.contains(rule.getOption()); 724 QuestionRuleType ruleAction = rule.getAction(); 725 726 if (!equalsRuleOption && ruleAction.equals(QuestionRuleType.HIDE) 727 || equalsRuleOption && ruleAction.equals(QuestionRuleType.SHOW)) 728 { 729 filteredQuestions.add(activeQuestion); 730 } 731 } 732 else 733 { 734 filteredQuestions.add(activeQuestion); 735 } 736 } 737 } 738 739 return filteredQuestions; 740 } 741 742 /** 743 * Get a list of the form questions not being hidden by a rule 744 * @param form the current form 745 * @param entryValues the entry values 746 * @param currentStepId current step of the entry. Can be empty if the form has no workflow 747 * @param onlyWritableQuestion <code>true</code> to have only writable question 748 * @param onlyReadableQuestion <code>true</code> to have only readable question 749 * @return a list of visible questions 750 */ 751 protected List<FormQuestion> _getActiveQuestions(Form form, FormEntryValues entryValues, Optional<Long> currentStepId, boolean onlyWritableQuestion, boolean onlyReadableQuestion) 752 { 753 String nextActivePage = null; 754 List<FormQuestion> activeQuestions = new ArrayList<>(); 755 for (FormPage page : form.getPages()) 756 { 757 if (nextActivePage == null || page.getId().equals(nextActivePage)) 758 { 759 nextActivePage = null; 760 for (FormQuestion question : page.getQuestions()) 761 { 762 if (currentStepId.isEmpty() // no current step id, ignore rights access 763 || (!onlyReadableQuestion || question.canRead(currentStepId.get())) 764 && 765 (!onlyWritableQuestion || question.canWrite(currentStepId.get()))) 766 { 767 activeQuestions.add(question); 768 } 769 770 if (question.getType() instanceof ChoicesListQuestionType type && !type.getSourceType(question).remoteData()) 771 { 772 List<String> ruleValues = _getRuleValues(entryValues, question.getNameForForm()); 773 for (FormPageRule rule : question.getPageRules()) 774 { 775 if (ruleValues.contains(rule.getOption())) 776 { 777 nextActivePage = _getNextActivePage(rule); 778 } 779 } 780 } 781 } 782 } 783 784 FormPageRule rule = page.getRule(); 785 if (rule != null && nextActivePage == null) 786 { 787 nextActivePage = _getNextActivePage(rule); 788 } 789 } 790 return activeQuestions; 791 } 792 793 private String _getNextActivePage(FormPageRule rule) 794 { 795 return rule.getType() == PageRuleType.FINISH 796 ? "finish" 797 : rule.getPageId(); 798 } 799 800 private List<String> _getRuleValues(FormEntryValues entryValues, String nameForForm) 801 { 802 Object ruleValue = entryValues.getValue(nameForForm); 803 if (ruleValue == null) 804 { 805 return ListUtils.EMPTY_LIST; 806 } 807 808 if (ruleValue.getClass().isArray()) 809 { 810 String[] stringArray = ArrayUtils.toStringArray((Object[]) ruleValue); 811 return Arrays.asList(stringArray); 812 } 813 else 814 { 815 return List.of(ruleValue.toString()); 816 } 817 } 818}