001/* 002 * Copyright 2012 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.Arrays; 019import java.util.Collections; 020import java.util.Iterator; 021import java.util.List; 022import java.util.Map; 023import java.util.Objects; 024import java.util.Set; 025import java.util.stream.Collectors; 026 027import javax.jcr.Node; 028import javax.jcr.RepositoryException; 029import javax.jcr.Value; 030 031import org.apache.avalon.framework.activity.Initializable; 032import org.apache.avalon.framework.component.Component; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.commons.lang3.StringUtils; 037 038import org.ametys.cms.content.ContentHelper; 039import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 040import org.ametys.cms.contenttype.ContentTypesHelper; 041import org.ametys.cms.repository.Content; 042import org.ametys.core.cache.AbstractCacheManager; 043import org.ametys.core.cache.Cache; 044import org.ametys.core.ui.Callable; 045import org.ametys.core.util.I18nUtils; 046import org.ametys.core.util.URIUtils; 047import org.ametys.odf.ProgramItem; 048import org.ametys.odf.catalog.CatalogsManager; 049import org.ametys.odf.course.Course; 050import org.ametys.odf.enumeration.OdfReferenceTableHelper; 051import org.ametys.odf.orgunit.RootOrgUnitProvider; 052import org.ametys.odf.program.AbstractProgram; 053import org.ametys.odf.program.Program; 054import org.ametys.odf.program.SubProgram; 055import org.ametys.odf.tree.OdfClassificationHandler; 056import org.ametys.odf.tree.OdfClassificationHandler.LevelValue; 057import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 058import org.ametys.plugins.odfweb.restrictions.OdfProgramRestriction; 059import org.ametys.plugins.odfweb.restrictions.OdfProgramRestrictionManager; 060import org.ametys.plugins.repository.AmetysObjectIterable; 061import org.ametys.plugins.repository.AmetysObjectResolver; 062import org.ametys.plugins.repository.AmetysRepositoryException; 063import org.ametys.plugins.repository.jcr.JCRAmetysObject; 064import org.ametys.plugins.repository.jcr.NameHelper; 065import org.ametys.plugins.repository.provider.WorkspaceSelector; 066import org.ametys.plugins.repository.query.expression.Expression; 067import org.ametys.plugins.repository.query.expression.VirtualFactoryExpression; 068import org.ametys.runtime.i18n.I18nizableText; 069import org.ametys.runtime.model.ModelItem; 070import org.ametys.runtime.plugin.component.AbstractLogEnabled; 071import org.ametys.web.repository.page.Page; 072import org.ametys.web.repository.page.PageQueryHelper; 073import org.ametys.web.repository.site.Site; 074import org.ametys.web.repository.sitemap.Sitemap; 075 076import com.google.common.collect.ImmutableList; 077 078/** 079 * Component providing methods to retrieve ODF virtual pages, such as the ODF root, 080 * level 1 and 2 metadata names, and so on. 081 */ 082public class OdfPageHandler extends AbstractLogEnabled implements Component, Initializable, Serviceable 083{ 084 /** The avalon role. */ 085 public static final String ROLE = OdfPageHandler.class.getName(); 086 087 /** First level attribute name. */ 088 public static final String LEVEL1_ATTRIBUTE_NAME = "firstLevel"; 089 090 /** Second level attribute name. */ 091 public static final String LEVEL2_ATTRIBUTE_NAME = "secondLevel"; 092 093 /** Catalog data name. */ 094 public static final String CATALOG_DATA_NAME = "odf-root-catalog"; 095 096 /** Content types that are not eligible for first and second level */ 097 // See ODF-1115 Exclude the mentions enumerator from the list : 098 protected static final List<String> NON_ELIGIBLE_CTYPES_FOR_LEVEL = Arrays.asList("org.ametys.plugins.odf.Content.programItem", "odf-enumeration.Mention"); 099 100 private static final String __ODF_ROOT_PAGES_CACHE = OdfPageHandler.class.getName() + "$odfRootPages"; 101 private static final String __HAS_ODF_ROOT_CACHE = OdfPageHandler.class.getName() + "$hasOdfRootPage"; 102 private static final String __PROGRAM_LEVEL_PATH_CACHE = OdfPageHandler.class.getName() + "$programLevelPath"; 103 private static final String __PROGRAM_RESTRICTION_CACHE = OdfPageHandler.class.getName() + "$programRestriction"; 104 105 private static final String __ROOT_CACHE_ALL_SITES_KEY = "ALL"; 106 private static final String __ROOT_CACHE_ALL_SITEMAPS_KEY = "ALL"; 107 108 /** The ametys object resolver. */ 109 protected AmetysObjectResolver _resolver; 110 111 /** The i18n utils. */ 112 protected I18nUtils _i18nUtils; 113 114 /** The content type extension point. */ 115 protected ContentTypeExtensionPoint _cTypeEP; 116 117 /** The ODF Catalog enumeration */ 118 protected CatalogsManager _catalogsManager; 119 120 /** The workspace selector. */ 121 protected WorkspaceSelector _workspaceSelector; 122 123 /** Avalon service manager */ 124 protected ServiceManager _manager; 125 126 /** Restriction manager */ 127 protected OdfProgramRestrictionManager _odfRestrictionsManager; 128 129 /** Content types helper */ 130 protected ContentTypesHelper _contentTypesHelper; 131 132 /** Content helper */ 133 protected ContentHelper _contentHelper; 134 135 /** Odf reference table helper */ 136 protected OdfReferenceTableHelper _odfReferenceTableHelper; 137 138 /** Root orgunit provider */ 139 protected RootOrgUnitProvider _orgUnitProvider; 140 141 /** Root orgunit provider */ 142 protected OdfClassificationHandler _odfClassificationHandler; 143 144 /** The cache manager */ 145 protected AbstractCacheManager _cacheManager; 146 147 @Override 148 public void service(ServiceManager serviceManager) throws ServiceException 149 { 150 _manager = serviceManager; 151 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 152 _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE); 153 _cTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 154 _workspaceSelector = (WorkspaceSelector) serviceManager.lookup(WorkspaceSelector.ROLE); 155 _catalogsManager = (CatalogsManager) serviceManager.lookup(CatalogsManager.ROLE); 156 _odfRestrictionsManager = (OdfProgramRestrictionManager) serviceManager.lookup(OdfProgramRestrictionManager.ROLE); 157 _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE); 158 _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE); 159 _odfReferenceTableHelper = (OdfReferenceTableHelper) serviceManager.lookup(OdfReferenceTableHelper.ROLE); 160 _orgUnitProvider = (RootOrgUnitProvider) serviceManager.lookup(RootOrgUnitProvider.ROLE); 161 _odfClassificationHandler = (OdfClassificationHandler) serviceManager.lookup(OdfClassificationHandler.ROLE); 162 _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE); 163 } 164 165 @Override 166 public void initialize() throws Exception 167 { 168 _cacheManager.createMemoryCache(__ODF_ROOT_PAGES_CACHE, 169 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_ROOT_PAGES_LABEL"), 170 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_ROOT_PAGES_DESCRIPTION"), 171 true, 172 null); 173 174 _cacheManager.createMemoryCache(__HAS_ODF_ROOT_CACHE, 175 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_HAS_ODF_ROOT_PAGE_LABEL"), 176 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_HAS_ODF_ROOT_PAGE_DESCRIPTION"), 177 true, 178 null); 179 180 _cacheManager.createRequestCache(__PROGRAM_LEVEL_PATH_CACHE, 181 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_LEVEL_PATH_LABEL"), 182 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_LEVEL_PATH_DESCRIPTION"), 183 false); 184 185 _cacheManager.createRequestCache(__PROGRAM_RESTRICTION_CACHE, 186 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_RESTRICTION_LABEL"), 187 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_PROGRAM_RESTRICTION_DESCRIPTION"), 188 false); 189 } 190 191 /** 192 * Get the first ODF root page. 193 * @param siteName The site name 194 * @param sitemapName The sitemap's name 195 * @return a ODF root page or null if not found. 196 * @throws AmetysRepositoryException if an error occurs 197 */ 198 public Page getOdfRootPage(String siteName, String sitemapName) throws AmetysRepositoryException 199 { 200 Set<Page> rootPages = getOdfRootPages(siteName, sitemapName); 201 return rootPages.isEmpty() ? null : rootPages.iterator().next(); 202 } 203 204 /** 205 * Get ODF root page of a specific catalog. 206 * @param siteName The site name 207 * @param sitemapName The sitemap name 208 * @param catalogName The catalog name 209 * @return the ODF root page or null if not found. 210 * @throws AmetysRepositoryException if an error occurs 211 */ 212 public Page getOdfRootPage(String siteName, String sitemapName, String catalogName) throws AmetysRepositoryException 213 { 214 String catalogToCompare = catalogName != null ? catalogName : ""; 215 216 for (Page odfRootPage : getOdfRootPages(siteName, sitemapName)) 217 { 218 if (catalogToCompare.equals(getCatalog(odfRootPage))) 219 { 220 return odfRootPage; 221 } 222 } 223 224 return null; 225 } 226 227 /** 228 * Get the id of ODF root pages 229 * @param siteName The site name 230 * @param sitemapName The sitemap name 231 * @return The ids of ODF root pages 232 * @throws AmetysRepositoryException if an error occurs. 233 */ 234 @Callable(rights = Callable.NO_CHECK_REQUIRED) // only retrieve page ids 235 public List<String> getOdfRootPageIds(String siteName, String sitemapName) throws AmetysRepositoryException 236 { 237 Set<Page> pages = getOdfRootPages(siteName, sitemapName); 238 return pages.stream().map(p -> p.getId()).collect(Collectors.toList()); 239 } 240 241 /** 242 * Get the ODF root pages. 243 * @param siteName the current site. 244 * @param sitemapName the current sitemap/language. 245 * @return the ODF root pages 246 * @throws AmetysRepositoryException if an error occurs. 247 */ 248 public Set<Page> getOdfRootPages(String siteName, String sitemapName) throws AmetysRepositoryException 249 { 250 Cache<OdfRootPageCacheKey, Set<String>> cache = _getOdfRootPagesCache(); 251 252 String workspaceName = _workspaceSelector.getWorkspace(); 253 254 Set<String> rootPageIds = cache.get(OdfRootPageCacheKey.of(workspaceName, Objects.toString(siteName, __ROOT_CACHE_ALL_SITES_KEY), Objects.toString(sitemapName, __ROOT_CACHE_ALL_SITEMAPS_KEY)), item -> { 255 return _getOdfRootPages(siteName, sitemapName) 256 .stream() 257 .map(Page::getId) 258 .collect(Collectors.toSet()); 259 }); 260 261 return rootPageIds.stream() 262 .map(id -> (Page) _resolver.resolveById(id)) 263 .collect(Collectors.toSet()); 264 } 265 266 /** 267 * Test if the given site has at least one sitemap with an odf root page. 268 * @param site the site to test. 269 * @return true if the site has at least one sitemap with an odf root page, false otherwise. 270 */ 271 public boolean hasOdfRootPage(Site site) 272 { 273 Cache<HasOdfRootPageCacheKey, Boolean> cache = _getHasOdfRootPageCache(); 274 275 String workspace = _workspaceSelector.getWorkspace(); 276 277 return cache.get(HasOdfRootPageCacheKey.of(workspace, site.getName()), item -> { 278 279 Iterator<Sitemap> sitemaps = site.getSitemaps().iterator(); 280 while (sitemaps.hasNext()) 281 { 282 String sitemapName = sitemaps.next().getName(); 283 284 if (!getOdfRootPages(site.getName(), sitemapName).isEmpty()) 285 { 286 return true; 287 } 288 } 289 290 return false; 291 }); 292 } 293 294 /** 295 * Determines if the program is part of the site restrictions 296 * @param rootPage The ODF root page 297 * @param program The program 298 * @return <code>true</code> the program is part of the site restrictions 299 */ 300 public boolean isValidRestriction(Page rootPage, Program program) 301 { 302 Cache<ProgramInRootCacheKey, Boolean> cache = _cacheManager.get(__PROGRAM_RESTRICTION_CACHE); 303 304 return cache.get(ProgramInRootCacheKey.of(rootPage.getId(), program.getId()), isValid -> { 305 // Check catalog 306 if (!program.getCatalog().equals(getCatalog(rootPage))) 307 { 308 return false; 309 } 310 311 // Check language 312 if (!program.getLanguage().equals(rootPage.getSitemapName())) 313 { 314 return false; 315 } 316 317 // Check site restrictions 318 OdfProgramRestriction restriction = _odfRestrictionsManager.getRestriction(rootPage); 319 if (restriction != null) 320 { 321 return restriction.contains(program); 322 } 323 324 return true; 325 }); 326 } 327 328 /** 329 * Clear the ODF root page cache. 330 */ 331 public void clearRootCache() 332 { 333 _getOdfRootPagesCache().invalidateAll(); 334 _getHasOdfRootPageCache().invalidateAll(); 335 } 336 337 /** 338 * Clear the ODF root page cache for a given site and language. 339 * @param siteName the current site. 340 * @param sitemapName the current sitemap/language. 341 */ 342 public void clearRootCache(String siteName, String sitemapName) 343 { 344 _getOdfRootPagesCache().invalidate(OdfRootPageCacheKey.of(null, siteName, sitemapName)); 345 _getHasOdfRootPageCache().invalidate(HasOdfRootPageCacheKey.of(null, siteName)); 346 } 347 348 /** 349 * Determines if the page is a ODF root page 350 * @param page The page to test 351 * @return true if the page is a ODF root page 352 */ 353 public boolean isODFRootPage (Page page) 354 { 355 if (page instanceof JCRAmetysObject) 356 { 357 try 358 { 359 JCRAmetysObject jcrPage = (JCRAmetysObject) page; 360 Node node = jcrPage.getNode(); 361 362 if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY)) 363 { 364 Value[] values = node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues(); 365 366 boolean hasValue = false; 367 for (int i = 0; i < values.length && !hasValue; i++) 368 { 369 hasValue = FirstLevelPageFactory.class.getName().equals(values[i].getString()); 370 } 371 372 return hasValue; 373 } 374 else 375 { 376 return false; 377 } 378 } 379 catch (RepositoryException e) 380 { 381 return false; 382 } 383 } 384 385 return false; 386 387 } 388 389 /** 390 * Get the ODF root page. 391 * @param siteName the current site. 392 * @param sitemapName the current sitemap/language. 393 * @return the ODF root page or null if not found. 394 * @throws AmetysRepositoryException if an error occurs. 395 */ 396 protected Page _getOdfRootPage(String siteName, String sitemapName) throws AmetysRepositoryException 397 { 398 Expression expression = new VirtualFactoryExpression(FirstLevelPageFactory.class.getName()); 399 String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null); 400 401 AmetysObjectIterable<Page> pages = _resolver.query(query); 402 Page page = pages.stream().findFirst().orElse(null); 403 404 return page; 405 } 406 407 /** 408 * Get the ODF root page. 409 * @param siteName the current site. 410 * @param sitemapName the current sitemap/language. 411 * @return the ODF root page or null if not found. 412 * @throws AmetysRepositoryException if an error occurs. 413 */ 414 protected Set<Page> _getOdfRootPages(String siteName, String sitemapName) throws AmetysRepositoryException 415 { 416 Expression expression = new VirtualFactoryExpression(FirstLevelPageFactory.class.getName()); 417 String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null); 418 419 return _resolver.<Page>query(query).stream().collect(Collectors.toSet()); 420 } 421 422 /** 423 * Get the catalog value of the ODF root page 424 * @param rootPage The ODF root page 425 * @return the catalog value 426 */ 427 public String getCatalog (Page rootPage) 428 { 429 return rootPage.getValue(CATALOG_DATA_NAME, StringUtils.EMPTY); 430 } 431 432 /** 433 * Get the first level metadata name. 434 * @param siteName the site name. 435 * @param sitemapName the sitemap name. 436 * @param catalog the current selected catalog. 437 * @return the first level metadata name. 438 */ 439 public String getLevel1Metadata(String siteName, String sitemapName, String catalog) 440 { 441 Page rootPage = getOdfRootPage(siteName, sitemapName, catalog); 442 443 return getLevel1Metadata(rootPage); 444 } 445 446 /** 447 * Get the first level metadata name. 448 * @param rootPage the ODF root page. 449 * @return the first level metadata name. 450 */ 451 public String getLevel1Metadata(Page rootPage) 452 { 453 return rootPage.getValue(LEVEL1_ATTRIBUTE_NAME); 454 } 455 456 /** 457 * Get the second level metadata name. 458 * @param siteName the site name. 459 * @param sitemapName the sitemap name. 460 * @param catalog the current selected catalog. 461 * @return the second level metadata name. 462 */ 463 public String getLevel2Metadata(String siteName, String sitemapName, String catalog) 464 { 465 Page rootPage = getOdfRootPage(siteName, sitemapName, catalog); 466 467 return getLevel2Metadata(rootPage); 468 } 469 470 /** 471 * Get the second level metadata name. 472 * @param rootPage the ODF root page. 473 * @return the second level metadata name. 474 */ 475 public String getLevel2Metadata(Page rootPage) 476 { 477 return rootPage.getValue(LEVEL2_ATTRIBUTE_NAME); 478 } 479 480 /** 481 * Get the first level metadata values (with translated label). 482 * @param siteName the site name. 483 * @param sitemapName the sitemap name. 484 * @param catalog the current selected catalog. 485 * @return the first level metadata values. 486 */ 487 public Map<String, LevelValue> getLevel1Values(String siteName, String sitemapName, String catalog) 488 { 489 Page rootPage = getOdfRootPage(siteName, sitemapName, catalog); 490 491 return getLevel1Values(rootPage); 492 } 493 494 /** 495 * Get the level value of a program by extracting and transforming the raw program value at the desired metadata path 496 * @param program The program 497 * @param levelMetaPath The desired metadata path that represent a level 498 * @return The final level value 499 */ 500 public String getProgramLevelValue(Program program, String levelMetaPath) 501 { 502 List<String> programLevelValue = _odfClassificationHandler.getProgramLevelValues(program, levelMetaPath); 503 return programLevelValue.isEmpty() ? null : programLevelValue.get(0); 504 } 505 506 /** 507 * Get the first level value of a program by extracting and transforming the raw program value 508 * @param rootPage The root page 509 * @param program The program 510 * @return The final level value or <code>null</code> if not found 511 */ 512 public String getProgramLevel1Value(Page rootPage, Program program) 513 { 514 String level1Metadata = getLevel1Metadata(rootPage); 515 if (StringUtils.isNotBlank(level1Metadata)) 516 { 517 List<String> programLevelValue = _odfClassificationHandler.getProgramLevelValues(program, level1Metadata); 518 return programLevelValue.isEmpty() ? null : programLevelValue.get(0); 519 } 520 else 521 { 522 return null; 523 } 524 } 525 526 /** 527 * Get the second level value of a program by extracting and transforming the raw program value 528 * @param rootPage The root page 529 * @param program The program 530 * @return The final level value or <code>null</code> if not found 531 */ 532 public String getProgramLevel2Value(Page rootPage, Program program) 533 { 534 String level2Metadata = getLevel2Metadata(rootPage); 535 if (StringUtils.isNotBlank(level2Metadata)) 536 { 537 List<String> programLevelValue = _odfClassificationHandler.getProgramLevelValues(program, level2Metadata); 538 return programLevelValue.isEmpty() ? null : programLevelValue.get(0); 539 } 540 else 541 { 542 return null; 543 } 544 } 545 546 /** 547 * Get the orgunit identifier given an uai code 548 * @param rootPage Odf root page 549 * @param uaiCode The uai code 550 * @return The orgunit id or null if not found 551 */ 552 public String getOrgunitIdFromUaiCode(Page rootPage, String uaiCode) 553 { 554 return _odfClassificationHandler.getOrgunitIdFromUaiCode(rootPage.getSitemapName(), uaiCode); 555 } 556 557 /** 558 * Get the programs available for a ODF root page, taking account the site's restrictions 559 * @param rootPage The ODF root page 560 * @param level1 filters results with a level1 value. Can be null. 561 * @param level2 filters results with a level2 value. Can be null. 562 * @param programCode expected program code. Can be null. 563 * @param programName expected program name. Can be null. 564 * @return an iterator over resulting programs 565 */ 566 public AmetysObjectIterable<Program> getProgramsWithRestrictions(Page rootPage, String level1, String level2, String programCode, String programName) 567 { 568 return getProgramsWithRestrictions(rootPage, getLevel1Metadata(rootPage), level1, getLevel2Metadata(rootPage), level2, programCode, programName); 569 } 570 571 /** 572 * Get the programs available for a ODF root page, taking account the site's restrictions 573 * @param rootPage The ODF root page 574 * @param level1Metadata metadata name for first level 575 * @param level1 filters results with a level1 value. Can be null. 576 * @param level2Metadata metadata name for second level 577 * @param level2 filters results with a level2 value. Can be null. 578 * @param programCode expected program code. Can be null. 579 * @param programName expected program name. Can be null. 580 * @return an iterator over resulting programs 581 */ 582 public AmetysObjectIterable<Program> getProgramsWithRestrictions(Page rootPage, String level1Metadata, String level1, String level2Metadata, String level2, String programCode, String programName) 583 { 584 OdfProgramRestriction restriction = _odfRestrictionsManager.getRestriction(rootPage); 585 return _odfClassificationHandler.getPrograms(getCatalog(rootPage), rootPage.getSitemapName(), level1Metadata, level1, level2Metadata, level2, programCode, programName, restriction == null ? null : ImmutableList.of(restriction.getExpression())); 586 } 587 588 /** 589 * Get the first level metadata values (with translated label) 590 * @param rootPage the ODF root page. 591 * @return the first level metadata values. Can be empty if there is no level1 attribute on root page. 592 */ 593 public Map<String, LevelValue> getLevel1Values(Page rootPage) 594 { 595 String level1Value = getLevel1Metadata(rootPage); 596 if (StringUtils.isNotBlank(level1Value)) 597 { 598 return _odfClassificationHandler.getLevelValues(level1Value, rootPage.getSitemapName()); 599 } 600 else 601 { 602 return Collections.EMPTY_MAP; 603 } 604 } 605 606 /** 607 * Get the second level metadata values (with translated label). 608 * @param siteName the site name. 609 * @param sitemapName the sitemap name. 610 * @param catalog the current selected catalog. 611 * @return the second level metadata values. 612 */ 613 public Map<String, LevelValue> getLevel2Values(String siteName, String sitemapName, String catalog) 614 { 615 Page rootPage = getOdfRootPage(siteName, sitemapName, catalog); 616 617 return getLevel2Values(rootPage); 618 } 619 620 /** 621 * Get the second level metadata values (with translated label). 622 * @param rootPage the ODF root page. 623 * @return the second level metadata values. Can be empty if there is no level2 attribute on root page. 624 */ 625 public Map<String, LevelValue> getLevel2Values(Page rootPage) 626 { 627 String level2Value = getLevel2Metadata(rootPage); 628 if (StringUtils.isNotBlank(level2Value)) 629 { 630 return _odfClassificationHandler.getLevelValues(level2Value, rootPage.getSitemapName()); 631 } 632 else 633 { 634 return Collections.EMPTY_MAP; 635 } 636 } 637 638 /** 639 * Encode level value to be use into a URI. 640 * @param value The raw value 641 * @return the encoded value 642 */ 643 public String encodeLevelValue(String value) 644 { 645 String encodedValue = StringUtils.replace(value, "-", "@2D"); 646 encodedValue = StringUtils.replace(encodedValue, "/", "@2F"); 647 encodedValue = StringUtils.replace(encodedValue, ":", "@3A"); 648 encodedValue = StringUtils.replace(encodedValue, "?", "@3F"); 649 return URIUtils.encodePathSegment(encodedValue); 650 } 651 652 /** 653 * Decode level value used in a URI 654 * @param value The encoded value 655 * @return the decoded value 656 */ 657 public String decodeLevelValue(String value) 658 { 659 String decodedValue = URIUtils.decode(value); 660 decodedValue = StringUtils.replace(decodedValue, "@3F", "?"); 661 decodedValue = StringUtils.replace(decodedValue, "@3A", ":"); 662 decodedValue = StringUtils.replace(decodedValue, "@2F", "/"); 663 return StringUtils.replace(decodedValue, "@2D", "-"); 664 } 665 666 /** 667 * Returns the page's name of a {@link ProgramItem}. 668 * Only {@link AbstractProgram} and {@link Course} can have a page. 669 * @param item The program item 670 * @return The page's name 671 * @throws IllegalArgumentException if the program item is not a {@link AbstractProgram} nor a {@link Course}. 672 */ 673 public String getPageName (ProgramItem item) 674 { 675 if (item instanceof AbstractProgram || item instanceof Course) 676 { 677 String filteredTitle = ""; 678 try 679 { 680 filteredTitle = NameHelper.filterName(((Content) item).getTitle()); 681 } 682 catch (IllegalArgumentException e) 683 { 684 // title does not match the expected regular expression : ^([0-9-_]*)[a-z].*$, use default title 685 if (item instanceof Program) 686 { 687 filteredTitle = "program"; 688 } 689 else if (item instanceof SubProgram) 690 { 691 filteredTitle = "subprogram"; 692 } 693 else if (item instanceof Course) 694 { 695 filteredTitle = "course"; 696 } 697 } 698 699 return filteredTitle + "-" + item.getCode(); 700 } 701 else 702 { 703 throw new IllegalArgumentException("Illegal program item : no page can be associated for a program item of type " + item.getClass().getName()); 704 } 705 } 706 707 /** 708 * Get the eligible enumerated attribute definitions for ODF page level 709 * @return the eligible attribute definitions 710 */ 711 public Map<String, ModelItem> getEligibleAttributesForLevel() 712 { 713 return _odfClassificationHandler.getEligibleAttributesForLevel(); 714 } 715 716 /** 717 * Get the ODF catalogs 718 * @return the ODF catalogs 719 */ 720 public Map<String, I18nizableText> getCatalogs() 721 { 722 return _odfClassificationHandler.getCatalogs(); 723 } 724 725 /** 726 * Get the enumerated attribute definitions for the given content type. 727 * Attribute with enumerator or content attribute are considered as enumerated 728 * @param programContentTypeId The content type's id 729 * @param allowMultiple <code>true</code> true to allow multiple attributes 730 * @return The definitions of enumerated attributes 731 */ 732 public Map<String, ModelItem> getEnumeratedAttributes(String programContentTypeId, boolean allowMultiple) 733 { 734 return _odfClassificationHandler.getEnumeratedAttributes(programContentTypeId, allowMultiple); 735 } 736 737 /** 738 * Compute the path from the root odf page, representing the first and second level pages. 739 * @param rootPage The odf root page 740 * @param parentProgram The program to compute 741 * @return the path, can be empty if no levels defined, and null if the parent program does not have values for levels attributes 742 */ 743 public String computeLevelsPath(Page rootPage, Program parentProgram) 744 { 745 Cache<ProgramInRootCacheKey, String> levelCache = _cacheManager.get(__PROGRAM_LEVEL_PATH_CACHE); 746 747 return levelCache.get(ProgramInRootCacheKey.of(rootPage.getId(), parentProgram.getId()), item -> { 748 // Level 1 is defined => Check the value 749 if (getLevel1Metadata(rootPage) != null) 750 { 751 String level1 = getProgramLevel1Value(rootPage, parentProgram); 752 753 // Value for level 1 is defined => Check for the second level 754 if (StringUtils.isNotBlank(level1)) 755 { 756 // Level 2 is defined => Check the value 757 if (getLevel2Metadata(rootPage) != null) 758 { 759 String level2 = getProgramLevel2Value(rootPage, parentProgram); 760 761 // Value for level 2 is defined => Return the level 2 page 762 if (StringUtils.isNotBlank(level2)) 763 { 764 Page secondLevelPage = findSecondLevelPage(rootPage, level1, level2); 765 return secondLevelPage.getParent().getName() + "/" + secondLevelPage.getName(); 766 } 767 768 // Value for level 2 is not defined => Return null 769 return null; 770 } 771 772 // Level 2 is not defined => Return the level 1 page 773 return findFirstLevelPage(rootPage, level1).getName(); 774 } 775 776 // Value for level 1 is not defined => Return null 777 return null; 778 } 779 780 // Level 1 is not defined => Return an empty path, all pages are on the root page 781 return StringUtils.EMPTY; 782 }); 783 } 784 785 /** 786 * Build the level 1 identifier 787 * @param rootPage The ODF root page 788 * @param level1Value The level1 name 789 * @return the identifier beginning by odfLevel1://... 790 */ 791 public String buildLevel1Id(Page rootPage, String level1Value) 792 { 793 // E.g: odfLevel1://XA?rootId=... 794 return "odfLevel1://" + encodeLevelValue(level1Value) + "?rootId=" + rootPage.getId(); 795 } 796 797 /** 798 * Build the level 2 identifier 799 * @param rootPage The ODF root page 800 * @param level1Value The level1 name 801 * @param level2Value The level2 name 802 * @return the identifier beginning by odfLevel2://... 803 */ 804 public String buildLevel2Id(Page rootPage, String level1Value, String level2Value) 805 { 806 // E.g: odfLevel2://XA/ALL?rootId=... 807 return "odfLevel2://" + encodeLevelValue(level1Value) + "/" + encodeLevelValue(level2Value) + "?rootId=" + rootPage.getId(); 808 } 809 810 /** 811 * Get the first level page from the given root page with the level 1 value. 812 * @param rootPage The odf root page 813 * @param level1Value The first level value 814 * @return a first level page 815 */ 816 public FirstLevelPage findFirstLevelPage(Page rootPage, String level1Value) 817 { 818 // Calculate the real path 819 return _resolver.resolveById(buildLevel1Id(rootPage, level1Value)); 820 } 821 822 /** 823 * Get the second level page from the given root page with the level 1 and level 2 values. 824 * @param rootPage The odf root page 825 * @param level1Value The first level value 826 * @param level2Value The second level value 827 * @return a second level page 828 */ 829 public SecondLevelPage findSecondLevelPage(Page rootPage, String level1Value, String level2Value) 830 { 831 // Calculate the real path 832 return _resolver.resolveById(buildLevel2Id(rootPage, level1Value, level2Value)); 833 } 834 835 /** 836 * Add an intermediate redirect page if the called page name doesn't match the real page name. 837 * @param page The page 838 * @param name The called page name 839 * @return The page maybe included in a {@link RedirectPage} 840 */ 841 public Page addRedirectIfNeeded(Page page, String name) 842 { 843 // Decode both because in the case of page.getName(), only the code is encoded (it is 844 // normal) and the name is already partially decoded before, so we encode both to be 845 // sure to compare the same values. 846 if (!decodeLevelValue(name).equals(decodeLevelValue(page.getName()))) 847 { 848 getLogger().warn("Redirect path '{}' to '{}' page", name, page.getName()); 849 return new RedirectPage(page instanceof RedirectPage redirectPage ? redirectPage.getRedirectPage() : page); 850 } 851 return page; 852 } 853 854 /** 855 * Explore the queue path if it is not empty 856 * @param page The root page to explore 857 * @param queuePath The path 858 * @return The child page, or given page if queue path is empty 859 */ 860 public Page exploreQueuePath(Page page, String queuePath) 861 { 862 if (StringUtils.isNotEmpty(queuePath)) 863 { 864 return page.getChild(queuePath); 865 } 866 return page; 867 } 868 869 private Cache<OdfRootPageCacheKey, Set<String>> _getOdfRootPagesCache() 870 { 871 return _cacheManager.get(__ODF_ROOT_PAGES_CACHE); 872 } 873 874 private Cache<HasOdfRootPageCacheKey, Boolean> _getHasOdfRootPageCache() 875 { 876 return _cacheManager.get(__HAS_ODF_ROOT_CACHE); 877 } 878 879 static class OdfRootPageCacheKey extends AbstractCacheKey 880 { 881 public OdfRootPageCacheKey(String workspaceName, String siteName, String sitemapName) 882 { 883 super(workspaceName, siteName, sitemapName); 884 } 885 886 public static OdfRootPageCacheKey of(String workspaceName, String siteName, String sitemapName) 887 { 888 return new OdfRootPageCacheKey(workspaceName, siteName, sitemapName); 889 } 890 } 891 892 static class HasOdfRootPageCacheKey extends AbstractCacheKey 893 { 894 public HasOdfRootPageCacheKey(String workspaceName, String siteName) 895 { 896 super(workspaceName, siteName); 897 } 898 899 public static HasOdfRootPageCacheKey of(String workspaceName, String siteName) 900 { 901 return new HasOdfRootPageCacheKey(workspaceName, siteName); 902 } 903 } 904 905 static class ProgramInRootCacheKey extends AbstractCacheKey 906 { 907 public ProgramInRootCacheKey(String rootPageId, String programId) 908 { 909 super(rootPageId, programId); 910 } 911 912 public static ProgramInRootCacheKey of(String rootPageId, String programId) 913 { 914 return new ProgramInRootCacheKey(rootPageId, programId); 915 } 916 } 917}