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