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