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 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.SKIP_BUILTIN_CHECK) 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.SKIP_BUILTIN_CHECK) 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.SKIP_BUILTIN_CHECK) 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.SKIP_BUILTIN_CHECK) 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.SKIP_BUILTIN_CHECK) 357 public Map<String, Object> getPageProperties (String pageId) 358 { 359 try 360 { 361 FormPage page = _resolver.resolveById(pageId); 362 return getPageProperties(page, true); 363 } 364 catch (UnknownAmetysObjectException e) 365 { 366 getLogger().warn("Can't find page with id: {}. It probably has just been deleted", pageId, e); 367 Map<String, Object> infos = new HashMap<>(); 368 infos.put("id", pageId); 369 return infos; 370 } 371 } 372 373 /** 374 * Get the page's properties 375 * @param page The form page 376 * @param withRights <code>true</code> to have the rights in the properties 377 * @return The page properties 378 */ 379 public Map<String, Object> getPageProperties (FormPage page, boolean withRights) 380 { 381 Map<String, Object> infos = new HashMap<>(); 382 383 List<String> fullPath = new ArrayList<>(); 384 fullPath.add(page.getTitle()); 385 386 AmetysObject node = page.getParent(); 387 fullPath.add(0, node.getName()); 388 389 infos.put("type", "page"); 390 391 /** Use in the bus message */ 392 infos.put("id", page.getId()); 393 infos.put("title", page.getTitle()); 394 infos.put("formId", page.getForm().getId()); 395 infos.put("hasEntries", !page.getForm().getEntries().isEmpty()); 396 infos.put("hasChildren", page.getQuestions().size() > 0); 397 398 boolean isConfigured = !page.getQuestions().stream().anyMatch(q -> !q.getType().isQuestionConfigured(q)); 399 infos.put("isConfigured", isConfigured); 400 401 if (withRights) 402 { 403 infos.put("rights", _getUserRights(page)); 404 } 405 else 406 { 407 infos.put("canWrite", _formDAO.hasWriteRightOnForm(_currentUserProvider.getUser(), page)); 408 } 409 410 return infos; 411 } 412 413 /** 414 * Get user rights for the given form page 415 * @param page the form page 416 * @return the set of rights 417 */ 418 protected Set<String> _getUserRights (FormPage page) 419 { 420 UserIdentity user = _currentUserProvider.getUser(); 421 return _rightManager.getUserRights(user, page); 422 } 423 424 /** 425 * Gets the ids of the path elements of a form component, i.e. the parent ids. 426 * <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"] 427 * @param id The id of the form component 428 * @return the ids of the path elements of a form 429 */ 430 @Callable (rights = FormDAO.HANDLE_FORMS_RIGHT_ID, rightContext = FormsDirectoryRightAssignmentContext.ID, paramIndex = 0) 431 public List<String> getIdsOfPath(String id) 432 { 433 AmetysObject formComponent = _resolver.resolveById(id); 434 435 if (!(formComponent instanceof FormPage) && !(formComponent instanceof FormQuestion)) 436 { 437 throw new IllegalArgumentException("The given id is not a form component"); 438 } 439 440 List<String> pathElements = new ArrayList<>(); 441 AmetysObject current = formComponent.getParent(); 442 while (!(current instanceof Form)) 443 { 444 pathElements.add(0, current.getId()); 445 current = current.getParent(); 446 } 447 448 return pathElements; 449 } 450 451 /** 452 * Gets all pages for given parent 453 * @param formId The id of the {@link Form}, defining the context from which getting children 454 * @return all forms for given parent 455 */ 456 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 457 public List<Map<String, Object>> getChildPages(String formId) 458 { 459 Form form = _resolver.resolveById(formId); 460 _formDAO.checkHandleFormRight(form); 461 462 return form.getPages() 463 .stream() 464 .map(p -> this.getPageProperties(p, false)) 465 .toList(); 466 } 467 468 /** 469 * Get the form containing the given object. 470 * @param obj the object. 471 * @return the parent Form. 472 */ 473 protected Form getParentForm(JCRAmetysObject obj) 474 { 475 try 476 { 477 JCRAmetysObject currentAo = obj.getParent(); 478 479 while (!(currentAo instanceof Form)) 480 { 481 currentAo = currentAo.getParent(); 482 } 483 484 if (currentAo instanceof Form) 485 { 486 return (Form) currentAo; 487 } 488 } 489 catch (AmetysRepositoryException e) 490 { 491 // Ignore, just return null. 492 } 493 494 return null; 495 } 496 497 /** 498 * Determines if a page is the last of form's pages. 499 * @param id The page id 500 * @return True if the page is the last one. 501 */ 502 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 503 public boolean isLastPage (String id) 504 { 505 FormPage page = _resolver.resolveById(id); 506 _formDAO.checkHandleFormRight(page); 507 508 Form form = page.getForm(); 509 510 List<FormPage> pages = form.getPages(); 511 FormPage lastPage = pages.get(pages.size() - 1); 512 513 return id.equals(lastPage.getId()); 514 } 515 516 /** 517 * Gets the branches for a form page. 518 * @param pageId The id of the form page. 519 * @return The branches 520 */ 521 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 522 public Map<String, Object> getBranches (String pageId) 523 { 524 Map<String, Object> result = new HashMap<>(); 525 526 FormPage page = _resolver.resolveById(pageId); 527 _formDAO.checkHandleFormRight(page); 528 529 result.put("id", pageId); 530 531 List<Object> questions = new ArrayList<>(); 532 List<FormQuestion> questionsAO = page.getQuestions(); 533 int index = 1; 534 for (FormQuestion question : questionsAO) 535 { 536 if (question.getType() instanceof ChoicesListQuestionType type 537 && !type.getSourceType(question).remoteData() 538 && !question.isModifiable()) 539 { 540 try 541 { 542 questions.add(_formQuestionDAO.getRules(question.getId(), index)); 543 } 544 catch (Exception e) 545 { 546 getLogger().error("an exception occured while getting rules for question " + question.getId()); 547 } 548 } 549 index++; 550 } 551 result.put("questions", questions); 552 553 // SAX page rule 554 result.put("rule", getRule(pageId)); 555 556 return result; 557 } 558 559 /** 560 * Gets the rule for a form page. 561 * @param id The id of the form page. 562 * @return The rule, or null 563 */ 564 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 565 public Map<String, Object> getRule (String id) 566 { 567 Map<String, Object> result = new HashMap<>(); 568 569 FormPage page = _resolver.resolveById(id); 570 _formDAO.checkHandleFormRight(page); 571 572 FormPageRule rule = page.getRule(); 573 574 if (rule != null) 575 { 576 result.put("type", rule.getType().name()); 577 String pageId = rule.getPageId(); 578 if (pageId != null) 579 { 580 try 581 { 582 FormPage pageAO = _resolver.resolveById(pageId); 583 result.put("page", pageId); 584 result.put("pageName", pageAO.getTitle()); 585 } 586 catch (UnknownAmetysObjectException e) 587 { 588 // The page does not exist anymore 589 } 590 } 591 } 592 else 593 { 594 result = null; 595 } 596 597 return result; 598 } 599 600 /** 601 * Adds a a new rule to a page. 602 * @param id The id of the page 603 * @param rule The rule type 604 * @param page The page to jump or skip 605 * @return An empty map 606 */ 607 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 608 public Map<String, Object> addRule (String id, String rule, String page) 609 { 610 FormPage formPage = _resolver.resolveById(id); 611 _formDAO.checkHandleFormRight(formPage); 612 613 formPage.setRule(PageRuleType.valueOf(rule), page); 614 formPage.saveChanges(); 615 616 Map<String, Object> eventParams = new HashMap<>(); 617 eventParams.put("form", formPage.getForm()); 618 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 619 620 return new HashMap<>(); 621 } 622 623 /** 624 * Deletes a rule to a page 625 * @param id The id of the page 626 * @return An empty map 627 */ 628 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 629 public Map<String, Object> deleteRule (String id) 630 { 631 FormPage formPage = _resolver.resolveById(id); 632 _formDAO.checkHandleFormRight(formPage); 633 634 formPage.deleteRule(); 635 formPage.saveChanges(); 636 637 Map<String, Object> eventParams = new HashMap<>(); 638 eventParams.put("form", formPage.getForm()); 639 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _getCurrentUser(), eventParams)); 640 641 return new HashMap<>(); 642 } 643 644 private void _removeReferencesFromPages (String pageId, Form parent) 645 { 646 List<FormPageRule> rulesWithPageId = parent.getPages().stream() 647 .map(page -> page.getRule()) 648 .filter(rule -> rule != null && pageId.equals(rule.getPageId())) 649 .collect(Collectors.toList()); 650 651 for (FormPageRule rule : rulesWithPageId) 652 { 653 rule.remove(); 654 } 655 parent.saveChanges(); 656 } 657 658 private void _removeReferencesFromQuestions (String pageId, Form parent) 659 { 660 List<FormPageRule> rulesWithPageId = parent.getQuestions().stream() 661 .map(question -> question.getPageRules()) 662 .flatMap(List::stream) 663 .filter(rule -> rule != null && pageId.equals(rule.getPageId())) 664 .collect(Collectors.toList()); 665 666 for (FormPageRule rule : rulesWithPageId) 667 { 668 rule.remove(); 669 } 670 parent.saveChanges(); 671 } 672 673 private void _removeReferencesFromQuestionsRules (FormPage page, Form parent) 674 { 675 for (FormQuestion questionToDelete : page.getQuestions()) 676 { 677 parent.deleteQuestionsRule(questionToDelete.getId()); 678 } 679 } 680 681 /** 682 * Provides the current user. 683 * @return the user which cannot be <code>null</code>. 684 */ 685 protected UserIdentity _getCurrentUser() 686 { 687 return _currentUserProvider.getUser(); 688 } 689}