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.Collections; 020import java.util.HashSet; 021import java.util.List; 022import java.util.Optional; 023import java.util.Set; 024 025import org.apache.avalon.framework.activity.Initializable; 026import org.apache.avalon.framework.component.Component; 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.avalon.framework.service.Serviceable; 030import org.apache.commons.lang.StringUtils; 031 032import org.ametys.cms.repository.Content; 033import org.ametys.core.cache.AbstractCacheManager; 034import org.ametys.core.cache.Cache; 035import org.ametys.odf.ODFHelper; 036import org.ametys.odf.ProgramItem; 037import org.ametys.odf.course.Course; 038import org.ametys.odf.courselist.CourseList; 039import org.ametys.odf.program.AbstractProgram; 040import org.ametys.odf.program.Program; 041import org.ametys.odf.program.ProgramPart; 042import org.ametys.odf.program.SubProgram; 043import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 044import org.ametys.plugins.odfweb.OdfURIResolver; 045import org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint; 046import org.ametys.plugins.repository.AmetysObjectResolver; 047import org.ametys.plugins.repository.UnknownAmetysObjectException; 048import org.ametys.runtime.config.Config; 049import org.ametys.runtime.i18n.I18nizableText; 050import org.ametys.runtime.plugin.component.AbstractLogEnabled; 051import org.ametys.web.repository.page.Page; 052 053/** 054 * Resolves an ODF page path from the associated ODF content. 055 */ 056public class OdfPageResolver extends AbstractLogEnabled implements Component, Serviceable, Initializable 057{ 058 /** The avalon role. */ 059 public static final String ROLE = OdfPageResolver.class.getName(); 060 061 private static final String __PATH_IN_SITEMAP_CACHE = OdfPageResolver.class.getName() + "$pathInSitemap"; 062 private static final String __RESOLVED_PATH_CACHE = OdfPageResolver.class.getName() + "$resolvedPath"; 063 private static final String __ODF_ROOT_PROGRAM_CACHE = OdfURIResolver.class.getName() + "$rootProgram"; 064 065 /** The ametys object resolver. */ 066 protected AmetysObjectResolver _ametysResolver; 067 /** The odf page handler */ 068 protected OdfPageHandler _odfPageHandler; 069 /** ODF helper */ 070 protected ODFHelper _odfHelper; 071 /** The cache manager */ 072 protected AbstractCacheManager _cacheManager; 073 /** The course page factory */ 074 protected CoursePageFactory _coursePageFactory; 075 /** The program page factory */ 076 protected ProgramPageFactory _programPageFactory; 077 078 @Override 079 public void service(ServiceManager serviceManager) throws ServiceException 080 { 081 _ametysResolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 082 _odfPageHandler = (OdfPageHandler) serviceManager.lookup(OdfPageHandler.ROLE); 083 _odfHelper = (ODFHelper) serviceManager.lookup(ODFHelper.ROLE); 084 _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE); 085 AmetysObjectFactoryExtensionPoint ametysObjectFactoryEP = (AmetysObjectFactoryExtensionPoint) serviceManager.lookup(AmetysObjectFactoryExtensionPoint.ROLE); 086 _coursePageFactory = (CoursePageFactory) ametysObjectFactoryEP.getExtension(CoursePageFactory.class.getName()); 087 _programPageFactory = (ProgramPageFactory) ametysObjectFactoryEP.getExtension(ProgramPageFactory.class.getName()); 088 } 089 090 public void initialize() throws Exception 091 { 092 _cacheManager.createRequestCache(__PATH_IN_SITEMAP_CACHE, 093 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PATH_IN_SITEMAP_LABEL"), 094 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PATH_IN_SITEMAP_DESCRIPTION"), 095 false); 096 097 _cacheManager.createRequestCache(__RESOLVED_PATH_CACHE, 098 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_RESOLVED_PATH_LABEL"), 099 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_RESOLVED_PATH_DESCRIPTION"), 100 false); 101 102 _cacheManager.createRequestCache(__ODF_ROOT_PROGRAM_CACHE, 103 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ROOT_PROGRAM_LABEL"), 104 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ROOT_PROGRAM_DESCRIPTION"), 105 false); 106 } 107 108 /** 109 * Get all referencing pages for this program item, in all sites and all sitemaps 110 * @param programItem The program item 111 * @return the referencing pages 112 */ 113 public Set<Page> getReferencingPages(ProgramItem programItem) 114 { 115 return getReferencingPages(programItem, null, ((Content) programItem).getLanguage()); 116 } 117 118 /** 119 * Get all referencing pages for this program item 120 * @param programItem The program item 121 * @param siteName The site name. Can be null to search on all sites 122 * @param lang The sitemap language. Can be null to search on all sitemaps 123 * @return the referencing pages 124 */ 125 public Set<Page> getReferencingPages(ProgramItem programItem, String siteName, String lang) 126 { 127 Set<Page> refPages = new HashSet<>(); 128 129 Set<Page> odfRootPages = _odfPageHandler.getOdfRootPages(siteName, lang); 130 131 for (Page rootPage : odfRootPages) 132 { 133 if (programItem instanceof Program) 134 { 135 ProgramPage programPage = getProgramPage(rootPage, (Program) programItem); 136 if (programPage != null) 137 { 138 refPages.add(programPage); 139 } 140 } 141 else if (programItem instanceof SubProgram) 142 { 143 Set<Program> parentPrograms = _odfHelper.getParentPrograms(programItem); 144 for (Program parentProgram : parentPrograms) 145 { 146 ProgramPage subProgramPage = getSubProgramPage(rootPage, (SubProgram) programItem, parentProgram); 147 if (subProgramPage != null) 148 { 149 refPages.add(subProgramPage); 150 } 151 } 152 } 153 else if (programItem instanceof Course) 154 { 155 List<CourseList> parentCourseLists = ((Course) programItem).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) programItem, 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) programItem, parentAbstractProgram); 172 if (coursePage != null) 173 { 174 refPages.add(coursePage); 175 } 176 } 177 } 178 } 179 } 180 181 return refPages; 182 } 183 184 /** 185 * Return the program page 186 * @param program the program 187 * @return the page program or null 188 */ 189 public ProgramPage getProgramPage(Program program) 190 { 191 return getProgramPage(program, null); 192 } 193 194 /** 195 * Return the program page 196 * @param program the program 197 * @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. 198 * @return the page program or null 199 */ 200 public ProgramPage getProgramPage(Program program, String siteName) 201 { 202 Page odfRootPage = getOdfRootPage(siteName, program.getLanguage(), program.getCatalog()); 203 204 if (odfRootPage == null) 205 { 206 return null; 207 } 208 209 return getProgramPage(odfRootPage, program); 210 } 211 212 /** 213 * Return the program page 214 * @param odfRootPage the odf root page 215 * @param program the program 216 * @return the page program or null 217 */ 218 public ProgramPage getProgramPage (Page odfRootPage, Program program) 219 { 220 // E.g: program://_root?rootId=xxxx&programId=xxxx 221 String pageId = "program://_root?rootId=" + odfRootPage.getId() + "&programId=" + program.getId(); 222 try 223 { 224 return _ametysResolver.resolveById(pageId); 225 } 226 catch (UnknownAmetysObjectException e) 227 { 228 return null; 229 } 230 } 231 232 /** 233 * Return the subprogram page 234 * @param subProgram the subprogram 235 * @param parentProgram The parent program 236 * @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. 237 * @return the subprogram page or null 238 */ 239 public ProgramPage getSubProgramPage(SubProgram subProgram, AbstractProgram parentProgram, String siteName) 240 { 241 Page odfRootPage = getOdfRootPage(siteName, subProgram.getLanguage(), subProgram.getCatalog()); 242 243 if (odfRootPage == null) 244 { 245 return null; 246 } 247 248 return getSubProgramPage(odfRootPage, subProgram, parentProgram); 249 } 250 251 /** 252 * Return the subprogram page 253 * @param odfRootPage the odf root page 254 * @param subProgram the subprogram 255 * @param parentAbstractProgram The parent program or subprogram 256 * @return the subprogram page or null 257 */ 258 public ProgramPage getSubProgramPage (Page odfRootPage, SubProgram subProgram, AbstractProgram parentAbstractProgram) 259 { 260 try 261 { 262 // Get first parent program matching an existing program page 263 Program parentProgram = getRootProgram(odfRootPage, subProgram, parentAbstractProgram); 264 if (parentProgram == null) 265 { 266 // No program page 267 return null; 268 } 269 270 AbstractProgram parent = getNearestAncestorAbstractProgram(subProgram, parentAbstractProgram); 271 if (parent == null) 272 { 273 return null; // no page 274 } 275 276 String path = getPathInProgram(parent, parentProgram); 277 if (path == null) 278 { 279 // Subprogram is not part of the selected parent program 280 return null; 281 } 282 return _programPageFactory.createProgramPage(odfRootPage, subProgram, path, parentProgram, null); 283 } 284 catch (UnknownAmetysObjectException e) 285 { 286 return null; 287 } 288 } 289 290 /** 291 * Returns the subprogram page 292 * @param odfRootPage the odf root page 293 * @param subProgram the subprogram 294 * @param path a full or partial path of subprogram 295 * @param checkHierarchy set to true to check that the given path is a valid hierarchy 296 * @return the subprogram page or null if not found 297 */ 298 public ProgramPage getSubProgramPage(Page odfRootPage, SubProgram subProgram, List<String> path, boolean checkHierarchy) 299 { 300 // Possible paths are : 301 // [subprogramContent://UUID, subprogramContent://UUID, subprogramContent://UUID] 302 // [programContent://UUID, subprogramContent://UUID, subprogramContent://UUID] 303 304 try 305 { 306 307 ProgramItem lastParent = _ametysResolver.resolveById(path.get(0)); 308 Program parentProgram = getRootProgram(odfRootPage, lastParent, null); 309 if (parentProgram == null) 310 { 311 // No page 312 return null; 313 } 314 315 List<String> reversedPath = path.reversed(); 316 if (checkHierarchy) 317 { 318 ProgramItem nextProgramItem = _ametysResolver.resolveById(path.getLast()); 319 if (!_checkHierarchy(subProgram, nextProgramItem)) 320 { 321 // Parent item in path is not part of program item's parents 322 getLogger().warn(reversedPath + " is not valid hierarchy"); 323 throw new UnknownAmetysObjectException(reversedPath + " is not valid hierarchy"); 324 } 325 } 326 327 String pagePath = _resolvePagePath(reversedPath, checkHierarchy); 328 if (pagePath == null) 329 { 330 // No page 331 return null; 332 } 333 334 return _programPageFactory.createProgramPage(odfRootPage, subProgram, pagePath, parentProgram, null); 335 } 336 catch (UnknownAmetysObjectException e) 337 { 338 return null; 339 } 340 } 341 342 /** 343 * Return the course page 344 * @param course the course 345 * @param parentProgram the parent program or subprogram. Can be null. 346 * @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. 347 * @return the course page or null if not found 348 */ 349 public CoursePage getCoursePage(Course course, AbstractProgram parentProgram, String siteName) 350 { 351 String catalog = course.getCatalog(); 352 Page odfRootPage = getOdfRootPage(siteName, course.getLanguage(), catalog); 353 354 if (odfRootPage == null) 355 { 356 return null; 357 } 358 359 return getCoursePage(odfRootPage, course, parentProgram); 360 } 361 362 /** 363 * Return the course page 364 * @param odfRootPage the odf root page 365 * @param course the course 366 * @param parentAbstractProgram the parent program or subprogram. Can be null. 367 * @return the course page or null if not found 368 */ 369 public CoursePage getCoursePage (Page odfRootPage, Course course, AbstractProgram parentAbstractProgram) 370 { 371 try 372 { 373 // Get first parent program matching an existing program page 374 Program parentProgram = getRootProgram(odfRootPage, course, parentAbstractProgram); 375 if (parentProgram == null) 376 { 377 // No program page 378 return null; 379 } 380 381 ProgramItem parent = null; 382 383 Course parentCourse = getNearestAncestorCourse(course, parentAbstractProgram); 384 if (parentCourse != null) 385 { 386 parent = parentCourse; 387 } 388 else 389 { 390 parent = getNearestAncestorAbstractProgram(course, parentAbstractProgram); 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 if (programItem instanceof AbstractProgram || programItem instanceof Course) 675 { 676 paths.add(_odfPageHandler.getPageName(programItem)); 677 } 678 679 ProgramItem parent = _odfHelper.getParentProgramItem(programItem, parentProgram); 680 while (parent != null && !(parent instanceof Program)) 681 { 682 if (parent instanceof AbstractProgram || parent instanceof Course) 683 { 684 paths.add(_odfPageHandler.getPageName(parent)); 685 } 686 parent = _odfHelper.getParentProgramItem(parent, parentProgram); 687 } 688 689 if (parent != null) 690 { 691 paths.add(_odfPageHandler.getPageName(parent)); 692 Collections.reverse(paths); 693 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 694 } 695 696 return null; 697 }); 698 } 699 700 private static class RootProgramCacheKey extends AbstractCacheKey 701 { 702 public RootProgramCacheKey(Page odfRootPage, String programItemId, String parentProgramId) 703 { 704 super(odfRootPage, programItemId, parentProgramId); 705 } 706 707 public static RootProgramCacheKey of(Page odfRootPage, String programItemId, String parentProgramId) 708 { 709 return new RootProgramCacheKey(odfRootPage, programItemId, parentProgramId); 710 } 711 } 712 713 private static class PathInProgramCacheKey extends AbstractCacheKey 714 { 715 public PathInProgramCacheKey(String programItemId, String parentProgramId) 716 { 717 super(programItemId, parentProgramId); 718 } 719 720 public static PathInProgramCacheKey of(String programItemId, String parentProgramId) 721 { 722 return new PathInProgramCacheKey(programItemId, parentProgramId); 723 } 724 } 725 726 /** 727 * 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> 728 * If 'parentProgram' is null, the first {@link Program} ancestor will be returned regardless of parent hierarchy.<br> 729 * If 'parentProgram' is a {@link SubProgram}, the first {@link Program} ancestor from this {@link SubProgram} will be returned regardless of parent hierarchy 730 * @param odfRootPage The ODf root page. Cannot be null. 731 * @param programItem a {@link ProgramItem} 732 * @param parentAbstractProgram The parent program or subprogram. Can be null. 733 * @return the parent {@link Program} into this (sub)program that matchs an existing program page, or null if not found 734 */ 735 public Program getRootProgram(Page odfRootPage, ProgramItem programItem, AbstractProgram parentAbstractProgram) 736 { 737 Cache<RootProgramCacheKey, Program> rootCache = _cacheManager.get(__ODF_ROOT_PROGRAM_CACHE); 738 739 return rootCache.get(RootProgramCacheKey.of(odfRootPage, programItem.getId(), parentAbstractProgram != null ? parentAbstractProgram.getId() : "__NOPARENT"), k -> { 740 // Get all parent programs 741 Set<Program> parentPrograms = _getParentPrograms(programItem, parentAbstractProgram); 742 743 // Get first parent program matching an existing program page 744 Optional<Program> parentProgram = parentPrograms.stream() 745 .filter(p -> isProgramPageExist(odfRootPage, p)) 746 .findFirst(); 747 748 return parentProgram.orElse(null); 749 }); 750 } 751 752 /** 753 * Returns all parent {@link Program} ancestors, ensuring that the given parent content 'parentProgram' is in the hierarchy, if not null.<br> 754 * If 'parentProgram' is null, the all {@link Program} ancestors will be returned regardless of parent hierarchy.<br> 755 * If 'parentProgram' is a {@link SubProgram}, the {@link Program} ancestors from this {@link SubProgram} will be returned regardless of parent hierarchy 756 * @param programItem a {@link ProgramItem} 757 * @param parentProgram The parent program or subprogram. Can be null. 758 * @return the parent {@link Program}s into this (sub)program or empty if not found 759 */ 760 private Set<Program> _getParentPrograms(ProgramItem programItem, AbstractProgram parentProgram) 761 { 762 if (programItem instanceof Program program) 763 { 764 return Set.of(program); 765 } 766 767 Set<Program> parentPrograms = new HashSet<>(); 768 769 AbstractProgram parent = parentProgram; 770 771 List<ProgramItem> parentItems = _odfHelper.getParentProgramItems(programItem, parentProgram); 772 773 for (ProgramItem parentItem : parentItems) 774 { 775 if (parentItem instanceof Program program) 776 { 777 parentPrograms.add(program); 778 } 779 else 780 { 781 if (parent != null && parentItem.equals(parent)) 782 { 783 // Once the desired abstract program parent is passed, the parent is null 784 parent = null; 785 } 786 787 parentPrograms.addAll(_getParentPrograms(parentItem, parent)); 788 } 789 } 790 791 return parentPrograms; 792 } 793 794 /** 795 * Returns the nearest {@link AbstractProgram} ancestors. 796 * @param programPart a {@link ProgramPart} 797 * @return the nearest {@link AbstractProgram} ancestors containing this program part 798 */ 799 public List<AbstractProgram> getNearestAncestorAbstractPrograms (ProgramPart programPart) 800 { 801 List<AbstractProgram> ancestors = new ArrayList<>(); 802 803 List<ProgramPart> parents = programPart.getProgramPartParents(); 804 for (ProgramPart parent : parents) 805 { 806 if (parent instanceof AbstractProgram) 807 { 808 ancestors.add((AbstractProgram) parent); 809 } 810 else 811 { 812 ancestors.addAll(getNearestAncestorAbstractPrograms(parent)); 813 } 814 } 815 816 return ancestors; 817 } 818 819 /** 820 * Returns the nearest {@link AbstractProgram} ancestor. 821 * @param programItem a {@link ProgramItem} 822 * @param parentProgram The parent program or subprogram 823 * @return the nearest {@link AbstractProgram} ancestor into this (sub)program or null if not found 824 */ 825 public AbstractProgram getNearestAncestorAbstractProgram (ProgramItem programItem, AbstractProgram parentProgram) 826 { 827 ProgramItem parentItem = _odfHelper.getParentProgramItem(programItem, parentProgram); 828 while (parentItem != null && !(parentItem instanceof AbstractProgram)) 829 { 830 parentItem = _odfHelper.getParentProgramItem(parentItem, parentProgram); 831 } 832 833 return parentItem != null ? (AbstractProgram) parentItem : null; 834 } 835 836 /** 837 * Returns the nearest {@link Course} ancestor. 838 * @param course a {@link Course} 839 * @param parentProgram The parent program or subprogram 840 * @return the nearest {@link Course} ancestor into this (sub)program or null if not found 841 */ 842 public Course getNearestAncestorCourse (Course course, AbstractProgram parentProgram) 843 { 844 ProgramItem parentItem = _odfHelper.getParentProgramItem(course, parentProgram); 845 while (parentItem != null && !(parentItem instanceof Course) && !(parentItem instanceof AbstractProgram)) 846 { 847 parentItem = _odfHelper.getParentProgramItem(parentItem, parentProgram); 848 } 849 850 return parentItem != null && parentItem instanceof Course ? (Course) parentItem : null; 851 } 852 853 /** 854 * Get the path of a Program page from the Program content. 855 * @param siteName the site name. 856 * @param language the language. 857 * @param program the program. 858 * @return the page path or empty if no page matches 859 */ 860 public String getProgramPagePath(String siteName, String language, Program program) 861 { 862 return getProgramItemPagePath(siteName, language, program, program); 863 } 864 865 /** 866 * Get the path of a {@link ProgramItem} page into the given {@link Program} 867 * @param siteName the site name 868 * @param language the language 869 * @param programItem the subprogram. 870 * @param parentProgram The parent program 871 * @return the page path or empty if no page matches 872 */ 873 public String getProgramItemPagePath(String siteName, String language, ProgramItem programItem, Program parentProgram) 874 { 875 StringBuilder sb = new StringBuilder(); 876 877 Page rootPage = _odfPageHandler.getOdfRootPage(siteName, language, programItem.getCatalog()); 878 if (rootPage != null) 879 { 880 if (programItem instanceof Program) 881 { 882 sb.append(rootPage.getSitemapName()).append('/') 883 .append(rootPage.getPathInSitemap()).append('/') 884 .append(_odfPageHandler.getLevel1PageName(rootPage, parentProgram)).append('/') 885 .append(_odfPageHandler.getLevel2PageName(rootPage, parentProgram)).append('/') 886 .append(_odfPageHandler.getPageName(programItem)); 887 } 888 else 889 { 890 String pathInProgram = getPathInProgram(programItem, parentProgram); 891 if (pathInProgram != null) 892 { 893 sb.append(rootPage.getSitemapName()).append('/') 894 .append(rootPage.getPathInSitemap()).append('/') 895 .append(_odfPageHandler.getLevel1PageName(rootPage, parentProgram)).append('/') 896 .append(_odfPageHandler.getLevel2PageName(rootPage, parentProgram)).append('/') 897 .append(pathInProgram); 898 } 899 } 900 901 } 902 903 return sb.toString(); 904 } 905}