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