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