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