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.lang3.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.impl.ChoicesListQuestionType; 058import org.ametys.plugins.forms.question.types.impl.ComputedQuestionType; 059import org.ametys.plugins.forms.question.types.impl.RichTextQuestionType; 060import org.ametys.plugins.forms.repository.CopyFormUpdater; 061import org.ametys.plugins.forms.repository.CopyFormUpdaterExtensionPoint; 062import org.ametys.plugins.forms.repository.Form; 063import org.ametys.plugins.forms.repository.FormEntry; 064import org.ametys.plugins.forms.repository.FormPage; 065import org.ametys.plugins.forms.repository.FormPageRule; 066import org.ametys.plugins.forms.repository.FormPageRule.PageRuleType; 067import org.ametys.plugins.forms.repository.FormQuestion; 068import org.ametys.plugins.forms.repository.type.Rule; 069import org.ametys.plugins.forms.repository.type.Rule.QuestionRuleType; 070import org.ametys.plugins.forms.rights.FormsDirectoryRightAssignmentContext; 071import org.ametys.plugins.repository.AmetysObjectResolver; 072import org.ametys.plugins.repository.UnknownAmetysObjectException; 073import org.ametys.plugins.repository.jcr.NameHelper; 074import org.ametys.plugins.repository.jcr.NameHelper.NameComputationMode; 075import org.ametys.runtime.i18n.I18nizableText; 076import org.ametys.runtime.model.DefinitionContext; 077import org.ametys.runtime.model.ElementDefinition; 078import org.ametys.runtime.model.Model; 079import org.ametys.runtime.model.ModelItem; 080import org.ametys.runtime.model.View; 081import org.ametys.runtime.plugin.component.AbstractLogEnabled; 082import org.ametys.web.parameters.ParametersManager; 083 084/** DAO for manipulating form questions */ 085public class FormQuestionDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 086{ 087 /** The Avalon role */ 088 public static final String ROLE = FormQuestionDAO.class.getName(); 089 090 /** Name for rules root jcr node */ 091 public static final String RULES_ROOT = "ametys-internal:form-page-rules"; 092 093 /** Ametys object resolver. */ 094 protected AmetysObjectResolver _resolver; 095 /** Observer manager. */ 096 protected ObservationManager _observationManager; 097 /** The current user provider. */ 098 protected CurrentUserProvider _currentUserProvider; 099 /** Manager for retrieving uploaded files */ 100 protected UploadManager _uploadManager; 101 /** JSON helper */ 102 protected JSONUtils _jsonUtils; 103 /** I18n Utils */ 104 protected I18nUtils _i18nUtils; 105 /** The form question type extension point */ 106 protected FormQuestionTypeExtensionPoint _formQuestionTypeExtensionPoint; 107 /** The parameters manager */ 108 protected ParametersManager _parametersManager; 109 /** The Avalon context */ 110 protected Context _context; 111 /** The cocoon context */ 112 protected org.apache.cocoon.environment.Context _cocoonContext; 113 /** The choice source type extension point */ 114 protected ChoiceSourceTypeExtensionPoint _choiceSourceTypeExtensionPoint; 115 /**The form DAO */ 116 protected FormDAO _formDAO; 117 /** The right manager */ 118 protected RightManager _rightManager; 119 /** The copy form updater extension point */ 120 protected CopyFormUpdaterExtensionPoint _copyFormEP; 121 122 @Override 123 public void service(ServiceManager serviceManager) throws ServiceException 124 { 125 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 126 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 127 _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE); 128 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 129 _uploadManager = (UploadManager) serviceManager.lookup(UploadManager.ROLE); 130 _jsonUtils = (JSONUtils) serviceManager.lookup(JSONUtils.ROLE); 131 _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE); 132 _formQuestionTypeExtensionPoint = (FormQuestionTypeExtensionPoint) serviceManager.lookup(FormQuestionTypeExtensionPoint.ROLE); 133 _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE); 134 _formDAO = (FormDAO) serviceManager.lookup(FormDAO.ROLE); 135 _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE); 136 _copyFormEP = (CopyFormUpdaterExtensionPoint) serviceManager.lookup(CopyFormUpdaterExtensionPoint.ROLE); 137 } 138 139 @Override 140 public void contextualize(Context context) throws ContextException 141 { 142 _context = context; 143 _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 144 } 145 146 /** 147 * Provides the current user. 148 * @return the user which cannot be <code>null</code>. 149 */ 150 protected UserIdentity _getCurrentUser() 151 { 152 return _currentUserProvider.getUser(); 153 } 154 155 /** 156 * Gets properties of a form question 157 * @param id The id of the form question 158 * @return The properties 159 */ 160 @Callable (rights = Callable.NO_CHECK_REQUIRED) 161 public Map<String, Object> getQuestionProperties (String id) 162 { 163 // Assume that no read access is checked (required for bus message target) 164 try 165 { 166 FormQuestion question = _resolver.resolveById(id); 167 return getQuestionProperties(question, true); 168 } 169 catch (UnknownAmetysObjectException e) 170 { 171 getLogger().warn("Can't find question with id: {}. It probably has just been deleted", id, e); 172 Map<String, Object> infos = new HashMap<>(); 173 infos.put("id", id); 174 return infos; 175 } 176 } 177 178 /** 179 * Gets properties of a form question 180 * @param question The form question 181 * @param withRight <code>true</code> to have the rights in the properties 182 * @return The properties 183 */ 184 public Map<String, Object> getQuestionProperties (FormQuestion question, boolean withRight) 185 { 186 Map<String, Object> properties = new HashMap<>(); 187 188 boolean hasTerminalRule = _hasTerminalRule(question); 189 List<String> questionTitlesWithRule = _getQuestionTitlesWithRule(question); 190 List<String> pageTitlesWithRule = _getPageTitlesWithRule(question); 191 192 properties.put("type", "question"); 193 properties.put("hasTerminalRule", hasTerminalRule); 194 properties.put("pageTitlesWithRule", pageTitlesWithRule); 195 properties.put("questionTitlesWithRule", questionTitlesWithRule); 196 properties.put("isReadRestricted", question.isReadRestricted()); 197 properties.put("isModifiable", question.isModifiable()); 198 properties.put("hasChildren", false); 199 200 /** Use in the bus message */ 201 properties.put("id", question.getId()); 202 properties.put("title", question.getTitle()); 203 properties.put("questionType", question.getType().getId()); 204 properties.put("pageId", question.getFormPage().getId()); 205 properties.put("formId", question.getForm().getId()); 206 properties.put("iconGlyph", question.getType().getIconGlyph()); 207 properties.put("typeLabel", question.getType().getLabel()); 208 properties.put("hasEntries", !question.getForm().getEntries().isEmpty()); 209 properties.put("hasRule", hasTerminalRule || !pageTitlesWithRule.isEmpty() || !questionTitlesWithRule.isEmpty()); 210 properties.put("isConfigured", question.getType().isQuestionConfigured(question)); 211 212 if (withRight) 213 { 214 properties.put("rights", _getUserRights(question)); 215 } 216 else 217 { 218 properties.put("canWrite", _formDAO.hasWriteRightOnForm(_currentUserProvider.getUser(), question)); 219 } 220 221 return properties; 222 } 223 224 /** 225 * Get options from the choice list question 226 * @param questionId the choice list question id 227 * @return the map of option 228 */ 229 @Callable (rights = FormDAO.HANDLE_FORMS_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 0) 230 public Map<String, I18nizableText> getChoiceListQuestionOptions (String questionId) 231 { 232 FormQuestion question = _resolver.resolveById(questionId); 233 if (question.getType() instanceof ChoicesListQuestionType type) 234 { 235 return type.getOptions(question); 236 } 237 238 return new HashMap<>(); 239 } 240 241 /** 242 * Get user rights for the given form question 243 * @param question the form question 244 * @return the set of rights 245 */ 246 protected Set<String> _getUserRights (FormQuestion question) 247 { 248 UserIdentity user = _currentUserProvider.getUser(); 249 return _rightManager.getUserRights(user, question); 250 } 251 252 private boolean _hasTerminalRule(FormQuestion question) 253 { 254 return question.getPageRules() 255 .stream() 256 .map(FormPageRule::getType) 257 .filter(t -> t == PageRuleType.FINISH) 258 .findAny() 259 .isPresent(); 260 } 261 262 /** 263 * Get the question titles having rule concerning the given question 264 * @param question the question 265 * @return the list of question titles 266 */ 267 protected List<String> _getQuestionTitlesWithRule(FormQuestion question) 268 { 269 return question.getForm() 270 .getQuestionsRule(question.getId()) 271 .keySet() 272 .stream() 273 .map(FormQuestion::getTitle) 274 .map(org.ametys.core.util.StringUtils::escapeHTML) 275 .toList(); 276 } 277 278 /** 279 * Get the page titles having rule concerning the given question 280 * @param question the question 281 * @return the list of page titles 282 */ 283 protected List<String> _getPageTitlesWithRule(FormQuestion question) 284 { 285 return question.getPageRules() 286 .stream() 287 .filter(r -> r.getType() != PageRuleType.FINISH) 288 .map(FormPageRule::getPageId) 289 .distinct() 290 .map(this::_getFormPage) 291 .map(FormPage::getTitle) 292 .map(org.ametys.core.util.StringUtils::escapeHTML) 293 .toList(); 294 } 295 296 private FormPage _getFormPage(String pageId) 297 { 298 return _resolver.resolveById(pageId); 299 } 300 301 /** 302 * Get view for question type 303 * @param typeID id of the question type 304 * @param formId id of the form 305 * @return the view parsed in json for configurableFormPanel 306 * @throws ProcessingException error while parsing view to json 307 */ 308 @Callable (rights = FormDAO.HANDLE_FORMS_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 1) 309 public Map<String, Object> getQuestionParametersDefinitions(String typeID, String formId) throws ProcessingException 310 { 311 Map<String, Object> response = new HashMap<>(); 312 Form form = _resolver.resolveById(formId); 313 FormQuestionType questionType = _formQuestionTypeExtensionPoint.getExtension(typeID); 314 View view = questionType.getView(form); 315 response.put("parameters", view.toJSON(DefinitionContext.newInstance().withEdition(true))); 316 response.put("questionNames", form.getQuestionsNames()); 317 return response; 318 } 319 320 /** 321 * Get questions parameters values 322 * @param questionID id of current question 323 * @return map of question parameters value 324 */ 325 @Callable (rights = FormDAO.HANDLE_FORMS_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 0) 326 public Map<String, Object> getQuestionParametersValues(String questionID) 327 { 328 Map<String, Object> results = new HashMap<>(); 329 330 FormQuestion question = _resolver.resolveById(questionID); 331 FormQuestionType type = question.getType(); 332 Collection< ? extends ModelItem> questionModelItems = type.getModel().getModelItems(); 333 Map<String, Object> parametersValues = _parametersManager.getParametersValues(questionModelItems, question, StringUtils.EMPTY); 334 results.put("values", parametersValues); 335 336 // Repeater values aren't handled by getParametersValues() 337 @SuppressWarnings("unchecked") 338 List<Map<String, Object>> repeaters = _parametersManager.getRepeatersValues((Collection<ModelItem>) questionModelItems, question, StringUtils.EMPTY); 339 results.put("repeaters", repeaters); 340 results.put("fieldToDisable", _getFieldNameToDisable(question)); 341 342 return results; 343 } 344 345 private List<String> _getFieldNameToDisable(FormQuestion question) 346 { 347 Form form = question.getForm(); 348 if (form.getEntries().isEmpty()) 349 { 350 return List.of(); 351 } 352 353 return question.getType().getFieldToDisableIfFormPublished(question); 354 } 355 356 /** 357 * Creates a {@link FormQuestion}. 358 * @param pageId id of current page 359 * @param typeId id of FormQuestionType 360 * @return The id of the created form question, the id of the page and the id of the form 361 */ 362 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 363 public Map<String, Object> createQuestion(String pageId, String typeId) 364 { 365 Map<String, Object> result = new HashMap<>(); 366 FormPage page = _resolver.resolveById(pageId); 367 368 _formDAO.checkHandleFormRight(page); 369 370 FormQuestionType type = _formQuestionTypeExtensionPoint.getExtension(typeId); 371 Form form = page.getForm(); 372 373 String defaultTitle = _i18nUtils.translate(type.getDefaultTitle()); 374 String nameForForm = form.findUniqueQuestionName(defaultTitle); 375 376 FormQuestion question = page.createChild(NameHelper.getUniqueAmetysObjectName(page, nameForForm, NameComputationMode.GENERATED_KEY, false), "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 (rights = Callable.CHECKED_BY_IMPLEMENTATION) 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 (rights = Callable.CHECKED_BY_IMPLEMENTATION) 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 (rights = Callable.CHECKED_BY_IMPLEMENTATION) 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 (rights = Callable.CHECKED_BY_IMPLEMENTATION) 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 uniqueName = parentForm.findUniqueQuestionName(originalQuestion.getNameForForm()); 534 FormQuestion questionCopy = parentPage.createChild(uniqueName, "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(uniqueName); 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 (rights = Callable.CHECKED_BY_IMPLEMENTATION) 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 (rights = Callable.CHECKED_BY_IMPLEMENTATION) 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 (rights = Callable.CHECKED_BY_IMPLEMENTATION) 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 819 /** 820 * <code>true</code> if the submitter can edit submission for the given question 821 * @param question the question 822 * @return <code>true</code> if the submitter can edit submission for the given question 823 */ 824 public boolean canSubmitterEditSubmission(FormQuestion question) 825 { 826 FormQuestionType type = question.getType(); 827 return !(type instanceof ComputedQuestionType || type instanceof RichTextQuestionType); 828 } 829}