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