001/* 002 * Copyright 2011 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.odfweb.repository; 017 018import java.util.ArrayList; 019import java.util.HashSet; 020import java.util.List; 021import java.util.Optional; 022import java.util.Set; 023 024import org.apache.avalon.framework.activity.Initializable; 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.avalon.framework.service.Serviceable; 029import org.apache.commons.lang3.StringUtils; 030 031import org.ametys.cms.repository.Content; 032import org.ametys.core.cache.AbstractCacheManager; 033import org.ametys.core.cache.Cache; 034import org.ametys.odf.ODFHelper; 035import org.ametys.odf.ProgramItem; 036import org.ametys.odf.course.Course; 037import org.ametys.odf.courselist.CourseList; 038import org.ametys.odf.program.AbstractProgram; 039import org.ametys.odf.program.Program; 040import org.ametys.odf.program.ProgramPart; 041import org.ametys.odf.program.SubProgram; 042import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 043import org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint; 044import org.ametys.plugins.repository.AmetysObjectResolver; 045import org.ametys.plugins.repository.UnknownAmetysObjectException; 046import org.ametys.runtime.config.Config; 047import org.ametys.runtime.i18n.I18nizableText; 048import org.ametys.runtime.plugin.component.AbstractLogEnabled; 049import org.ametys.web.repository.page.Page; 050 051/** 052 * Resolves an ODF page path from the associated ODF content. 053 */ 054public class OdfPageResolver extends AbstractLogEnabled implements Component, Serviceable, Initializable 055{ 056 /** The avalon role. */ 057 public static final String ROLE = OdfPageResolver.class.getName(); 058 059 private static final String __PATH_IN_SITEMAP_CACHE = OdfPageResolver.class.getName() + "$pathInSitemap"; 060 private static final String __RESOLVED_PATH_CACHE = OdfPageResolver.class.getName() + "$resolvedPath"; 061 private static final String __ODF_ROOT_PROGRAM_CACHE = OdfPageResolver.class.getName() + "$rootProgram"; 062 063 /** The ametys object resolver. */ 064 protected AmetysObjectResolver _ametysResolver; 065 /** The odf page handler */ 066 protected OdfPageHandler _odfPageHandler; 067 /** ODF helper */ 068 protected ODFHelper _odfHelper; 069 /** The cache manager */ 070 protected AbstractCacheManager _cacheManager; 071 /** The course page factory */ 072 protected CoursePageFactory _coursePageFactory; 073 /** The program page factory */ 074 protected ProgramPageFactory _programPageFactory; 075 076 public void service(ServiceManager serviceManager) throws ServiceException 077 { 078 _ametysResolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 079 _odfPageHandler = (OdfPageHandler) serviceManager.lookup(OdfPageHandler.ROLE); 080 _odfHelper = (ODFHelper) serviceManager.lookup(ODFHelper.ROLE); 081 _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE); 082 AmetysObjectFactoryExtensionPoint ametysObjectFactoryEP = (AmetysObjectFactoryExtensionPoint) serviceManager.lookup(AmetysObjectFactoryExtensionPoint.ROLE); 083 _coursePageFactory = (CoursePageFactory) ametysObjectFactoryEP.getExtension(CoursePageFactory.class.getName()); 084 _programPageFactory = (ProgramPageFactory) ametysObjectFactoryEP.getExtension(ProgramPageFactory.class.getName()); 085 } 086 087 public void initialize() throws Exception 088 { 089 _cacheManager.createRequestCache(__PATH_IN_SITEMAP_CACHE, 090 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PATH_IN_SITEMAP_LABEL"), 091 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PATH_IN_SITEMAP_DESCRIPTION"), 092 false); 093 094 _cacheManager.createRequestCache(__RESOLVED_PATH_CACHE, 095 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_RESOLVED_PATH_LABEL"), 096 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_RESOLVED_PATH_DESCRIPTION"), 097 false); 098 099 _cacheManager.createRequestCache(__ODF_ROOT_PROGRAM_CACHE, 100 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ROOT_PROGRAM_LABEL"), 101 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ROOT_PROGRAM_DESCRIPTION"), 102 false); 103 } 104 105 /** 106 * Get all referencing pages for this program item, in all sites and all sitemaps 107 * @param programItem The program item 108 * @return the referencing pages 109 */ 110 public Set<Page> getReferencingPages(ProgramItem programItem) 111 { 112 return getReferencingPages(programItem, null, ((Content) programItem).getLanguage()); 113 } 114 115 /** 116 * Get all referencing pages for this program item 117 * @param programItem The program item 118 * @param siteName The site name. Can be null to search on all sites 119 * @param lang The sitemap language. Can be null to search on all sitemaps 120 * @return the referencing pages 121 */ 122 public Set<Page> getReferencingPages(ProgramItem programItem, String siteName, String lang) 123 { 124 Set<Page> refPages = new HashSet<>(); 125 126 Set<Page> odfRootPages = _odfPageHandler.getOdfRootPages(siteName, lang); 127 128 for (Page rootPage : odfRootPages) 129 { 130 // Ignore ODF root pages that does not belong to the same catalog as the program item 131 if (_odfPageHandler.getCatalog(rootPage).equals(programItem.getCatalog())) 132 { 133 if (programItem instanceof Program program) 134 { 135 ProgramPage programPage = getProgramPage(rootPage, program); 136 if (programPage != null) 137 { 138 refPages.add(programPage); 139 } 140 } 141 else if (programItem instanceof SubProgram subProgram) 142 { 143 Set<Program> parentPrograms = _odfHelper.getParentPrograms(programItem); 144 for (Program parentProgram : parentPrograms) 145 { 146 ProgramPage subProgramPage = getSubProgramPage(rootPage, subProgram, parentProgram); 147 if (subProgramPage != null) 148 { 149 refPages.add(subProgramPage); 150 } 151 } 152 } 153 else if (programItem instanceof Course course) 154 { 155 List<CourseList> parentCourseLists = course.getParentCourseLists(); 156 for (CourseList courseList : parentCourseLists) 157 { 158 List<Course> parentCourses = courseList.getParentCourses(); 159 for (Course parentCourse : parentCourses) 160 { 161 CoursePage coursePage = getCoursePage(rootPage, course, parentCourse); 162 if (coursePage != null) 163 { 164 refPages.add(coursePage); 165 } 166 } 167 168 List<AbstractProgram> parentAbstractPrograms = getNearestAncestorAbstractPrograms(courseList); 169 for (AbstractProgram parentAbstractProgram : parentAbstractPrograms) 170 { 171 CoursePage coursePage = getCoursePage(rootPage, course, parentAbstractProgram); 172 if (coursePage != null) 173 { 174 refPages.add(coursePage); 175 } 176 } 177 } 178 } 179 } 180 } 181 182 return refPages; 183 } 184 185 /** 186 * Return the program page 187 * @param program the program 188 * @return the page program or null 189 */ 190 public ProgramPage getProgramPage(Program program) 191 { 192 return getProgramPage(program, null); 193 } 194 195 /** 196 * Return the program page 197 * @param program the program 198 * @param siteName The current site name. If the no ODF root page is present in this site, the default ODF site will be used instead. 199 * @return the page program or null 200 */ 201 public ProgramPage getProgramPage(Program program, String siteName) 202 { 203 Page odfRootPage = getOdfRootPage(siteName, program.getLanguage(), program.getCatalog()); 204 205 if (odfRootPage == null) 206 { 207 return null; 208 } 209 210 return getProgramPage(odfRootPage, program); 211 } 212 213 /** 214 * Return the program page 215 * @param odfRootPage the odf root page 216 * @param program the program 217 * @return the page program or null 218 */ 219 public ProgramPage getProgramPage (Page odfRootPage, Program program) 220 { 221 // E.g: program://_root?rootId=xxxx&programId=xxxx 222 String pageId = "program://_root?rootId=" + odfRootPage.getId() + "&programId=" + program.getId(); 223 try 224 { 225 return _ametysResolver.resolveById(pageId); 226 } 227 catch (UnknownAmetysObjectException e) 228 { 229 return null; 230 } 231 } 232 233 /** 234 * Return the subprogram page 235 * @param subProgram the subprogram 236 * @param parentProgram The parent program 237 * @param siteName The current site name. If the no ODF root page is present in this site, the default ODF site will be used instead. 238 * @return the subprogram page or null 239 */ 240 public ProgramPage getSubProgramPage(SubProgram subProgram, AbstractProgram parentProgram, String siteName) 241 { 242 Page odfRootPage = getOdfRootPage(siteName, subProgram.getLanguage(), subProgram.getCatalog()); 243 244 if (odfRootPage == null) 245 { 246 return null; 247 } 248 249 return getSubProgramPage(odfRootPage, subProgram, parentProgram); 250 } 251 252 /** 253 * Return the subprogram page 254 * @param odfRootPage the odf root page 255 * @param subProgram the subprogram 256 * @param parentAbstractProgram The parent program or subprogram 257 * @return the subprogram page or null 258 */ 259 public ProgramPage getSubProgramPage (Page odfRootPage, SubProgram subProgram, AbstractProgram parentAbstractProgram) 260 { 261 try 262 { 263 // Get first parent program matching an existing program page 264 Program parentProgram = getRootProgram(odfRootPage, subProgram, parentAbstractProgram); 265 if (parentProgram == null) 266 { 267 // No program page 268 return null; 269 } 270 271 AbstractProgram nearestParentAbstractProgram = Optional.ofNullable(parentAbstractProgram).orElse(parentProgram); 272 AbstractProgram parent = getNearestAncestorAbstractProgram(subProgram, nearestParentAbstractProgram); 273 if (parent == null) 274 { 275 return null; // no page 276 } 277 278 String path = getPathInProgram(parent, parentProgram); 279 if (path == null) 280 { 281 // Subprogram is not part of the selected parent program 282 return null; 283 } 284 return _programPageFactory.createProgramPage(odfRootPage, subProgram, path, parentProgram, null); 285 } 286 catch (UnknownAmetysObjectException e) 287 { 288 return null; 289 } 290 } 291 292 /** 293 * Returns the subprogram page 294 * @param odfRootPage the odf root page 295 * @param subProgram the subprogram 296 * @param path a full or partial path of subprogram 297 * @param checkHierarchy set to true to check that the given path is a valid hierarchy 298 * @return the subprogram page or null if not found 299 */ 300 public ProgramPage getSubProgramPage(Page odfRootPage, SubProgram subProgram, List<String> path, boolean checkHierarchy) 301 { 302 // Possible paths are : 303 // [subprogramContent://UUID, subprogramContent://UUID, subprogramContent://UUID] 304 // [programContent://UUID, subprogramContent://UUID, subprogramContent://UUID] 305 306 try 307 { 308 309 ProgramItem lastParent = _ametysResolver.resolveById(path.get(0)); 310 Program parentProgram = getRootProgram(odfRootPage, lastParent, null); 311 if (parentProgram == null) 312 { 313 // No page 314 return null; 315 } 316 317 List<String> reversedPath = path.reversed(); 318 if (checkHierarchy) 319 { 320 ProgramItem nextProgramItem = _ametysResolver.resolveById(path.getLast()); 321 if (!_checkHierarchy(subProgram, nextProgramItem)) 322 { 323 // Parent item in path is not part of program item's parents 324 getLogger().warn(reversedPath + " is not valid hierarchy"); 325 throw new UnknownAmetysObjectException(reversedPath + " is not valid hierarchy"); 326 } 327 } 328 329 String pagePath = _resolvePagePath(reversedPath, checkHierarchy); 330 if (pagePath == null) 331 { 332 // No page 333 return null; 334 } 335 336 return _programPageFactory.createProgramPage(odfRootPage, subProgram, pagePath, parentProgram, null); 337 } 338 catch (UnknownAmetysObjectException e) 339 { 340 return null; 341 } 342 } 343 344 /** 345 * Return the course page 346 * @param course the course 347 * @param parentProgram the parent program or subprogram. Can be null. 348 * @param siteName The current site name. If the no ODF root page is present in this site, the default ODF site will be used instead. 349 * @return the course page or null if not found 350 */ 351 public CoursePage getCoursePage(Course course, AbstractProgram parentProgram, String siteName) 352 { 353 String catalog = course.getCatalog(); 354 Page odfRootPage = getOdfRootPage(siteName, course.getLanguage(), catalog); 355 356 if (odfRootPage == null) 357 { 358 return null; 359 } 360 361 return getCoursePage(odfRootPage, course, parentProgram); 362 } 363 364 /** 365 * Return the course page 366 * @param odfRootPage the odf root page 367 * @param course the course 368 * @param parentAbstractProgram the parent program or subprogram. Can be null. 369 * @return the course page or null if not found 370 */ 371 public CoursePage getCoursePage (Page odfRootPage, Course course, AbstractProgram parentAbstractProgram) 372 { 373 try 374 { 375 // Get first parent program matching an existing program page 376 Program parentProgram = getRootProgram(odfRootPage, course, parentAbstractProgram); 377 if (parentProgram == null) 378 { 379 // No program page 380 return null; 381 } 382 383 AbstractProgram nearestParentAbstractProgram = Optional.ofNullable(parentAbstractProgram).orElse(parentProgram); 384 385 ProgramItem parent = null; 386 387 Course parentCourse = getNearestAncestorCourse(course, nearestParentAbstractProgram); 388 if (parentCourse != null) 389 { 390 parent = parentCourse; 391 } 392 else 393 { 394 parent = getNearestAncestorAbstractProgram(course, nearestParentAbstractProgram); 395 } 396 397 if (parent == null) 398 { 399 return null; // no page 400 } 401 402 String path = getPathInProgram(parent, parentProgram); 403 if (path == null) 404 { 405 // Course is not part of the selected parent program 406 return null; 407 } 408 409 return _coursePageFactory.createCoursePage(odfRootPage, course, parentProgram, path, null); 410 } 411 catch (UnknownAmetysObjectException e) 412 { 413 return null; 414 } 415 } 416 417 /** 418 * Return the course page 419 * @param course the course 420 * @param parentCourse the parent course. Can NOT be null. 421 * @param siteName The current site name. If the no ODF root page is present in this site, the default ODF site will be used instead. 422 * @return the course page or null if not found 423 */ 424 public CoursePage getCoursePage (Course course, Course parentCourse, String siteName) 425 { 426 String catalog = course.getCatalog(); 427 Page odfRootPage = getOdfRootPage(siteName, course.getLanguage(), catalog); 428 429 if (odfRootPage == null) 430 { 431 return null; 432 } 433 434 return getCoursePage(odfRootPage, course, parentCourse); 435 } 436 437 /** 438 * Return the course page 439 * @param odfRootPage the odf root page 440 * @param course the course 441 * @param parentCourse the parent course. Can NOT be null. 442 * @return the course page or null if not found 443 */ 444 public CoursePage getCoursePage (Page odfRootPage, Course course, Course parentCourse) 445 { 446 try 447 { 448 // Get first parent program matching an existing program page 449 Program parentProgram = getRootProgram(odfRootPage, parentCourse, null); 450 if (parentProgram == null) 451 { 452 // No page 453 return null; 454 } 455 456 String path = getPathInProgram(parentCourse, parentProgram); 457 if (path == null) 458 { 459 // Course is not part of the selected parent program 460 return null; 461 } 462 return _coursePageFactory.createCoursePage(odfRootPage, course, parentProgram, path, null); 463 } 464 catch (UnknownAmetysObjectException e) 465 { 466 return null; 467 } 468 } 469 470 /** 471 * Returns the course page in given ODF root page, 472 * @param odfRootPage the odf root page 473 * @param course the course 474 * @param path a (partial) education path or a (partial) sitemap path. Be careful, assume that the given path correspond to a valid path in ODF root. Use {@link #getCoursePage(Page, Course, List, boolean)} with true if not sure. 475 * @return the course page or null if not found 476 */ 477 public CoursePage getCoursePage(Page odfRootPage, Course course, List<String> path) 478 { 479 return getCoursePage(odfRootPage, course, path, false); 480 } 481 482 /** 483 * Returns the course page in given ODF root page, 484 * @param odfRootPage the odf root page 485 * @param course the course 486 * @param path a (partial) education path or a (partial) sitemap path 487 * @param checkHierarchy set to true to check that the given path correspond to a valid hierarchy 488 * @return the course page or null if not found 489 */ 490 public CoursePage getCoursePage(Page odfRootPage, Course course, List<String> path, boolean checkHierarchy) 491 { 492 // Possible paths are : 493 // [(sub)programContent://UUID, container://UUID, courseContent://UUID1, courseContent://UUID2] 494 // [courseContent://UUID1, courseContent://UUID2, ..., courseContent://UUID3] 495 // [subprogramContent://UUID2, ..., (sub)programContent://UUID3, courseContent://UUID1] 496 497 try 498 { 499 // Get first parent program matching an existing program page 500 ProgramItem lastParent = _ametysResolver.resolveById(path.get(0)); 501 Program rootProgram = getRootProgram(odfRootPage, lastParent, null); 502 if (rootProgram == null) 503 { 504 // No page 505 return null; 506 } 507 508 List<String> reversedPath = path.reversed(); 509 if (checkHierarchy) 510 { 511 ProgramItem nextProgramItem = _ametysResolver.resolveById(path.getLast()); 512 if (!_checkHierarchy(course, nextProgramItem)) 513 { 514 // Parent item in path is not part of program item's parents 515 getLogger().warn(reversedPath + " is not valid hierarchy"); 516 throw new UnknownAmetysObjectException(reversedPath + " is not valid hierarchy"); 517 } 518 } 519 520 String pagePath = _resolvePagePath(reversedPath, checkHierarchy); 521 if (pagePath == null) 522 { 523 // No page 524 return null; 525 } 526 527 return _coursePageFactory.createCoursePage(odfRootPage, course, rootProgram, pagePath, null); 528 } 529 catch (UnknownAmetysObjectException e) 530 { 531 return null; 532 } 533 } 534 535 /** 536 * Determines if a program page exists 537 * @param odfRootPage the ODF root page 538 * @param program the program page 539 * @return true if program page existes, false otherwise 540 */ 541 public boolean isProgramPageExist(Page odfRootPage, Program program) 542 { 543 if (program == null || !_odfPageHandler.isValidRestriction(odfRootPage, program)) 544 { 545 // No program page 546 return false; 547 } 548 549 String levelsPath = _odfPageHandler.computeLevelsPath(odfRootPage, program); 550 if (levelsPath == null) 551 { 552 // The current program has no valid attributes for the levels selected in the ODF root 553 return false; 554 } 555 556 return true; 557 } 558 559 /** 560 * Get the ODF root page, either in the given site if it exists, or in the default ODF site. 561 * @param siteName the desired site name. 562 * @param language the sitemap language to search in. 563 * @param catalog The ODF catalog 564 * @return the ODF root page, either in the given site if it exists, or in the default ODF site. 565 */ 566 public Page getOdfRootPage(String siteName, String language, String catalog) 567 { 568 Page odfRootPage = null; 569 570 if (StringUtils.isNotEmpty(siteName)) 571 { 572 odfRootPage = _odfPageHandler.getOdfRootPage(siteName, language, catalog); 573 } 574 575 if (odfRootPage == null) 576 { 577 String odfSiteName = Config.getInstance().getValue("odf.web.site.name"); 578 odfRootPage = _odfPageHandler.getOdfRootPage(odfSiteName, language, catalog); 579 } 580 581 return odfRootPage; 582 } 583 584 private String _resolvePagePath(List<String> reversedPath, boolean checkHierarchy) 585 { 586 // Possible reversed paths are : 587 // [courseContent://UUID1, courseContent://UUID2, ..., courseContent://UUID3, (sub)programContent://UUID] 588 // [courseContent://UUID1, courseContent://UUID2, ..., courseContent://UUID3] 589 // [courseContent://UUID1, subprogramContent://UUID2, ..., (sub)programContent://UUID3] 590 591 Cache<String, String> cache = _cacheManager.get(__RESOLVED_PATH_CACHE); 592 return cache.get(StringUtils.join(reversedPath, ";"), item -> { 593 String pagePath = null; 594 595 try 596 { 597 if (reversedPath.size() == 1) 598 { 599 ProgramItem programItem = _ametysResolver.resolveById(reversedPath.get(0)); 600 pagePath = getPathInProgram(programItem, null); 601 } 602 else 603 { 604 String parentPath = _resolvePagePath(reversedPath.subList(1, reversedPath.size()), checkHierarchy); 605 if (parentPath != null) 606 { 607 ProgramItem programItem = _ametysResolver.resolveById(reversedPath.get(0)); 608 if (checkHierarchy) 609 { 610 // Get next parent given in path 611 ProgramItem nextProgramItem = _ametysResolver.resolveById(reversedPath.get(1)); 612 // Check that next parent is a parent item 613 if (!_checkHierarchy(programItem, nextProgramItem)) 614 { 615 // Parent item in path is not part of program item's parents 616 getLogger().warn(reversedPath + " is not valid hierarchy"); 617 throw new UnknownAmetysObjectException(reversedPath + " is not valid hierarchy"); 618 } 619 } 620 if (programItem instanceof AbstractProgram || programItem instanceof Course) 621 { 622 parentPath += '/' + _odfPageHandler.getPageName(programItem); 623 } 624 625 pagePath = parentPath; 626 } 627 } 628 } 629 catch (UnknownAmetysObjectException e) 630 { 631 // Nothing 632 } 633 634 cache.put(StringUtils.join(reversedPath, ";"), pagePath); 635 return pagePath; 636 }); 637 } 638 639 private boolean _checkHierarchy(ProgramItem childProgramItem, ProgramItem nextParentProgramItem) 640 { 641 List<ProgramItem> parentProgramItems = _odfHelper.getParentProgramItems(childProgramItem); 642 if (parentProgramItems.contains(nextParentProgramItem)) 643 { 644 return true; 645 } 646 647 for (ProgramItem parentProgramItem : parentProgramItems) 648 { 649 if (!(parentProgramItem instanceof AbstractProgram) && !(parentProgramItem instanceof Course) && _checkHierarchy(parentProgramItem, nextParentProgramItem)) 650 { 651 return true; 652 } 653 } 654 655 return false; 656 } 657 658 /** 659 * Get the path in sitemap of a ODF content into a {@link Program} or {@link SubProgram}<br> 660 * Be careful, this is the path in sitemap, to get the path of a item into a Program, use {@link ODFHelper#getPathInProgram} instead. 661 * @param programItem The program item 662 * @param parentProgram The parent root (sub)program. Can be null. 663 * @return the path in sitemap from the parent program or null if no found ODF path for those program item and parent program 664 */ 665 public String getPathInProgram (ProgramItem programItem, AbstractProgram parentProgram) 666 { 667 Cache<PathInProgramCacheKey, String> cache = _cacheManager.get(__PATH_IN_SITEMAP_CACHE); 668 669 return cache.get(PathInProgramCacheKey.of(programItem.getId(), parentProgram != null ? parentProgram.getId() : "__NOPARENT"), item -> { 670 671 if (programItem instanceof Program || programItem.equals(parentProgram)) 672 { 673 // The program item is already the program it self 674 return _odfPageHandler.getPageName(programItem); 675 } 676 677 List<String> paths = new ArrayList<>(); 678 679 // Add the parent path in program if exists 680 ProgramItem parent = _odfHelper.getParentProgramItem(programItem, parentProgram); 681 if (parent != null) 682 { 683 String parentPath = getPathInProgram(parent, parentProgram); 684 if (parentPath != null) 685 { 686 paths.add(parentPath); 687 } 688 } 689 690 // Add the current page name (if it is an item with a page (only course, subprogram and program) 691 if (programItem instanceof AbstractProgram || programItem instanceof Course) 692 { 693 paths.add(_odfPageHandler.getPageName(programItem)); 694 } 695 696 // If the path is empty, return null 697 return paths.isEmpty() ? null : StringUtils.join(paths, "/"); 698 }); 699 } 700 701 private static class RootProgramCacheKey extends AbstractCacheKey 702 { 703 protected RootProgramCacheKey(Page odfRootPage, String programItemId, String parentProgramId) 704 { 705 super(odfRootPage, programItemId, parentProgramId); 706 } 707 708 public static RootProgramCacheKey of(Page odfRootPage, String programItemId, String parentProgramId) 709 { 710 return new RootProgramCacheKey(odfRootPage, programItemId, parentProgramId); 711 } 712 } 713 714 private static class PathInProgramCacheKey extends AbstractCacheKey 715 { 716 protected PathInProgramCacheKey(String programItemId, String parentProgramId) 717 { 718 super(programItemId, parentProgramId); 719 } 720 721 public static PathInProgramCacheKey of(String programItemId, String parentProgramId) 722 { 723 return new PathInProgramCacheKey(programItemId, parentProgramId); 724 } 725 } 726 727 /** 728 * Returns the first {@link Program} ancestor matching an existing {@link ProgramPage} in given ODF root page, ensuring that the given parent content 'parentProgram' is in the hierarchy (if not null)<br> 729 * If 'parentProgram' is null, the first {@link Program} ancestor will be returned regardless of parent hierarchy.<br> 730 * If 'parentProgram' is a {@link SubProgram}, the first {@link Program} ancestor from this {@link SubProgram} will be returned regardless of parent hierarchy 731 * @param odfRootPage The ODf root page. Cannot be null. 732 * @param programItem a {@link ProgramItem} 733 * @param parentAbstractProgram The parent program or subprogram. Can be null. 734 * @return the parent {@link Program} into this (sub)program that matchs an existing program page, or null if not found 735 */ 736 public Program getRootProgram(Page odfRootPage, ProgramItem programItem, AbstractProgram parentAbstractProgram) 737 { 738 Cache<RootProgramCacheKey, Program> rootCache = _cacheManager.get(__ODF_ROOT_PROGRAM_CACHE); 739 740 return rootCache.get(RootProgramCacheKey.of(odfRootPage, programItem.getId(), parentAbstractProgram != null ? parentAbstractProgram.getId() : "__NOPARENT"), k -> { 741 // Get all parent programs 742 Set<Program> parentPrograms = _getParentPrograms(programItem, parentAbstractProgram); 743 744 // Get first parent program matching an existing program page 745 Optional<Program> parentProgram = parentPrograms.stream() 746 .filter(p -> isProgramPageExist(odfRootPage, p)) 747 .findFirst(); 748 749 return parentProgram.orElse(null); 750 }); 751 } 752 753 /** 754 * Returns all parent {@link Program} ancestors, ensuring that the given parent content 'parentProgram' is in the hierarchy, if not null.<br> 755 * If 'parentProgram' is null, the all {@link Program} ancestors will be returned regardless of parent hierarchy.<br> 756 * If 'parentProgram' is a {@link SubProgram}, the {@link Program} ancestors from this {@link SubProgram} will be returned regardless of parent hierarchy 757 * @param programItem a {@link ProgramItem} 758 * @param parentProgram The parent program or subprogram. Can be null. 759 * @return the parent {@link Program}s into this (sub)program or empty if not found 760 */ 761 private Set<Program> _getParentPrograms(ProgramItem programItem, AbstractProgram parentProgram) 762 { 763 if (programItem instanceof Program program) 764 { 765 return Set.of(program); 766 } 767 768 Set<Program> parentPrograms = new HashSet<>(); 769 770 AbstractProgram parent = parentProgram; 771 772 List<ProgramItem> parentItems = _odfHelper.getParentProgramItems(programItem, parentProgram); 773 774 for (ProgramItem parentItem : parentItems) 775 { 776 if (parentItem instanceof Program program) 777 { 778 parentPrograms.add(program); 779 } 780 else 781 { 782 if (parent != null && parentItem.equals(parent)) 783 { 784 // Once the desired abstract program parent is passed, the parent is null 785 parent = null; 786 } 787 788 parentPrograms.addAll(_getParentPrograms(parentItem, parent)); 789 } 790 } 791 792 return parentPrograms; 793 } 794 795 /** 796 * Returns the nearest {@link AbstractProgram} ancestors. 797 * @param programPart a {@link ProgramPart} 798 * @return the nearest {@link AbstractProgram} ancestors containing this program part 799 */ 800 public List<AbstractProgram> getNearestAncestorAbstractPrograms (ProgramPart programPart) 801 { 802 List<AbstractProgram> ancestors = new ArrayList<>(); 803 804 List<ProgramPart> parents = programPart.getProgramPartParents(); 805 for (ProgramPart parent : parents) 806 { 807 if (parent instanceof AbstractProgram) 808 { 809 ancestors.add((AbstractProgram) parent); 810 } 811 else 812 { 813 ancestors.addAll(getNearestAncestorAbstractPrograms(parent)); 814 } 815 } 816 817 return ancestors; 818 } 819 820 /** 821 * Returns the nearest {@link AbstractProgram} ancestor. 822 * @param programItem a {@link ProgramItem} 823 * @param parentProgram The parent program or subprogram 824 * @return the nearest {@link AbstractProgram} ancestor into this (sub)program or null if not found 825 */ 826 public AbstractProgram getNearestAncestorAbstractProgram (ProgramItem programItem, AbstractProgram parentProgram) 827 { 828 ProgramItem parentItem = _odfHelper.getParentProgramItem(programItem, parentProgram); 829 while (parentItem != null && !(parentItem instanceof AbstractProgram)) 830 { 831 parentItem = _odfHelper.getParentProgramItem(parentItem, parentProgram); 832 } 833 834 return parentItem != null ? (AbstractProgram) parentItem : null; 835 } 836 837 /** 838 * Returns the nearest {@link Course} ancestor. 839 * @param course a {@link Course} 840 * @param parentProgram The parent program or subprogram 841 * @return the nearest {@link Course} ancestor into this (sub)program or null if not found 842 */ 843 public Course getNearestAncestorCourse (Course course, AbstractProgram parentProgram) 844 { 845 ProgramItem parentItem = _odfHelper.getParentProgramItem(course, parentProgram); 846 while (parentItem != null && !(parentItem instanceof Course) && !(parentItem instanceof AbstractProgram)) 847 { 848 parentItem = _odfHelper.getParentProgramItem(parentItem, parentProgram); 849 } 850 851 return parentItem != null && parentItem instanceof Course ? (Course) parentItem : null; 852 } 853 854 /** 855 * Get the path of a {@link ProgramItem} page into the given {@link Program} 856 * @param siteName the site name 857 * @param language the language 858 * @param programItem the subprogram. 859 * @param parentProgram The parent program 860 * @return the page path or empty if no page matches 861 */ 862 public String getProgramItemPagePath(String siteName, String language, ProgramItem programItem, Program parentProgram) 863 { 864 Page rootPage = _odfPageHandler.getOdfRootPage(siteName, language, programItem.getCatalog()); 865 if (rootPage != null) 866 { 867 Page page = null; 868 if (programItem instanceof Program program) 869 { 870 page = getProgramPage(rootPage, program); 871 } 872 else if (programItem instanceof SubProgram subProgram) 873 { 874 page = getSubProgramPage(rootPage, subProgram, parentProgram); 875 } 876 else if (programItem instanceof Course course) 877 { 878 page = getCoursePage(rootPage, course, parentProgram); 879 } 880 881 if (page != null) 882 { 883 return rootPage.getSitemapName() + "/" + page.getPathInSitemap(); 884 } 885 } 886 887 return StringUtils.EMPTY; 888 } 889}