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.Set; 022 023import org.apache.avalon.framework.activity.Initializable; 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.avalon.framework.service.Serviceable; 028import org.apache.commons.lang3.StringUtils; 029 030import org.ametys.cms.repository.Content; 031import org.ametys.core.cache.AbstractCacheManager; 032import org.ametys.core.cache.Cache; 033import org.ametys.odf.ODFHelper; 034import org.ametys.odf.ProgramItem; 035import org.ametys.odf.course.Course; 036import org.ametys.odf.courselist.CourseList; 037import org.ametys.odf.program.AbstractProgram; 038import org.ametys.odf.program.Program; 039import org.ametys.odf.program.ProgramPart; 040import org.ametys.odf.program.SubProgram; 041import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 042import org.ametys.plugins.repository.AmetysObjectResolver; 043import org.ametys.plugins.repository.UnknownAmetysObjectException; 044import org.ametys.runtime.config.Config; 045import org.ametys.runtime.i18n.I18nizableText; 046import org.ametys.runtime.plugin.component.AbstractLogEnabled; 047import org.ametys.web.repository.page.Page; 048 049/** 050 * Resolves an ODF page path from the associated ODF content. 051 */ 052public class OdfPageResolver extends AbstractLogEnabled implements Component, Serviceable, Initializable 053{ 054 /** The avalon role. */ 055 public static final String ROLE = OdfPageResolver.class.getName(); 056 057 private static final String __PATH_IN_SITEMAP_CACHE = OdfPageResolver.class.getName() + "$pathInSitemap"; 058 private static final String __LEVELS_PATH_CACHE = OdfPageResolver.class.getName() + "$levelsPath"; 059 060 /** The ametys object resolver. */ 061 protected AmetysObjectResolver _ametysResolver; 062 /** The odf page handler */ 063 protected OdfPageHandler _odfPageHandler; 064 /** ODF helper */ 065 protected ODFHelper _odfHelper; 066 /** The cache manager */ 067 protected AbstractCacheManager _cacheManager; 068 069 public void service(ServiceManager serviceManager) throws ServiceException 070 { 071 _ametysResolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 072 _odfPageHandler = (OdfPageHandler) serviceManager.lookup(OdfPageHandler.ROLE); 073 _odfHelper = (ODFHelper) serviceManager.lookup(ODFHelper.ROLE); 074 _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE); 075 } 076 077 public void initialize() throws Exception 078 { 079 _cacheManager.createRequestCache(__PATH_IN_SITEMAP_CACHE, 080 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PATH_IN_SITEMAP_LABEL"), 081 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PATH_IN_SITEMAP_DESCRIPTION"), 082 false); 083 _cacheManager.createRequestCache(__LEVELS_PATH_CACHE, 084 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_LEVELS_PATH_LABEL"), 085 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_LEVELS_PATH_DESCRIPTION"), 086 false); 087 } 088 089 /** 090 * Get all referencing pages for this program item, in all sites and all sitemaps 091 * @param programItem The program item 092 * @return the referencing pages 093 */ 094 public Set<Page> getReferencingPages(ProgramItem programItem) 095 { 096 return getReferencingPages(programItem, null, ((Content) programItem).getLanguage()); 097 } 098 099 /** 100 * Get all referencing pages for this program item 101 * @param programItem The program item 102 * @param siteName The site name. Can be null to search on all sites 103 * @param lang The sitemap language. Can be null to search on all sitemaps 104 * @return the referencing pages 105 */ 106 public Set<Page> getReferencingPages(ProgramItem programItem, String siteName, String lang) 107 { 108 Set<Page> refPages = new HashSet<>(); 109 110 Set<Page> odfRootPages = _odfPageHandler.getOdfRootPages(siteName, lang); 111 112 for (Page rootPage : odfRootPages) 113 { 114 if (programItem instanceof Program) 115 { 116 ProgramPage programPage = getProgramPage(rootPage, (Program) programItem); 117 if (programPage != null) 118 { 119 refPages.add(programPage); 120 } 121 } 122 else if (programItem instanceof SubProgram) 123 { 124 Set<Program> parentPrograms = _odfHelper.getParentPrograms(programItem); 125 for (Program parentProgram : parentPrograms) 126 { 127 ProgramPage subProgramPage = getSubProgramPage(rootPage, (SubProgram) programItem, parentProgram); 128 if (subProgramPage != null) 129 { 130 refPages.add(subProgramPage); 131 } 132 } 133 } 134 else if (programItem instanceof Course) 135 { 136 List<CourseList> parentCourseLists = ((Course) programItem).getParentCourseLists(); 137 for (CourseList courseList : parentCourseLists) 138 { 139 List<Course> parentCourses = courseList.getParentCourses(); 140 for (Course parentCourse : parentCourses) 141 { 142 CoursePage coursePage = getCoursePage(rootPage, (Course) programItem, parentCourse); 143 if (coursePage != null) 144 { 145 refPages.add(coursePage); 146 } 147 } 148 149 List<AbstractProgram> parentAbstractPrograms = getNearestAncestorAbstractPrograms(courseList); 150 for (AbstractProgram parentAbstractProgram : parentAbstractPrograms) 151 { 152 CoursePage coursePage = getCoursePage(rootPage, (Course) programItem, parentAbstractProgram); 153 if (coursePage != null) 154 { 155 refPages.add(coursePage); 156 } 157 } 158 } 159 } 160 } 161 162 return refPages; 163 } 164 165 /** 166 * Return the program page 167 * @param program the program 168 * @return the page program or null 169 */ 170 public ProgramPage getProgramPage(Program program) 171 { 172 return getProgramPage(program, null); 173 } 174 175 /** 176 * Return the program page 177 * @param program the program 178 * @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. 179 * @return the page program or null 180 */ 181 public ProgramPage getProgramPage(Program program, String siteName) 182 { 183 Page odfRootPage = getOdfRootPage(siteName, program.getLanguage(), program.getCatalog()); 184 185 if (odfRootPage == null) 186 { 187 return null; 188 } 189 190 return getProgramPage(odfRootPage, program); 191 } 192 193 /** 194 * Return the program page 195 * @param odfRootPage the odf root page 196 * @param program the program 197 * @return the page program or null 198 */ 199 public ProgramPage getProgramPage (Page odfRootPage, Program program) 200 { 201 // E.g: program://_root?rootId=xxxx&programId=xxxx 202 String pageId = "program://_root?rootId=" + odfRootPage.getId() + "&programId=" + program.getId(); 203 try 204 { 205 return _ametysResolver.resolveById(pageId); 206 } 207 catch (UnknownAmetysObjectException e) 208 { 209 return null; 210 } 211 } 212 213 /** 214 * Return the subprogram page 215 * @param subProgram the subprogram 216 * @param parentProgram The parent program 217 * @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. 218 * @return the subprogram page or null 219 */ 220 public ProgramPage getSubProgramPage(SubProgram subProgram, AbstractProgram parentProgram, String siteName) 221 { 222 Page odfRootPage = getOdfRootPage(siteName, subProgram.getLanguage(), subProgram.getCatalog()); 223 224 if (odfRootPage == null) 225 { 226 return null; 227 } 228 229 return getSubProgramPage(odfRootPage, subProgram, parentProgram); 230 } 231 232 /** 233 * Return the subprogram page 234 * @param odfRootPage the odf root page 235 * @param subProgram the subprogram 236 * @param parentAbstractProgram The parent program or subprogram 237 * @return the subprogram page or null 238 */ 239 public ProgramPage getSubProgramPage (Page odfRootPage, SubProgram subProgram, AbstractProgram parentAbstractProgram) 240 { 241 AbstractProgram parent = getNearestAncestorAbstractProgram(subProgram, parentAbstractProgram); 242 Program parentProgram = getParentProgram(subProgram, parentAbstractProgram); 243 244 if (parent == null || parentProgram == null) 245 { 246 // No page 247 return null; 248 } 249 250 // Id is like program://path/to/subprogram?rootId=xxxx&programId=xxxx&parentId=xxxx 251 String pageId = "program://" + getPathInProgram(parent, parentProgram) + "?rootId=" + odfRootPage.getId() + "&programId=" + subProgram.getId() + "&parentId=" + parentProgram.getId(); 252 try 253 { 254 return _ametysResolver.resolveById(pageId); 255 } 256 catch (UnknownAmetysObjectException e) 257 { 258 return null; 259 } 260 } 261 262 /** 263 * Return the course page 264 * @param course the course 265 * @param parentProgram the parent program or subprogram. Can be null. 266 * @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. 267 * @return the course page or null if not found 268 */ 269 public CoursePage getCoursePage(Course course, AbstractProgram parentProgram, String siteName) 270 { 271 String catalog = course.getCatalog(); 272 Page odfRootPage = getOdfRootPage(siteName, course.getLanguage(), catalog); 273 274 if (odfRootPage == null) 275 { 276 return null; 277 } 278 279 return getCoursePage(odfRootPage, course, parentProgram); 280 } 281 282 /** 283 * Return the course page 284 * @param odfRootPage the odf root page 285 * @param course the course 286 * @param parentAbstractProgram the parent program or subprogram. Can be null. 287 * @return the course page or null if not found 288 */ 289 public CoursePage getCoursePage (Page odfRootPage, Course course, AbstractProgram parentAbstractProgram) 290 { 291 ProgramItem parent = null; 292 293 Course parentCourse = getNearestAncestorCourse(course, parentAbstractProgram); 294 if (parentCourse != null) 295 { 296 parent = parentCourse; 297 } 298 else 299 { 300 parent = getNearestAncestorAbstractProgram(course, parentAbstractProgram); 301 } 302 Program parentProgram = getParentProgram(course, parentAbstractProgram); 303 304 if (parent == null || parentProgram == null) 305 { 306 // No page 307 return null; 308 } 309 310 // Test program restriction 311 if (!_odfPageHandler.isValidRestriction(odfRootPage, parentProgram)) 312 { 313 return null; 314 } 315 316 // Id is like course://path/from/program?rootId=xxx&courseId=xxx&programId=xxxx 317 String pageId = "course://" + getPathInProgram(parent, parentProgram) + "?rootId=" + odfRootPage.getId() + "&courseId=" + course.getId() + "&programId=" + parentProgram.getId(); 318 try 319 { 320 return _ametysResolver.resolveById(pageId); 321 } 322 catch (UnknownAmetysObjectException e) 323 { 324 return null; 325 } 326 } 327 328 /** 329 * Return the course page 330 * @param course the course 331 * @param parentCourse the parent course. Can NOT be null. 332 * @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. 333 * @return the course page or null if not found 334 */ 335 public CoursePage getCoursePage (Course course, Course parentCourse, String siteName) 336 { 337 String catalog = course.getCatalog(); 338 Page odfRootPage = getOdfRootPage(siteName, course.getLanguage(), catalog); 339 340 if (odfRootPage == null) 341 { 342 return null; 343 } 344 345 return getCoursePage(odfRootPage, course, parentCourse); 346 } 347 348 /** 349 * Return the course page 350 * @param odfRootPage the odf root page 351 * @param course the course 352 * @param parentCourse the parent course. Can NOT be null. 353 * @return the course page or null if not found 354 */ 355 public CoursePage getCoursePage (Page odfRootPage, Course course, Course parentCourse) 356 { 357 AbstractProgram parent = getNearestAncestorAbstractProgram(parentCourse, null); 358 Program parentProgram = getParentProgram(parentCourse, null); 359 360 if (parent == null || parentProgram == null) 361 { 362 // No page 363 return null; 364 } 365 366 // Test program restriction 367 if (!_odfPageHandler.isValidRestriction(odfRootPage, parentProgram)) 368 { 369 return null; 370 } 371 372 // Id is like course://path/from/program?rootId=xxx&courseId=xxx&programId=xxxx 373 String pageId = "course://" + getPathInProgram(parentCourse, parentProgram) + "?rootId=" + odfRootPage.getId() + "&courseId=" + course.getId() + "&programId=" + parentProgram.getId(); 374 try 375 { 376 return _ametysResolver.resolveById(pageId); 377 } 378 catch (UnknownAmetysObjectException e) 379 { 380 return null; 381 } 382 } 383 384 /** 385 * Get the ODF root page, either in the given site if it exists, or in the default ODF site. 386 * @param siteName the desired site name. 387 * @param language the sitemap language to search in. 388 * @param catalog The ODF catalog 389 * @return the ODF root page, either in the given site if it exists, or in the default ODF site. 390 */ 391 public Page getOdfRootPage(String siteName, String language, String catalog) 392 { 393 Page odfRootPage = null; 394 395 if (StringUtils.isNotEmpty(siteName)) 396 { 397 odfRootPage = _odfPageHandler.getOdfRootPage(siteName, language, catalog); 398 } 399 400 if (odfRootPage == null) 401 { 402 String odfSiteName = Config.getInstance().getValue("odf.web.site.name"); 403 odfRootPage = _odfPageHandler.getOdfRootPage(odfSiteName, language, catalog); 404 } 405 406 return odfRootPage; 407 } 408 409 /** 410 * Get the path in sitemap of a ODF content into a {@link Program} or {@link SubProgram}<br> 411 * Be careful, this is the path in sitemap, to get the path of a item into a Program, use {@link ODFHelper#getPathInProgram} instead. 412 * @param programItem The program item 413 * @param parentProgram The parent root (sub)program. Can be null. 414 * @return the path in sitemap from the parent program or null if no found ODF path for those program item and parent program 415 */ 416 public String getPathInProgram (ProgramItem programItem, AbstractProgram parentProgram) 417 { 418 Cache<PathInProgramCacheKey, String> cache = _cacheManager.get(__PATH_IN_SITEMAP_CACHE); 419 420 return cache.get(PathInProgramCacheKey.of(programItem.getId(), parentProgram != null ? parentProgram.getId() : "__NOPARENT"), item -> { 421 422 if (programItem instanceof Program || programItem.equals(parentProgram)) 423 { 424 // The program item is already the program it self 425 return _odfPageHandler.getPageName(programItem); 426 } 427 428 List<String> paths = new ArrayList<>(); 429 430 // Add the parent path in program if exists 431 ProgramItem parent = _odfHelper.getParentProgramItem(programItem, parentProgram); 432 if (parent != null) 433 { 434 String parentPath = getPathInProgram(parent, parentProgram); 435 if (parentPath != null) 436 { 437 paths.add(parentPath); 438 } 439 } 440 441 // Add the current page name (if it is an item with a page (only course, subprogram and program) 442 if (programItem instanceof AbstractProgram || programItem instanceof Course) 443 { 444 paths.add(_odfPageHandler.getPageName(programItem)); 445 } 446 447 // If the path is empty, return null 448 return paths.isEmpty() ? null : StringUtils.join(paths, "/"); 449 }); 450 } 451 452 private static class PathInProgramCacheKey extends AbstractCacheKey 453 { 454 protected PathInProgramCacheKey(String programItemId, String parentProgramId) 455 { 456 super(programItemId, parentProgramId); 457 } 458 459 public static PathInProgramCacheKey of(String programItemId, String parentProgramId) 460 { 461 return new PathInProgramCacheKey(programItemId, parentProgramId); 462 } 463 } 464 465 /** 466 * Returns the first {@link Program} ancestor, ensuring that the given parent content 'parentProgram' is in the hierarchy, if not null.<br> 467 * If 'parentProgram' is null, the first {@link Program} ancestor will be returned regardless of parent hierarchy.<br> 468 * If 'parentProgram' is a {@link SubProgram}, the first {@link Program} ancestor from this {@link SubProgram} will be returned regardless of parent hierarchy 469 * @param programItem a {@link ProgramItem} 470 * @param parentProgram The parent program or subprogram. Can be null. 471 * @return the parent {@link Program} into this (sub)program or null if not found 472 */ 473 public Program getParentProgram (ProgramItem programItem, AbstractProgram parentProgram) 474 { 475 AbstractProgram parent = parentProgram; 476 477 ProgramItem parentItem = _odfHelper.getParentProgramItem(programItem, parentProgram); 478 while (parentItem != null && !(parentItem instanceof Program)) 479 { 480 if (parent != null && parentItem.equals(parent)) 481 { 482 // Once the desired abstract program parent is passed, the parent is null 483 parent = null; 484 } 485 parentItem = _odfHelper.getParentProgramItem(parentItem, parent); 486 } 487 488 return parentItem != null ? (Program) parentItem : null; 489 } 490 491 /** 492 * Returns the nearest {@link AbstractProgram} ancestors. 493 * @param programPart a {@link ProgramPart} 494 * @return the nearest {@link AbstractProgram} ancestors containing this program part 495 */ 496 public List<AbstractProgram> getNearestAncestorAbstractPrograms (ProgramPart programPart) 497 { 498 List<AbstractProgram> ancestors = new ArrayList<>(); 499 500 List<ProgramPart> parents = programPart.getProgramPartParents(); 501 for (ProgramPart parent : parents) 502 { 503 if (parent instanceof AbstractProgram) 504 { 505 ancestors.add((AbstractProgram) parent); 506 } 507 else 508 { 509 ancestors.addAll(getNearestAncestorAbstractPrograms(parent)); 510 } 511 } 512 513 return ancestors; 514 } 515 516 /** 517 * Returns the nearest {@link AbstractProgram} ancestor. 518 * @param programItem a {@link ProgramItem} 519 * @param parentProgram The parent program or subprogram 520 * @return the nearest {@link AbstractProgram} ancestor into this (sub)program or null if not found 521 */ 522 public AbstractProgram getNearestAncestorAbstractProgram (ProgramItem programItem, AbstractProgram parentProgram) 523 { 524 ProgramItem parentItem = _odfHelper.getParentProgramItem(programItem, parentProgram); 525 while (parentItem != null && !(parentItem instanceof AbstractProgram)) 526 { 527 parentItem = _odfHelper.getParentProgramItem(parentItem, parentProgram); 528 } 529 530 return parentItem != null ? (AbstractProgram) parentItem : null; 531 } 532 533 /** 534 * Returns the nearest {@link Course} ancestor. 535 * @param course a {@link Course} 536 * @param parentProgram The parent program or subprogram 537 * @return the nearest {@link Course} ancestor into this (sub)program or null if not found 538 */ 539 public Course getNearestAncestorCourse (Course course, AbstractProgram parentProgram) 540 { 541 ProgramItem parentItem = _odfHelper.getParentProgramItem(course, parentProgram); 542 while (parentItem != null && !(parentItem instanceof Course) && !(parentItem instanceof AbstractProgram)) 543 { 544 parentItem = _odfHelper.getParentProgramItem(parentItem, parentProgram); 545 } 546 547 return parentItem != null && parentItem instanceof Course ? (Course) parentItem : null; 548 } 549 550 /** 551 * Get the path of a {@link ProgramItem} page into the given {@link Program} 552 * @param siteName the site name 553 * @param language the language 554 * @param programItem the subprogram. 555 * @param parentProgram The parent program 556 * @return the page path or empty if no page matches 557 */ 558 public String getProgramItemPagePath(String siteName, String language, ProgramItem programItem, Program parentProgram) 559 { 560 String catalog = programItem.getCatalog(); 561 562 // Get the path with the language, path to odf root page, and levels, finishing by a / 563 Cache<ODFLevelsPathCacheKey, String> cache = _cacheManager.get(__LEVELS_PATH_CACHE); 564 String rootPath = cache.get(ODFLevelsPathCacheKey.of(siteName, language, catalog, parentProgram.getId()), key -> _getLevelsPath(siteName, language, programItem.getCatalog(), parentProgram)); 565 566 if (rootPath != null) 567 { 568 // Add the path from the parent program to the program item 569 String pathInProgram = getPathInProgram(programItem, parentProgram); 570 if (pathInProgram != null) 571 { 572 return rootPath + pathInProgram; 573 } 574 } 575 576 return StringUtils.EMPTY; 577 } 578 579 private String _getLevelsPath(String siteName, String language, String catalog, Program parentProgram) 580 { 581 // Get the ODF root page for given site, language and catalog 582 Page rootPage = _odfPageHandler.getOdfRootPage(siteName, language, catalog); 583 584 StringBuilder sb = new StringBuilder() 585 // Sitemap (language) 586 .append(rootPage.getSitemapName()).append('/') 587 // Path from sitemap to ODF root page 588 .append(rootPage.getPathInSitemap()).append('/'); 589 590 // Add level1 if defined 591 String level1 = _odfPageHandler.getLevel1PageName(rootPage, parentProgram); 592 if (StringUtils.isNotEmpty(level1)) 593 { 594 sb.append(level1).append('/'); 595 } 596 597 // Add level2 if defined 598 String level2 = _odfPageHandler.getLevel2PageName(rootPage, parentProgram); 599 if (StringUtils.isNotEmpty(level1)) 600 { 601 sb.append(level2).append('/'); 602 } 603 604 return sb.toString(); 605 } 606 607 private static class ODFLevelsPathCacheKey extends AbstractCacheKey 608 { 609 protected ODFLevelsPathCacheKey(String site, String lang, String catalog, String parentProgramId) 610 { 611 super(site, lang, catalog, parentProgramId); 612 } 613 614 public static ODFLevelsPathCacheKey of(String site, String lang, String catalog, String parentProgramId) 615 { 616 return new ODFLevelsPathCacheKey(site, lang, catalog, parentProgramId); 617 } 618 } 619}