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.Collection; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.context.Context; 027import org.apache.avalon.framework.context.ContextException; 028import org.apache.avalon.framework.context.Contextualizable; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.cocoon.Constants; 033import org.apache.cocoon.ProcessingException; 034import org.apache.commons.lang.StringUtils; 035 036import org.ametys.core.observation.Event; 037import org.ametys.core.observation.ObservationManager; 038import org.ametys.core.right.RightManager; 039import org.ametys.core.right.RightManager.RightResult; 040import org.ametys.core.ui.Callable; 041import org.ametys.core.upload.UploadManager; 042import org.ametys.core.user.CurrentUserProvider; 043import org.ametys.core.user.UserIdentity; 044import org.ametys.core.util.I18nUtils; 045import org.ametys.core.util.JSONUtils; 046import org.ametys.plugins.forms.FormEvents; 047import org.ametys.plugins.forms.question.FormQuestionType; 048import org.ametys.plugins.forms.question.FormQuestionTypeExtensionPoint; 049import org.ametys.plugins.forms.question.sources.AbstractSourceType; 050import org.ametys.plugins.forms.question.sources.ChoiceOption; 051import org.ametys.plugins.forms.question.sources.ChoiceSourceType; 052import org.ametys.plugins.forms.question.sources.ChoiceSourceTypeExtensionPoint; 053import org.ametys.plugins.forms.question.types.ChoicesListQuestionType; 054import org.ametys.plugins.forms.repository.Form; 055import org.ametys.plugins.forms.repository.FormPage; 056import org.ametys.plugins.forms.repository.FormPageRule; 057import org.ametys.plugins.forms.repository.FormPageRule.PageRuleType; 058import org.ametys.plugins.forms.repository.FormQuestion; 059import org.ametys.plugins.repository.AmetysObject; 060import org.ametys.plugins.repository.AmetysObjectResolver; 061import org.ametys.plugins.repository.UnknownAmetysObjectException; 062import org.ametys.plugins.repository.jcr.NameHelper; 063import org.ametys.runtime.authentication.AccessDeniedException; 064import org.ametys.runtime.i18n.I18nizableText; 065import org.ametys.runtime.model.DefinitionContext; 066import org.ametys.runtime.model.ElementDefinition; 067import org.ametys.runtime.model.Model; 068import org.ametys.runtime.model.ModelItem; 069import org.ametys.runtime.model.View; 070import org.ametys.runtime.plugin.component.AbstractLogEnabled; 071import org.ametys.web.parameters.ParametersManager; 072 073/** DAO for manipulating form questions */ 074public class FormQuestionDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 075{ 076 /** The Avalon role */ 077 public static final String ROLE = FormQuestionDAO.class.getName(); 078 079 /** Name for rules root jcr node */ 080 public static final String RULES_ROOT = "ametys-internal:form-page-rules"; 081 082 /** The String representing the type of a form question node */ 083 protected static final String FORM_QUESTION_TYPE = "question"; 084 085 /** Ametys object resolver. */ 086 protected AmetysObjectResolver _resolver; 087 /** Observer manager. */ 088 protected ObservationManager _observationManager; 089 /** The current user provider. */ 090 protected CurrentUserProvider _currentUserProvider; 091 /** Manager for retrieving uploaded files */ 092 protected UploadManager _uploadManager; 093 /** JSON helper */ 094 protected JSONUtils _jsonUtils; 095 /** I18n Utils */ 096 protected I18nUtils _i18nUtils; 097 /** The form question type extension point */ 098 protected FormQuestionTypeExtensionPoint _formQuestionTypeExtensionPoint; 099 /** The parameters manager */ 100 protected ParametersManager _parametersManager; 101 /** The Avalon context */ 102 protected Context _context; 103 /** The cocoon context */ 104 protected org.apache.cocoon.environment.Context _cocoonContext; 105 /** The choice source type extension point */ 106 protected ChoiceSourceTypeExtensionPoint _choiceSourceTypeExtensionPoint; 107 /**The form DAO */ 108 protected FormDAO _formDAO; 109 /** The right manager */ 110 protected RightManager _rightManager; 111 112 @Override 113 public void service(ServiceManager serviceManager) throws ServiceException 114 { 115 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 116 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 117 _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE); 118 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 119 _uploadManager = (UploadManager) serviceManager.lookup(UploadManager.ROLE); 120 _jsonUtils = (JSONUtils) serviceManager.lookup(JSONUtils.ROLE); 121 _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE); 122 _formQuestionTypeExtensionPoint = (FormQuestionTypeExtensionPoint) serviceManager.lookup(FormQuestionTypeExtensionPoint.ROLE); 123 _parametersManager = (ParametersManager) serviceManager.lookup(ParametersManager.ROLE); 124 _formDAO = (FormDAO) serviceManager.lookup(FormDAO.ROLE); 125 _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE); 126 } 127 128 @Override 129 public void contextualize(Context context) throws ContextException 130 { 131 _context = context; 132 _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 133 } 134 135 /** 136 * Check rights for a form element as ametys object 137 * @param ao the ametys object 138 */ 139 protected void _checkRights(AmetysObject ao) 140 { 141 if (_rightManager.hasRight(_currentUserProvider.getUser(), FormDAO.HANDLE_FORMS_RIGHT_ID, ao) != RightResult.RIGHT_ALLOW) 142 { 143 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to handle forms without convenient right [" + FormDAO.HANDLE_FORMS_RIGHT_ID + "]"); 144 } 145 } 146 147 /** 148 * Provides the current user. 149 * @return the user which cannot be <code>null</code>. 150 */ 151 protected UserIdentity _getCurrentUser() 152 { 153 return _currentUserProvider.getUser(); 154 } 155 156 /** 157 * Gets properties of a form question 158 * @param id The id of the form question 159 * @return The properties 160 */ 161 @Callable 162 public Map<String, Object> getQuestionProperties (String id) 163 { 164 try 165 { 166 FormQuestion question = _resolver.resolveById(id); 167 return getQuestionProperties(question); 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 * @return The properties 182 */ 183 public Map<String, Object> getQuestionProperties (FormQuestion question) 184 { 185 Map<String, Object> properties = new HashMap<>(); 186 187 properties.put("id", question.getId()); 188 properties.put("nameForForm", question.getNameForForm()); 189 properties.put("type", FORM_QUESTION_TYPE); 190 properties.put("questionType", question.getType().getId()); 191 properties.put("title", question.getTitle()); 192 properties.put("pageId", question.getFormPage().getId()); 193 properties.put("formId", question.getForm().getId()); 194 properties.put("iconGlyph", question.getType().getIconGlyph()); 195 properties.put("typeLabel", question.getType().getLabel()); 196 properties.put("formHasEntries", !question.getForm().getEntries().isEmpty()); 197 198 boolean hasTerminalRule = _hasTerminalRule(question); 199 List<String> questionTitlesWithRule = _getQuestionTitlesWithRule(question); 200 List<String> pageTitlesWithRule = _getPageTitlesWithRule(question); 201 202 properties.put("hasTerminalRule", hasTerminalRule); 203 properties.put("pageTitlesWithRule", pageTitlesWithRule); 204 properties.put("questionTitlesWithRule", questionTitlesWithRule); 205 properties.put("hasRule", hasTerminalRule || !pageTitlesWithRule.isEmpty() || !questionTitlesWithRule.isEmpty()); 206 properties.put("hasChildren", false); 207 UserIdentity currentUser = _currentUserProvider.getUser(); 208 properties.put("canWrite", _formDAO.hasWriteRightOnForm(currentUser, question.getForm())); 209 properties.put("isConfigured", question.getType().isQuestionConfigured(question)); 210 211 if (question.getType() instanceof ChoicesListQuestionType type) 212 { 213 Map<String, I18nizableText> options = type.getOptions(question); 214 if (options.size() > 0) 215 { 216 properties.put("otherOption", type.hasOtherOption(question)); 217 properties.put("options", options); 218 } 219 } 220 221 return properties; 222 } 223 224 private boolean _hasTerminalRule(FormQuestion question) 225 { 226 return question.getPageRules() 227 .stream() 228 .map(FormPageRule::getType) 229 .filter(t -> t == PageRuleType.FINISH) 230 .findAny() 231 .isPresent(); 232 } 233 234 /** 235 * Get the question titles having rule concerning the given question 236 * @param question the question 237 * @return the list of question titles 238 */ 239 protected List<String> _getQuestionTitlesWithRule(FormQuestion question) 240 { 241 return question.getForm() 242 .getQuestionsRule(question.getId()) 243 .keySet() 244 .stream() 245 .map(FormQuestion::getTitle) 246 .toList(); 247 } 248 249 /** 250 * Get the page titles having rule concerning the given question 251 * @param question the question 252 * @return the list of page titles 253 */ 254 protected List<String> _getPageTitlesWithRule(FormQuestion question) 255 { 256 return question.getPageRules() 257 .stream() 258 .filter(r -> r.getType() != PageRuleType.FINISH) 259 .map(FormPageRule::getPageId) 260 .distinct() 261 .map(this::_getFormPage) 262 .map(FormPage::getTitle) 263 .toList(); 264 } 265 266 private FormPage _getFormPage(String pageId) 267 { 268 return _resolver.resolveById(pageId); 269 } 270 271 /** 272 * Get view for question type 273 * @param typeID id of the question type 274 * @param formId id of the form 275 * @return the view parsed in json for configurableFormPanel 276 * @throws ProcessingException error while parsing view to json 277 */ 278 @Callable 279 public Map<String, Object> getQuestionParametersDefinitions(String typeID, String formId) throws ProcessingException 280 { 281 Map<String, Object> response = new HashMap<>(); 282 Form form = _resolver.resolveById(formId); 283 FormQuestionType questionType = _formQuestionTypeExtensionPoint.getExtension(typeID); 284 View view = questionType.getView(); 285 response.put("parameters", view.toJSON(DefinitionContext.newInstance().withEdition(true))); 286 response.put("questionNames", form.getQuestionsNames()); 287 return response; 288 } 289 290 /** 291 * Get questions parameters values 292 * @param questionID id of current question 293 * @return map of question parameters value 294 */ 295 @Callable 296 public Map<String, Object> getQuestionParametersValues(String questionID) 297 { 298 Map<String, Object> results = new HashMap<>(); 299 300 FormQuestion question = _resolver.resolveById(questionID); 301 FormQuestionType type = question.getType(); 302 Collection< ? extends ModelItem> questionModelItems = type.getModel().getModelItems(); 303 Map<String, Object> parametersValues = _parametersManager.getParametersValues(questionModelItems, question, StringUtils.EMPTY); 304 results.put("values", parametersValues); 305 306 // Repeater values aren't handled by getParametersValues() 307 @SuppressWarnings("unchecked") 308 List<Map<String, Object>> repeaters = _parametersManager.getRepeatersValues((Collection<ModelItem>) questionModelItems, question, StringUtils.EMPTY); 309 results.put("repeaters", repeaters); 310 results.put("fieldToDisable", _getFieldNameToDisable(question)); 311 312 return results; 313 } 314 315 private List<String> _getFieldNameToDisable(FormQuestion question) 316 { 317 Form form = question.getForm(); 318 if (form.getEntries().isEmpty()) 319 { 320 return List.of(); 321 } 322 323 return question.getType().getFieldToDisableIfFormPublished(question); 324 } 325 326 /** 327 * Creates a {@link FormQuestion}. 328 * @param pageId id of current page 329 * @param typeId id of FormQuestionType 330 * @return The id of the created form question, the id of the page and the id of the form 331 */ 332 @Callable 333 public Map<String, Object> createQuestion(String pageId, String typeId) 334 { 335 Map<String, Object> result = new HashMap<>(); 336 FormPage page = _resolver.resolveById(pageId); 337 338 _checkRights(page); 339 340 FormQuestionType type = _formQuestionTypeExtensionPoint.getExtension(typeId); 341 Form form = page.getForm(); 342 343 String defaultTitle = _i18nUtils.translate(type.getDefaultTitle()); 344 String nameForForm = form.findUniqueQuestionName(NameHelper.filterName(defaultTitle)); 345 346 String id = nameForForm; 347 int i = 1; 348 while (page.hasChild(id)) 349 { 350 id = nameForForm + "-" + i; 351 i++; 352 } 353 FormQuestion question = page.createChild(id, "ametys:form-question"); 354 question.setNameForForm(nameForForm); 355 question.setTypeId(typeId); 356 357 Model model = question.getType().getModel(); 358 for (ModelItem modelItem : model.getModelItems()) 359 { 360 if (modelItem instanceof ElementDefinition) 361 { 362 Object defaultValue = ((ElementDefinition) modelItem).getDefaultValue(); 363 if (defaultValue != null) 364 { 365 question.setValue(modelItem.getPath(), defaultValue); 366 } 367 } 368 } 369 370 question.setTitle(form.findUniqueQuestionTitle(defaultTitle)); 371 372 page.saveChanges(); 373 374 Map<String, Object> eventParams = new HashMap<>(); 375 eventParams.put("form", page.getForm()); 376 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 377 378 result.put("id", question.getId()); 379 result.put("pageId", page.getId()); 380 result.put("formId", question.getForm().getId()); 381 result.put("type", typeId); 382 return result; 383 } 384 385 /** 386 * Rename a {@link FormQuestion} 387 * @param id The id of the question 388 * @param newName The new name of the question 389 * @return A result map 390 */ 391 @Callable 392 public Map<String, String> renameQuestion (String id, String newName) 393 { 394 Map<String, String> results = new HashMap<>(); 395 396 FormQuestion question = _resolver.resolveById(id); 397 _checkRights(question); 398 399 question.setTitle(newName); 400 question.saveChanges(); 401 402 Map<String, Object> eventParams = new HashMap<>(); 403 eventParams.put("form", question.getForm()); 404 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 405 406 results.put("id", id); 407 results.put("newName", newName); 408 results.put("formId", question.getForm().getId()); 409 410 return results; 411 } 412 413 /** 414 * Edits a {@link FormQuestion}. 415 * @param questionId id of current question 416 * @param values The question's values 417 * @return The id of the edited form question, the id of the page and the id of the form 418 */ 419 @Callable 420 public Map<String, Object> editQuestion (String questionId, Map<String, Object> values) 421 { 422 Map<String, Object> result = new HashMap<>(); 423 Map<String, I18nizableText> errors = new HashMap<>(); 424 425 FormQuestion question = _resolver.resolveById(questionId); 426 _checkRights(question); 427 428 Form parentForm = question.getForm(); 429 String questionName = StringUtils.defaultString((String) values.get("name-for-form")); 430 431 // if question can not be answered by user, id can't be changed and is unique by default 432 if (!question.getType().canBeAnsweredByUser(question) || questionName.equals(question.getNameForForm()) || parentForm.isQuestionNameUnique(questionName)) 433 { 434 FormQuestionType type = question.getType(); 435 type.validateQuestionValues(values, errors); 436 437 if (!errors.isEmpty()) 438 { 439 result.put("errors", errors); 440 return result; 441 } 442 443 _parametersManager.setParameterValues(question.getDataHolder(), type.getModel().getModelItems(), values); 444 type.doAdditionalOperations(question, values); 445 446 question.saveChanges(); 447 448 Map<String, Object> eventParams = new HashMap<>(); 449 eventParams.put("form", parentForm); 450 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 451 452 result.put("id", question.getId()); 453 result.put("pageId", question.getParent().getId()); 454 result.put("formId", parentForm.getId()); 455 result.put("type", question.getType().toString()); 456 } 457 else 458 { 459 errors.put("duplicate_name", new I18nizableText("plugin.forms", "PLUGINS_FORMS_QUESTIONS_SET_ID_ERROR")); 460 result.put("errors", errors); 461 getLogger().error("An error occurred creating the question. The identifier value '" + questionName + "' is already used."); 462 } 463 464 return result; 465 } 466 467 /** 468 * Deletes a {@link FormQuestion}. 469 * @param id The id of the form question to delete 470 * @return The id of the form question, the id of the page and the id of the form 471 */ 472 @Callable 473 public Map<String, String> deleteQuestion (String id) 474 { 475 FormQuestion question = _resolver.resolveById(id); 476 _checkRights(question); 477 478 question.getForm().deleteQuestionsRule(question.getId()); 479 480 FormPage page = question.getParent(); 481 question.remove(); 482 483 page.saveChanges(); 484 485 Map<String, Object> eventParams = new HashMap<>(); 486 eventParams.put("form", page.getForm()); 487 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 488 489 return Map.of("id", id); 490 } 491 492 /** 493 * Copies and pastes a form question. 494 * @param pageId The id of the page, target of the copy 495 * @param questionId The id of the question to copy 496 * @return The id of the created question, the id of the page and the id of the form 497 */ 498 @Callable 499 public Map<String, String> copyQuestion(String pageId, String questionId) 500 { 501 Map<String, String> result = new HashMap<>(); 502 503 FormQuestion originalQuestion = _resolver.resolveById(questionId); 504 _checkRights(originalQuestion); 505 506 FormPage parentPage = _resolver.resolveById(pageId); 507 508 Form parentForm = parentPage.getForm(); 509 510 String questionName = parentForm.findUniqueQuestionName(originalQuestion.getNameForForm()); 511 FormQuestion questionCopy = parentPage.createChild(questionName, "ametys:form-question"); 512 originalQuestion.copyTo(questionCopy); 513 514 String copyTitle = _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGIN_FORMS_TREE_COPY_NAME_PREFIX")) + originalQuestion.getTitle(); 515 questionCopy.setTitle(parentForm.findUniqueQuestionTitle(copyTitle)); 516 questionCopy.setTypeId(originalQuestion.getType().getId()); 517 questionCopy.setNameForForm(questionName); 518 519 parentPage.saveChanges(); 520 521 Map<String, Object> eventParams = new HashMap<>(); 522 eventParams.put("form", parentForm); 523 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 524 525 result.put("id", questionCopy.getId()); 526 result.put("pageId", parentPage.getId()); 527 result.put("formId", parentForm.getId()); 528 result.put("type", questionCopy.getType().getId()); 529 530 return result; 531 } 532 533 /** 534 * Gets the page rules for a form question. 535 * @param id The id of the form question. 536 * @param number The question number 537 * @return The rules 538 * @throws Exception error while getting choice options 539 */ 540 @Callable 541 public Map<String, Object> getRules (String id, int number) throws Exception 542 { 543 Map<String, Object> result = new HashMap<>(); 544 545 FormQuestion question = _resolver.resolveById(id); 546 FormQuestionType type = question.getType(); 547 if (type instanceof ChoicesListQuestionType cLType) 548 { 549 ChoiceSourceType sourceType = cLType.getSourceType(question); 550 551 result.put("id", question.getId()); 552 result.put("number", String.valueOf(number)); 553 result.put("title", question.getTitle()); 554 555 List<Object> rules = new ArrayList<>(); 556 for (FormPageRule rule : question.getPageRules()) 557 { 558 String option = rule.getOption(); 559 Map<String, Object> enumParam = new HashMap<>(); 560 enumParam.put(AbstractSourceType.QUESTION_PARAM_KEY, question); 561 I18nizableText label = sourceType.getEntry(new ChoiceOption(option), enumParam); 562 563 Map<String, Object> resultRule = new HashMap<>(); 564 resultRule.put("option", option); 565 resultRule.put("optionLabel", label); 566 resultRule.put("type", rule.getType()); 567 String pageId = rule.getPageId(); 568 if (pageId != null) 569 { 570 try 571 { 572 FormPage page = _resolver.resolveById(pageId); 573 resultRule.put("page", pageId); 574 resultRule.put("pageName", page.getTitle()); 575 } 576 catch (UnknownAmetysObjectException e) 577 { 578 // Page does not exist anymore 579 } 580 } 581 582 rules.add(resultRule); 583 } 584 585 result.put("rules", rules); 586 } 587 588 return result; 589 } 590 591 /** 592 * Adds a new rule to a question. 593 * @param id The question id 594 * @param option The option 595 * @param rule The rule type 596 * @param page The page to jump or skip 597 * @return An empty map, or an error 598 */ 599 @Callable 600 public Map<String, Object> addPageRule (String id, String option, String rule, String page) 601 { 602 Map<String, Object> result = new HashMap<>(); 603 604 FormQuestion question = _resolver.resolveById(id); 605 _checkRights(question); 606 607 // Check if exists 608 if (question.hasPageRule(option)) 609 { 610 result.put("error", "already-exists"); 611 return result; 612 } 613 614 question.addPageRules(option, PageRuleType.valueOf(rule), page); 615 question.saveChanges(); 616 617 Map<String, Object> eventParams = new HashMap<>(); 618 eventParams.put("form", question.getForm()); 619 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 620 621 result.put("id", question.getId()); 622 result.put("pageId", question.getFormPage().getId()); 623 result.put("formId", question.getForm().getId()); 624 result.put("type", question.getType().getId()); 625 return result; 626 } 627 628 /** 629 * Deletes a rule to a question. 630 * @param id The question id 631 * @param option The option to delete 632 * @return An empty map 633 */ 634 @Callable 635 public Map<String, Object> deletePageRule (String id, String option) 636 { 637 FormQuestion question = _resolver.resolveById(id); 638 _checkRights(question); 639 640 question.deletePageRule(option); 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 return new HashMap<>(); 648 } 649}