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