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 */ 016package org.ametys.plugins.forms.dao; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023import java.util.stream.Collectors; 024 025import javax.jcr.Node; 026import javax.jcr.RepositoryException; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.jackrabbit.util.Text; 033 034import org.ametys.core.observation.Event; 035import org.ametys.core.observation.ObservationManager; 036import org.ametys.core.right.RightManager; 037import org.ametys.core.ui.Callable; 038import org.ametys.core.user.CurrentUserProvider; 039import org.ametys.core.user.UserIdentity; 040import org.ametys.core.util.I18nUtils; 041import org.ametys.plugins.forms.FormEvents; 042import org.ametys.plugins.forms.question.types.ChoicesListQuestionType; 043import org.ametys.plugins.forms.repository.CopyFormUpdater; 044import org.ametys.plugins.forms.repository.CopyFormUpdaterExtensionPoint; 045import org.ametys.plugins.forms.repository.Form; 046import org.ametys.plugins.forms.repository.FormPage; 047import org.ametys.plugins.forms.repository.FormPageFactory; 048import org.ametys.plugins.forms.repository.FormPageRule; 049import org.ametys.plugins.forms.repository.FormPageRule.PageRuleType; 050import org.ametys.plugins.forms.repository.FormQuestion; 051import org.ametys.plugins.forms.rights.FormsDirectoryRightAssignmentContext; 052import org.ametys.plugins.repository.AmetysObject; 053import org.ametys.plugins.repository.AmetysObjectResolver; 054import org.ametys.plugins.repository.AmetysRepositoryException; 055import org.ametys.plugins.repository.UnknownAmetysObjectException; 056import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject; 057import org.ametys.plugins.repository.jcr.JCRAmetysObject; 058import org.ametys.plugins.repository.jcr.NameHelper; 059import org.ametys.runtime.i18n.I18nizableText; 060import org.ametys.runtime.plugin.component.AbstractLogEnabled; 061 062/** 063 * DAO for manipulating form pages 064 */ 065public class FormPageDAO extends AbstractLogEnabled implements Serviceable, Component 066{ 067 /** The Avalon role */ 068 public static final String ROLE = FormPageDAO.class.getName(); 069 /** Observer manager. */ 070 protected ObservationManager _observationManager; 071 /** The Ametys object resolver */ 072 protected AmetysObjectResolver _resolver; 073 /** The current user provider. */ 074 protected CurrentUserProvider _currentUserProvider; 075 /** The form question DAO */ 076 protected FormQuestionDAO _formQuestionDAO; 077 /** I18n Utils */ 078 protected I18nUtils _i18nUtils; 079 /** The form DAO */ 080 protected FormDAO _formDAO; 081 /** The right manager */ 082 protected RightManager _rightManager; 083 /** The copy form updater extension point */ 084 protected CopyFormUpdaterExtensionPoint _copyFormEP; 085 086 public void service(ServiceManager manager) throws ServiceException 087 { 088 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 089 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 090 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 091 _formQuestionDAO = (FormQuestionDAO) manager.lookup(FormQuestionDAO.ROLE); 092 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 093 _formDAO = (FormDAO) manager.lookup(FormDAO.ROLE); 094 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 095 _copyFormEP = (CopyFormUpdaterExtensionPoint) manager.lookup(CopyFormUpdaterExtensionPoint.ROLE); 096 } 097 098 /** 099 * Creates a {@link FormPage}. 100 * @param parentId The id of the parent. 101 * @param name The desired name for the new {@link FormPage} 102 * @return The id of the created form page 103 * @throws Exception if an error occurs during the form creation process 104 */ 105 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 106 public Map<String, String> createPage (String parentId, String name) throws Exception 107 { 108 Map<String, String> result = new HashMap<>(); 109 110 Form rootNode = _resolver.resolveById(parentId); 111 _formDAO.checkHandleFormRight(rootNode); 112 113 // Find unique name 114 String originalName = NameHelper.filterName(name); 115 String uniqueName = originalName; 116 int index = 1; 117 while (rootNode.hasChild(uniqueName)) 118 { 119 uniqueName = originalName + "-" + index; 120 index++; 121 } 122 123 FormPage page = rootNode.createChild(uniqueName, FormPageFactory.FORM_PAGE_NODETYPE); 124 125 page.setTitle(name + " " + index); 126 127 rootNode.saveChanges(); 128 129 Map<String, Object> eventParams = new HashMap<>(); 130 eventParams.put("form", rootNode); 131 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 132 133 134 result.put("id", page.getId()); 135 result.put("name", page.getTitle()); 136 137 return result; 138 } 139 140 /** 141 * Rename a {@link FormPage} 142 * @param id The id of the page 143 * @param newName The new name of the page 144 * @return A result map 145 */ 146 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 147 public Map<String, String> renamePage (String id, String newName) 148 { 149 Map<String, String> results = new HashMap<>(); 150 151 FormPage page = _resolver.resolveById(id); 152 _formDAO.checkHandleFormRight(page); 153 154 String legalName = Text.escapeIllegalJcrChars(newName); 155 Node node = page.getNode(); 156 try 157 { 158 page.setTitle(newName); 159 160 node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName); 161 node.getSession().save(); 162 163 Map<String, Object> eventParams = new HashMap<>(); 164 eventParams.put("form", page.getForm()); 165 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 166 167 results.put("id", id); 168 results.put("newName", legalName); 169 results.put("formId", page.getForm().getId()); 170 } 171 catch (RepositoryException e) 172 { 173 getLogger().error("Form renaming failed.", e); 174 results.put("message", "cannot-rename"); 175 } 176 177 return results; 178 } 179 180 /** 181 * Deletes a {@link FormPage}. 182 * @param id The id of the page to delete 183 * @return The id of the page 184 */ 185 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 186 public Map<String, String> deletePage (String id) 187 { 188 Map<String, String> result = new HashMap<>(); 189 190 FormPage page = _resolver.resolveById(id); 191 _formDAO.checkHandleFormRight(page); 192 193 Form parent = page.getForm(); 194 195 //remove question rules references 196 _removeReferencesFromQuestionsRules(page, parent); 197 198 page.remove(); 199 200 // Remove page rules references 201 _removeReferencesFromPages (id, parent); 202 _removeReferencesFromQuestions(id, parent); 203 parent.saveChanges(); 204 205 Map<String, Object> eventParams = new HashMap<>(); 206 eventParams.put("form", parent); 207 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 208 209 result.put("id", id); 210 result.put("formId", parent.getId()); 211 212 return result; 213 } 214 215 /** 216 * Copies and pastes a form page. 217 * @param formId The id of the form, target of the copy 218 * @param pageId The id of the page to copy 219 * @return The id of the created page 220 */ 221 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 222 public Map<String, String> copyPage(String formId, String pageId) 223 { 224 Map<String, String> result = new HashMap<>(); 225 226 Form parentForm = _resolver.resolveById(formId); 227 _formDAO.checkHandleFormRight(parentForm); 228 229 FormPage originalPage = _resolver.resolveById(pageId); 230 231 // Find unique name 232 String originalName = originalPage.getName(); 233 String name = originalName; 234 int index = 2; 235 while (parentForm.hasChild(name)) 236 { 237 name = originalName + "-" + (index++); 238 } 239 FormPage cPage = originalPage.copyTo(parentForm, name); 240 String copyTitle = _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGIN_FORMS_TREE_COPY_NAME_PREFIX")) + originalPage.getTitle(); 241 cPage.setTitle(copyTitle); 242 cPage.deleteRule(); 243 244 parentForm.saveChanges(); 245 246 for (String epId : _copyFormEP.getExtensionsIds()) 247 { 248 CopyFormUpdater copyFormUpdater = _copyFormEP.getExtension(epId); 249 copyFormUpdater.updateFormPage(originalPage, cPage); 250 } 251 252 Map<String, Object> eventParams = new HashMap<>(); 253 eventParams.put("form", parentForm); 254 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 255 256 result.put("id", cPage.getId()); 257 result.put("formId", parentForm.getId()); 258 259 return result; 260 } 261 262 /** 263 * Moves an element of the form. 264 * @param id The id of the element to move. 265 * @param oldParent The id of the element's parent. 266 * @param newParent The id of the new element's parent. 267 * @param index The index where to move. null to place the element at the end. 268 * @return A map with the ids of the element, the old parent and the new parent 269 * @throws Exception if an error occurs when moving an element of the form 270 */ 271 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 272 public Map<String, String> moveObject (String id, String oldParent, String newParent, long index) throws Exception 273 { 274 Map<String, String> result = new HashMap<>(); 275 276 JCRAmetysObject aoMoved = _resolver.resolveById(id); 277 _formDAO.checkHandleFormRight(aoMoved); 278 279 DefaultTraversableAmetysObject newParentAO = _resolver.resolveById(newParent); 280 JCRAmetysObject brother = null; 281 long size = newParentAO.getChildren().getSize(); 282 if (index != -1 && index < size) 283 { 284 brother = newParentAO.getChildAt(index); 285 } 286 else if (index >= size) 287 { 288 brother = newParentAO.getChildAt(Math.toIntExact(size) - 1); 289 } 290 Form oldForm = getParentForm(aoMoved); 291 if (oldForm != null) 292 { 293 result.put("oldFormId", oldForm.getId()); 294 } 295 296 if (oldParent.equals(newParent) && brother != null) 297 { 298 Node node = aoMoved.getNode(); 299 String name = ""; 300 try 301 { 302 name = (index == size) 303 ? null 304 : brother.getName(); 305 node.getParent().orderBefore(node.getName(), name); 306 } 307 catch (RepositoryException e) 308 { 309 throw new AmetysRepositoryException(String.format("Unable to order AmetysOject '%s' before sibling '%s'", this, name), e); 310 } 311 } 312 else 313 { 314 Node node = aoMoved.getNode(); 315 316 String name = node.getName(); 317 // Find unused name on new parent node 318 int localIndex = 2; 319 while (newParentAO.hasChild(name)) 320 { 321 name = node.getName() + "-" + localIndex++; 322 } 323 324 node.getSession().move(node.getPath(), newParentAO.getNode().getPath() + "/" + name); 325 326 if (brother != null) 327 { 328 node.getParent().orderBefore(node.getName(), brother.getName()); 329 } 330 } 331 332 if (newParentAO.needsSave()) 333 { 334 newParentAO.saveChanges(); 335 } 336 337 Form form = getParentForm(aoMoved); 338 if (form != null) 339 { 340 result.put("newFormId", form.getId()); 341 342 Map<String, Object> eventParams = new HashMap<>(); 343 eventParams.put("form", form); 344 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 345 } 346 347 result.put("id", id); 348 349 if (aoMoved instanceof FormPage) 350 { 351 result.put("type", "page"); 352 } 353 else if (aoMoved instanceof FormQuestion) 354 { 355 result.put("type", "question"); 356 result.put("questionType", ((FormQuestion) aoMoved).getType().getId()); 357 } 358 359 result.put("newParentId", newParentAO.getId()); 360 result.put("oldParentId", oldParent); 361 362 return result; 363 } 364 365 /** 366 * Get the page's properties 367 * @param pageId The form page's id 368 * @return The page properties 369 */ 370 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 371 public Map<String, Object> getPageProperties (String pageId) 372 { 373 try 374 { 375 FormPage page = _resolver.resolveById(pageId); 376 return getPageProperties(page, true); 377 } 378 catch (UnknownAmetysObjectException e) 379 { 380 getLogger().warn("Can't find page with id: {}. It probably has just been deleted", pageId, e); 381 Map<String, Object> infos = new HashMap<>(); 382 infos.put("id", pageId); 383 return infos; 384 } 385 } 386 387 /** 388 * Get the page's properties 389 * @param page The form page 390 * @param withRights <code>true</code> to have the rights in the properties 391 * @return The page properties 392 */ 393 public Map<String, Object> getPageProperties (FormPage page, boolean withRights) 394 { 395 Map<String, Object> infos = new HashMap<>(); 396 397 List<String> fullPath = new ArrayList<>(); 398 fullPath.add(page.getTitle()); 399 400 AmetysObject node = page.getParent(); 401 fullPath.add(0, node.getName()); 402 403 infos.put("type", "page"); 404 405 /** Use in the bus message */ 406 infos.put("id", page.getId()); 407 infos.put("title", page.getTitle()); 408 infos.put("formId", page.getForm().getId()); 409 infos.put("hasEntries", !page.getForm().getEntries().isEmpty()); 410 infos.put("hasChildren", page.getQuestions().size() > 0); 411 412 boolean isConfigured = !page.getQuestions().stream().anyMatch(q -> !q.getType().isQuestionConfigured(q)); 413 infos.put("isConfigured", isConfigured); 414 415 if (withRights) 416 { 417 infos.put("rights", _getUserRights(page)); 418 } 419 else 420 { 421 infos.put("canWrite", _formDAO.hasWriteRightOnForm(_currentUserProvider.getUser(), page)); 422 } 423 424 return infos; 425 } 426 427 /** 428 * Get user rights for the given form page 429 * @param page the form page 430 * @return the set of rights 431 */ 432 protected Set<String> _getUserRights (FormPage page) 433 { 434 UserIdentity user = _currentUserProvider.getUser(); 435 return _rightManager.getUserRights(user, page); 436 } 437 438 /** 439 * Gets the ids of the path elements of a form component, i.e. the parent ids. 440 * <br>For instance, if the page path is 'a/b/c', then the result list will be ["id-of-a", "id-of-b", "id-of-c"] 441 * @param id The id of the form component 442 * @return the ids of the path elements of a form 443 */ 444 @Callable (rights = FormDAO.HANDLE_FORMS_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 0) 445 public List<String> getIdsOfPath(String id) 446 { 447 AmetysObject formComponent = _resolver.resolveById(id); 448 449 if (!(formComponent instanceof FormPage) && !(formComponent instanceof FormQuestion)) 450 { 451 throw new IllegalArgumentException("The given id is not a form component"); 452 } 453 454 List<String> pathElements = new ArrayList<>(); 455 AmetysObject current = formComponent.getParent(); 456 while (!(current instanceof Form)) 457 { 458 pathElements.add(0, current.getId()); 459 current = current.getParent(); 460 } 461 462 return pathElements; 463 } 464 465 /** 466 * Gets all pages for given parent 467 * @param formId The id of the {@link Form}, defining the context from which getting children 468 * @return all forms for given parent 469 */ 470 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 471 public List<Map<String, Object>> getChildPages(String formId) 472 { 473 Form form = _resolver.resolveById(formId); 474 _formDAO.checkHandleFormRight(form); 475 476 return form.getPages() 477 .stream() 478 .map(p -> this.getPageProperties(p, false)) 479 .toList(); 480 } 481 482 /** 483 * Get the form containing the given object. 484 * @param obj the object. 485 * @return the parent Form. 486 */ 487 protected Form getParentForm(JCRAmetysObject obj) 488 { 489 try 490 { 491 JCRAmetysObject currentAo = obj.getParent(); 492 493 while (!(currentAo instanceof Form)) 494 { 495 currentAo = currentAo.getParent(); 496 } 497 498 if (currentAo instanceof Form) 499 { 500 return (Form) currentAo; 501 } 502 } 503 catch (AmetysRepositoryException e) 504 { 505 // Ignore, just return null. 506 } 507 508 return null; 509 } 510 511 /** 512 * Determines if a page is the last of form's pages. 513 * @param id The page id 514 * @return True if the page is the last one. 515 */ 516 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 517 public boolean isLastPage (String id) 518 { 519 FormPage page = _resolver.resolveById(id); 520 _formDAO.checkHandleFormRight(page); 521 522 Form form = page.getForm(); 523 524 List<FormPage> pages = form.getPages(); 525 FormPage lastPage = pages.get(pages.size() - 1); 526 527 return id.equals(lastPage.getId()); 528 } 529 530 /** 531 * Gets the branches for a form page. 532 * @param pageId The id of the form page. 533 * @return The branches 534 */ 535 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 536 public Map<String, Object> getBranches (String pageId) 537 { 538 Map<String, Object> result = new HashMap<>(); 539 540 FormPage page = _resolver.resolveById(pageId); 541 _formDAO.checkHandleFormRight(page); 542 543 result.put("id", pageId); 544 545 List<Object> questions = new ArrayList<>(); 546 List<FormQuestion> questionsAO = page.getQuestions(); 547 int index = 1; 548 for (FormQuestion question : questionsAO) 549 { 550 if (question.getType() instanceof ChoicesListQuestionType type 551 && !type.getSourceType(question).remoteData() 552 && !question.isModifiable()) 553 { 554 try 555 { 556 questions.add(_formQuestionDAO.getRules(question.getId(), index)); 557 } 558 catch (Exception e) 559 { 560 getLogger().error("an exception occured while getting rules for question " + question.getId()); 561 } 562 } 563 index++; 564 } 565 result.put("questions", questions); 566 567 // SAX page rule 568 result.put("rule", getRule(pageId)); 569 570 return result; 571 } 572 573 /** 574 * Gets the rule for a form page. 575 * @param id The id of the form page. 576 * @return The rule, or null 577 */ 578 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 579 public Map<String, Object> getRule (String id) 580 { 581 Map<String, Object> result = new HashMap<>(); 582 583 FormPage page = _resolver.resolveById(id); 584 _formDAO.checkHandleFormRight(page); 585 586 FormPageRule rule = page.getRule(); 587 588 if (rule != null) 589 { 590 result.put("type", rule.getType().name()); 591 String pageId = rule.getPageId(); 592 if (pageId != null) 593 { 594 try 595 { 596 FormPage pageAO = _resolver.resolveById(pageId); 597 result.put("page", pageId); 598 result.put("pageName", pageAO.getTitle()); 599 } 600 catch (UnknownAmetysObjectException e) 601 { 602 // The page does not exist anymore 603 } 604 } 605 } 606 else 607 { 608 result = null; 609 } 610 611 return result; 612 } 613 614 /** 615 * Adds a a new rule to a page. 616 * @param id The id of the page 617 * @param rule The rule type 618 * @param page The page to jump or skip 619 * @return An empty map 620 */ 621 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 622 public Map<String, Object> addRule (String id, String rule, String page) 623 { 624 FormPage formPage = _resolver.resolveById(id); 625 _formDAO.checkHandleFormRight(formPage); 626 627 formPage.setRule(PageRuleType.valueOf(rule), page); 628 formPage.saveChanges(); 629 630 Map<String, Object> eventParams = new HashMap<>(); 631 eventParams.put("form", formPage.getForm()); 632 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 633 634 return new HashMap<>(); 635 } 636 637 /** 638 * Deletes a rule to a page 639 * @param id The id of the page 640 * @return An empty map 641 */ 642 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 643 public Map<String, Object> deleteRule (String id) 644 { 645 FormPage formPage = _resolver.resolveById(id); 646 _formDAO.checkHandleFormRight(formPage); 647 648 formPage.deleteRule(); 649 formPage.saveChanges(); 650 651 Map<String, Object> eventParams = new HashMap<>(); 652 eventParams.put("form", formPage.getForm()); 653 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 654 655 return new HashMap<>(); 656 } 657 658 private void _removeReferencesFromPages (String pageId, Form parent) 659 { 660 List<FormPageRule> rulesWithPageId = parent.getPages().stream() 661 .map(page -> page.getRule()) 662 .filter(rule -> rule != null && pageId.equals(rule.getPageId())) 663 .collect(Collectors.toList()); 664 665 for (FormPageRule rule : rulesWithPageId) 666 { 667 rule.remove(); 668 } 669 parent.saveChanges(); 670 } 671 672 private void _removeReferencesFromQuestions (String pageId, Form parent) 673 { 674 List<FormPageRule> rulesWithPageId = parent.getQuestions().stream() 675 .map(question -> question.getPageRules()) 676 .flatMap(List::stream) 677 .filter(rule -> rule != null && pageId.equals(rule.getPageId())) 678 .collect(Collectors.toList()); 679 680 for (FormPageRule rule : rulesWithPageId) 681 { 682 rule.remove(); 683 } 684 parent.saveChanges(); 685 } 686 687 private void _removeReferencesFromQuestionsRules (FormPage page, Form parent) 688 { 689 for (FormQuestion questionToDelete : page.getQuestions()) 690 { 691 parent.deleteQuestionsRule(questionToDelete.getId()); 692 } 693 } 694 695 /** 696 * Provides the current user. 697 * @return the user which cannot be <code>null</code>. 698 */ 699 protected UserIdentity _getCurrentUser() 700 { 701 return _currentUserProvider.getUser(); 702 } 703}