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